#include "platform/esp/common/storage/snapshot_stream_sha256.h" #include "track_sharing/file_integrity.h" #include "track_sharing/image_source_binding_store.h" #include #include #include #include #include #if defined(_WIN32) extern "C" void mbedtls_platform_zeroize(void* pointer, size_t length) { auto* bytes = static_cast(pointer); while (length--) *bytes++ = 0; } #endif struct Io { enum class Mode { Read, CreateTemporary }; enum class OpenStatus { Ready, Missing, Unavailable }; struct OpenResult { void* handle; OpenStatus status; }; std::map> files; std::string path; size_t offset = 0; unsigned operations = 0, fail_at = 0; bool active = false, short_write = false; uint64_t reported_size = 0; bool fail() { return ++operations == fail_at; } OpenResult open(const char* name, Mode mode) { assert(!active); if (fail()) return {nullptr, OpenStatus::Unavailable}; path = name; offset = 0; if (mode == Mode::Read && !files.count(path)) return {nullptr, OpenStatus::Missing}; if (mode == Mode::CreateTemporary) files[path].clear(); active = true; return {this, OpenStatus::Ready}; } bool close(void*) { active = false; return !fail(); } bool size(void*, uint64_t& size) { if (fail()) return false; size = reported_size ? reported_size : files[path].size(); return true; } bool seek(void*, uint64_t at) { if (fail()) return false; offset = size_t(at); return offset <= files[path].size(); } size_t read(void*, uint8_t* output, size_t length) { if (fail()) return 0; length = std::min(length, files[path].size() - offset); std::memcpy(output, files[path].data() + offset, length); offset += length; return length; } size_t write(void*, const uint8_t* input, size_t length) { if (fail()) return 0; if (short_write) length /= 2; files[path].assign(input, input + length); return length; } bool sync(void*) { return !fail(); } bool remove(const char* name) { assert(!active); return !fail() && files.erase(name) == 1; } bool rename(const char* from, const char* to) { assert(!active); if (fail() || !files.count(from) || files.count(to)) return false; files[to] = std::move(files[from]); files.erase(from); return true; } }; using Result = track_sharing::ImageBindingResult; const track_sharing::ImageBindingPaths paths = {"image.tmp", "image.jpg", "source.new", "source.meta", "thumb.bmp", "view.bmp"}; std::array digest(const std::vector& bytes) { std::array hash{}; mbedtls_sha256_context context; platform::esp::storage::SnapshotStreamSha256 stream(context); assert(stream.start() && stream.update(bytes.data(), bytes.size()) && stream.finish(hash.data())); return hash; } std::vector record(const track_sharing::ImageSourceBindingView& value) { std::vector output(track_sharing::kImageSourceBindingBytes); assert(track_sharing::encodeImageSourceBinding(value, output.data(), output.size())); return output; } int main() { const std::vector old_image = {1, 2, 3}, new_image = {4, 5, 6, 7, 8}, old_reference = {'a'}, new_reference = {'b'}; const auto old_file = digest(old_image), new_file = digest(new_image), old_source = digest(old_reference), new_source = digest(new_reference); // Independent Python hashlib SHA-256 fixture for bytes 04 05 06 07 08. const std::array golden = {0x01, 0x5d, 0xaf, 0x60, 0x13, 0x37, 0x6b, 0x77, 0x42, 0x1b, 0x7f, 0xb9, 0xd5, 0x65, 0x4d, 0x84, 0x4c, 0x8c, 0xba, 0x4f, 0x7a, 0x53, 0x26, 0xd8, 0x29, 0x42, 0x6d, 0x87, 0xfd, 0x0d, 0x1a, 0x5f}; assert(new_file == golden); const track_sharing::ImageSourceBindingView old_binding = {{old_source.data(), 32}, {old_file.data(), 32}, 3}, expected = {{new_source.data(), 32}, {new_file.data(), 32}, 5}; const auto old_record = record(old_binding), new_record = record(expected); assert(new_record.size() == 75 && !std::memcmp(new_record.data(), "TMSI", 4)); for (size_t i = 0; i < new_record.size(); ++i) { auto corrupt = new_record; corrupt[i] ^= 1; track_sharing::ImageSourceBindingView view; assert(!track_sharing::decodeImageSourceBinding(corrupt.data(), corrupt.size(), view) && !view.file_bytes); } auto oversized = expected; oversized.file_bytes = track_sharing::kImageSourceByteLimit + 1; uint8_t scratch[128]{}; assert(!track_sharing::encodeImageSourceBinding(oversized, scratch, sizeof(scratch))); oversized.file_bytes = 0; assert(!track_sharing::encodeImageSourceBinding(oversized, scratch, sizeof(scratch))); track_sharing::ImageSourceBindingView alias; assert(track_sharing::decodeImageSourceBinding(new_record.data(), new_record.size(), alias)); auto aliased = new_record; track_sharing::ImageSourceBindingView aliased_view; assert(track_sharing::decodeImageSourceBinding(aliased.data(), aliased.size(), aliased_view)); assert(!track_sharing::encodeImageSourceBinding(aliased_view, aliased.data(), aliased.size())); auto initial = [&]() {Io io;io.files={{paths.image,old_image},{paths.binding,old_record},{paths.candidate_image,new_image},{paths.preview,{9}},{paths.view,{10}}};return io; }; unsigned terminal_operations = 0; for (unsigned failure = 0; failure < 50; ++failure) { auto io = initial(); mbedtls_sha256_context context; platform::esp::storage::SnapshotStreamSha256 hash(context); track_sharing::FileIntegrityCheck verified(io, hash); assert(verified.begin(paths.candidate_image, new_image.size(), new_file.data()) == track_sharing::FileIntegrityResult::Pending); while (verified.active()) verified.step(scratch, sizeof(scratch)); assert(verified.matchesSource(paths.candidate_image, 5, new_file.data())); assert(!verified.matchesSource("other.tmp", 5, new_file.data())); assert(!verified.matchesSource(paths.candidate_image, 4, new_file.data())); assert(!verified.matchesSource(paths.candidate_image, 5, old_file.data())); track_sharing::ImageSourceBindingStore store(io); static_assert(sizeof(store) == sizeof(void*), "Store retained metadata or buffers"); io.operations = 0; io.fail_at = failure; const auto status = store.commit(paths, expected, verified, scratch, sizeof(scratch)); assert(!io.active); if (!failure) { assert(status == Result::Ready); terminal_operations = io.operations; } else if (failure <= terminal_operations) assert(status != Result::Ready); else { assert(status == Result::Ready); break; } // Whatever failed, a published binding must describe its image bytes. if (io.files.count(paths.binding)) { track_sharing::ImageSourceBindingView saved; assert(track_sharing::decodeImageSourceBinding(io.files.at(paths.binding).data(), 75, saved)); assert(io.files.count(paths.image)); const auto actual = digest(io.files.at(paths.image)); assert(saved.file_bytes == io.files.at(paths.image).size() && !std::memcmp(saved.file_hash.data, actual.data(), 32)); if (io.files.at(paths.image) == new_image) assert(!io.files.count(paths.preview) && !io.files.count(paths.view)); } if (status == Result::Ready) { assert(io.files.at(paths.image) == new_image && io.files.at(paths.binding) == new_record); assert(!io.files.count(paths.candidate_image) && !io.files.count(paths.candidate_binding)); // Reusing a proof after its candidate was renamed must not delete // the published image on retry. io.fail_at = 0; const auto before = io.files; assert(store.commit(paths, expected, verified, scratch, sizeof(scratch)) == Result::Unverified && io.files == before); } } auto io = initial(); mbedtls_sha256_context context; platform::esp::storage::SnapshotStreamSha256 hash(context); track_sharing::FileIntegrityCheck unverified(io, hash); track_sharing::ImageSourceBindingStore store(io); const auto before = io.files; assert(store.commit(paths, expected, unverified, scratch, sizeof(scratch)) == Result::Unverified && io.files == before); assert(unverified.begin(paths.candidate_image, 5, new_file.data()) == track_sharing::FileIntegrityResult::Pending); while (unverified.active()) unverified.step(scratch, sizeof(scratch)); auto alias_paths = paths; alias_paths.binding = "IMAGE.JPG"; assert(store.commit(alias_paths, expected, unverified, scratch, sizeof(scratch)) == Result::Invalid && io.files == before); io.short_write = true; assert(store.commit(paths, expected, unverified, scratch, sizeof(scratch)) == Result::StorageFailure); assert(io.files.at(paths.image) == old_image && io.files.at(paths.binding) == old_record); io.short_write = false; track_sharing::ImageSourceBindingView loaded; assert(store.read(paths.binding, scratch, sizeof(scratch), loaded) == Result::Ready && track_sharing::imageSourceBindingMatches(loaded, old_binding)); io.reported_size = uint64_t(UINT32_MAX) + 1 + 75; assert(store.read(paths.binding, scratch, sizeof(scratch), loaded) == Result::Corrupt && !loaded.file_bytes); io.reported_size = 0; io.files.erase(paths.binding); assert(store.read(paths.binding, scratch, sizeof(scratch), loaded) == Result::Missing && !loaded.file_bytes); }