diff options
author | Tao Bao <tbao@google.com> | 2016-12-28 19:05:45 +0100 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2016-12-28 19:05:45 +0100 |
commit | 344c8eb453f162745cde8f7ae9d7506c6a3ec0ca (patch) | |
tree | 500cce081dab6124dd7320f160f54291dc00013f /tests | |
parent | Merge "tests: Add test coverage for DirUtil." (diff) | |
parent | Add tests for imgdiff. (diff) | |
download | android_bootable_recovery-344c8eb453f162745cde8f7ae9d7506c6a3ec0ca.tar android_bootable_recovery-344c8eb453f162745cde8f7ae9d7506c6a3ec0ca.tar.gz android_bootable_recovery-344c8eb453f162745cde8f7ae9d7506c6a3ec0ca.tar.bz2 android_bootable_recovery-344c8eb453f162745cde8f7ae9d7506c6a3ec0ca.tar.lz android_bootable_recovery-344c8eb453f162745cde8f7ae9d7506c6a3ec0ca.tar.xz android_bootable_recovery-344c8eb453f162745cde8f7ae9d7506c6a3ec0ca.tar.zst android_bootable_recovery-344c8eb453f162745cde8f7ae9d7506c6a3ec0ca.zip |
Diffstat (limited to '')
-rw-r--r-- | tests/Android.mk | 29 | ||||
-rw-r--r-- | tests/component/imgdiff_test.cpp | 448 |
2 files changed, 477 insertions, 0 deletions
diff --git a/tests/Android.mk b/tests/Android.mk index 17e2b7f5b..0aca8c6c7 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -65,6 +65,7 @@ LOCAL_SRC_FILES := \ component/applypatch_test.cpp \ component/bootloader_message_test.cpp \ component/edify_test.cpp \ + component/imgdiff_test.cpp \ component/uncrypt_test.cpp \ component/updater_test.cpp \ component/verifier_test.cpp @@ -83,6 +84,9 @@ LOCAL_STATIC_LIBRARIES := \ libapplypatch_modes \ libapplypatch \ libedify \ + libimgdiff \ + libimgpatch \ + libbsdiff \ libotafault \ libupdater \ libbootloader_message \ @@ -90,6 +94,8 @@ LOCAL_STATIC_LIBRARIES := \ libminui \ libotautil \ libmounts \ + libdivsufsort \ + libdivsufsort64 \ libfs_mgr \ liblog \ libselinux \ @@ -130,3 +136,26 @@ LOCAL_GENERATED_SOURCES += $(GEN) LOCAL_PICKUP_FILES := $(testdata_continuous_zip_prefix) include $(BUILD_NATIVE_TEST) + +# Host tests +include $(CLEAR_VARS) +LOCAL_CFLAGS := -Werror +LOCAL_MODULE := recovery_host_test +LOCAL_MODULE_HOST_OS := linux +LOCAL_C_INCLUDES := bootable/recovery +LOCAL_SRC_FILES := \ + component/imgdiff_test.cpp +LOCAL_STATIC_LIBRARIES := \ + libimgdiff \ + libimgpatch \ + libbsdiff \ + libziparchive \ + libbase \ + libcrypto \ + libbz \ + libdivsufsort64 \ + libdivsufsort \ + libz +LOCAL_SHARED_LIBRARIES := \ + liblog +include $(BUILD_HOST_NATIVE_TEST) diff --git a/tests/component/imgdiff_test.cpp b/tests/component/imgdiff_test.cpp new file mode 100644 index 000000000..3711859de --- /dev/null +++ b/tests/component/imgdiff_test.cpp @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/test_utils.h> +#include <applypatch/imgdiff.h> +#include <applypatch/imgpatch.h> +#include <gtest/gtest.h> +#include <ziparchive/zip_writer.h> + +#include "applypatch/utils.h" + +static ssize_t MemorySink(const unsigned char* data, ssize_t len, void* token) { + std::string* s = static_cast<std::string*>(token); + s->append(reinterpret_cast<const char*>(data), len); + return len; +} + +// Sanity check for the given imgdiff patch header. +static void verify_patch_header(const std::string& patch, size_t* num_normal, size_t* num_raw, + size_t* num_deflate) { + const size_t size = patch.size(); + const char* data = patch.data(); + + ASSERT_GE(size, 12U); + ASSERT_EQ("IMGDIFF2", std::string(data, 8)); + + const int num_chunks = Read4(data + 8); + ASSERT_GE(num_chunks, 0); + + size_t normal = 0; + size_t raw = 0; + size_t deflate = 0; + + size_t pos = 12; + for (int i = 0; i < num_chunks; ++i) { + ASSERT_LE(pos + 4, size); + int type = Read4(data + pos); + pos += 4; + if (type == CHUNK_NORMAL) { + pos += 24; + ASSERT_LE(pos, size); + normal++; + } else if (type == CHUNK_RAW) { + ASSERT_LE(pos + 4, size); + ssize_t data_len = Read4(data + pos); + ASSERT_GT(data_len, 0); + pos += 4 + data_len; + ASSERT_LE(pos, size); + raw++; + } else if (type == CHUNK_DEFLATE) { + pos += 60; + ASSERT_LE(pos, size); + deflate++; + } else { + FAIL() << "Invalid patch type: " << type; + } + } + + if (num_normal != nullptr) *num_normal = normal; + if (num_raw != nullptr) *num_raw = raw; + if (num_deflate != nullptr) *num_deflate = deflate; +} + +TEST(ImgdiffTest, invalid_args) { + // Insufficient inputs. + ASSERT_EQ(2, imgdiff(1, (const char* []){ "imgdiff" })); + ASSERT_EQ(2, imgdiff(2, (const char* []){ "imgdiff", "-z" })); + ASSERT_EQ(2, imgdiff(2, (const char* []){ "imgdiff", "-b" })); + ASSERT_EQ(2, imgdiff(3, (const char* []){ "imgdiff", "-z", "-b" })); + + // Failed to read bonus file. + ASSERT_EQ(1, imgdiff(3, (const char* []){ "imgdiff", "-b", "doesntexist" })); + + // Failed to read input files. + ASSERT_EQ(1, imgdiff(4, (const char* []){ "imgdiff", "doesntexist", "doesntexist", "output" })); + ASSERT_EQ( + 1, imgdiff(5, (const char* []){ "imgdiff", "-z", "doesntexist", "doesntexist", "output" })); +} + +TEST(ImgdiffTest, image_mode_smoke) { + // Random bytes. + const std::string src("abcdefg"); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + const std::string tgt("abcdefgxyz"); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect one CHUNK_RAW entry. + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(0U, num_deflate); + ASSERT_EQ(1U, num_raw); + + std::string patched; + ASSERT_EQ(0, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(), + reinterpret_cast<const unsigned char*>(patch.data()), patch.size(), + MemorySink, &patched)); + ASSERT_EQ(tgt, patched); +} + +TEST(ImgdiffTest, zip_mode_smoke_store) { + // Construct src and tgt zip files. + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.fd, "wb"); + ZipWriter src_writer(src_file_ptr); + ASSERT_EQ(0, src_writer.StartEntry("file1.txt", 0)); // Store mode. + const std::string src_content("abcdefg"); + ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size())); + ASSERT_EQ(0, src_writer.FinishEntry()); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.fd, "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", 0)); // Store mode. + const std::string tgt_content("abcdefgxyz"); + ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size())); + ASSERT_EQ(0, tgt_writer.FinishEntry()); + ASSERT_EQ(0, tgt_writer.Finish()); + ASSERT_EQ(0, fclose(tgt_file_ptr)); + + // Compute patch. + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + std::string src; + ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src)); + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect one CHUNK_RAW entry. + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(0U, num_deflate); + ASSERT_EQ(1U, num_raw); + + std::string patched; + ASSERT_EQ(0, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(), + reinterpret_cast<const unsigned char*>(patch.data()), patch.size(), + MemorySink, &patched)); + ASSERT_EQ(tgt, patched); +} + +TEST(ImgdiffTest, zip_mode_smoke_compressed) { + // Construct src and tgt zip files. + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.fd, "wb"); + ZipWriter src_writer(src_file_ptr); + ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress)); + const std::string src_content("abcdefg"); + ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size())); + ASSERT_EQ(0, src_writer.FinishEntry()); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.fd, "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress)); + const std::string tgt_content("abcdefgxyz"); + ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size())); + ASSERT_EQ(0, tgt_writer.FinishEntry()); + ASSERT_EQ(0, tgt_writer.Finish()); + ASSERT_EQ(0, fclose(tgt_file_ptr)); + + // Compute patch. + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + std::string src; + ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src)); + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect three entries: CHUNK_RAW (header) + CHUNK_DEFLATE (data) + CHUNK_RAW (footer). + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(1U, num_deflate); + ASSERT_EQ(2U, num_raw); + + std::string patched; + ASSERT_EQ(0, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(), + reinterpret_cast<const unsigned char*>(patch.data()), patch.size(), + MemorySink, &patched)); + ASSERT_EQ(tgt, patched); +} + +TEST(ImgdiffTest, image_mode_simple) { + // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd). + const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e', + '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac', + '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03', + '\x00', '\x00', '\x00' }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: "abcdefgxyz" + gzipped "xxyyzz". + const std::vector<char> tgt_data = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z', '\x1f', '\x8b', + '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xa8', '\xac', + '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00', '\x00', '\x00' + }; + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect three entries: CHUNK_RAW (header) + CHUNK_DEFLATE (data) + CHUNK_RAW (footer). + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(1U, num_deflate); + ASSERT_EQ(2U, num_raw); + + std::string patched; + ASSERT_EQ(0, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(), + reinterpret_cast<const unsigned char*>(patch.data()), patch.size(), + MemorySink, &patched)); + ASSERT_EQ(tgt, patched); +} + +TEST(ImgdiffTest, image_mode_different_num_chunks) { + // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd) + gzipped "test". + const std::vector<char> src_data = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', '\x1f', '\x8b', '\x08', + '\x00', '\xc4', '\x1e', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac', '\x02', + '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03', '\x00', '\x00', '\x00', '\x1f', '\x8b', + '\x08', '\x00', '\xb2', '\x3a', '\x53', '\x58', '\x00', '\x03', '\x2b', '\x49', '\x2d', + '\x2e', '\x01', '\x00', '\x0c', '\x7e', '\x7f', '\xd8', '\x04', '\x00', '\x00', '\x00' + }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: "abcdefgxyz" + gzipped "xxyyzz". + const std::vector<char> tgt_data = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z', '\x1f', '\x8b', + '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xa8', '\xac', + '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00', '\x00', '\x00' + }; + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(1, imgdiff(args.size(), args.data())); +} + +TEST(ImgdiffTest, image_mode_merge_chunks) { + // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd). + const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e', + '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac', + '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03', + '\x00', '\x00', '\x00' }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: gzipped "xyz" + "abcdefgh". + const std::vector<char> tgt_data = { + '\x1f', '\x8b', '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', + '\xa8', '\xac', '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00', + '\x00', '\x00', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' + }; + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + // Since a gzipped entry will become CHUNK_RAW (header) + CHUNK_DEFLATE (data) + + // CHUNK_RAW (footer), they both should contain the same chunk types after merging. + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect three entries: CHUNK_RAW (header) + CHUNK_DEFLATE (data) + CHUNK_RAW (footer). + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(1U, num_deflate); + ASSERT_EQ(2U, num_raw); + + std::string patched; + ASSERT_EQ(0, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(), + reinterpret_cast<const unsigned char*>(patch.data()), patch.size(), + MemorySink, &patched)); + ASSERT_EQ(tgt, patched); +} + +TEST(ImgdiffTest, image_mode_spurious_magic) { + // src: "abcdefgh" + '0x1f8b0b00' + some bytes. + const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e', + '\x53', '\x58', 't', 'e', 's', 't' }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: "abcdefgxyz". + const std::vector<char> tgt_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' }; + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect one CHUNK_RAW (header) entry. + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(0U, num_normal); + ASSERT_EQ(0U, num_deflate); + ASSERT_EQ(1U, num_raw); + + std::string patched; + ASSERT_EQ(0, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(), + reinterpret_cast<const unsigned char*>(patch.data()), patch.size(), + MemorySink, &patched)); + ASSERT_EQ(tgt, patched); +} + +TEST(ImgdiffTest, image_mode_single_entry_long) { + // src: "abcdefgh" + '0x1f8b0b00' + some bytes. + const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e', + '\x53', '\x58', 't', 'e', 's', 't' }; + const std::string src(src_data.cbegin(), src_data.cend()); + TemporaryFile src_file; + ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); + + // tgt: "abcdefgxyz" + 200 bytes. + std::vector<char> tgt_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' }; + tgt_data.resize(tgt_data.size() + 200); + + const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + TemporaryFile tgt_file; + ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); + + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + // Expect one CHUNK_NORMAL entry, since it's exceeding the 160-byte limit for RAW. + size_t num_normal; + size_t num_raw; + size_t num_deflate; + verify_patch_header(patch, &num_normal, &num_raw, &num_deflate); + ASSERT_EQ(1U, num_normal); + ASSERT_EQ(0U, num_deflate); + ASSERT_EQ(0U, num_raw); + + std::string patched; + ASSERT_EQ(0, ApplyImagePatch(reinterpret_cast<const unsigned char*>(src.data()), src.size(), + reinterpret_cast<const unsigned char*>(patch.data()), patch.size(), + MemorySink, &patched)); + ASSERT_EQ(tgt, patched); +} |