#include "platform/esp/arduino_common/hostlink/hostlink_management_service.h" #include "platform/esp/arduino_common/hostlink/web_geocache_confirmation.h" #include "platform/esp/arduino_common/hostlink/web_geocache_signature.h" #include #include #include static std::vector response; static uint16_t response_sequence; static unsigned draft_reads = 0; static uint64_t draft_expected = 0; static unsigned draft_put_queues = 0; static bool draft_put_release = false; using WebResult = platform::esp::arduino_common::geocaching::browse_runtime::WebReadResult; static WebResult job_current_result = WebResult::Ready, job_history_result = WebResult::Ready; static unsigned job_current_reads = 0, job_history_reads = 0; static WebResult job_stop_result = WebResult::Ready; static unsigned job_stop_calls = 0; static unsigned job_queue_calls = 0, job_admissions = 0; static bool job_queue_admit = true; static unsigned local_signature_queues = 0; static std::array local_signature_operation{}; static std::array local_signature_hash{}; namespace platform::esp::arduino_common::geocaching::browse_runtime { WebReadResult queueWebSignature(const uint8_t* operation, const uint8_t*, const uint8_t* transfer, const uint8_t* hash, uint32_t total, const uint8_t* author, uint32_t generation, const uint8_t*, uint64_t) { assert(transfer[0] == 33 && total == 100 && generation == 7); for (unsigned i = 0; i < 64; ++i) assert(author[i] == i); std::memcpy(local_signature_operation.data(), operation, 16); std::memcpy(local_signature_hash.data(), hash, 32); ++local_signature_queues; return WebReadResult::Ready; } WebReadResult readWebSignature(const uint8_t* operation, const uint8_t*, bool status, uint32_t, uint16_t, uint8_t* output, size_t capacity, size_t& written) { assert(status && !std::memcmp(operation, local_signature_operation.data(), 16)); ::geocaching::protocol::CmpWriter writer(output, capacity); assert(writer.array(5) && writer.binary({local_signature_operation.data(), 16}) && writer.binary({local_signature_hash.data(), 32}) && writer.unsignedInteger(7) && writer.unsignedInteger(0) && writer.unsignedInteger(0)); written = writer.size(); return WebReadResult::Ready; } WebReadResult releaseWebSignature(const uint8_t*, const uint8_t*) { return WebReadResult::Ready; } WebReadResult queueWebConfirmation(const uint8_t* transfer, const uint8_t*, const uint8_t* hash, uint32_t total, const uint8_t*, uint32_t generation, const uint8_t* proof, size_t proof_size) { assert(transfer[0] == 33 && hash[0] == 44 && total == 100 && generation == 7 && proof && proof_size > 0); ++local_signature_queues; return WebReadResult::Ready; } WebReadResult readWebConfirmation(const uint8_t* transfer, const uint8_t*, uint8_t* output, size_t capacity, size_t& written) { ::geocaching::protocol::CmpWriter writer(output, capacity); assert(writer.array(8) && writer.binary({transfer, 16}) && writer.binary({local_signature_hash.data(), 32}) && writer.unsignedInteger(7) && writer.unsignedInteger(0) && writer.nil() && writer.nil() && writer.nil() && writer.nil()); written = writer.size(); return WebReadResult::Ready; } WebReadResult releaseWebConfirmation(const uint8_t*, const uint8_t*) { return WebReadResult::Ready; } } // namespace platform::esp::arduino_common::geocaching::browse_runtime static std::array job_operation{}; static WebResult draft_put_check = WebResult::Ready, draft_put_queue = WebResult::Pending; static WebResult prepare_binding_result = WebResult::Ready, prepare_latest_result = WebResult::Pending; static std::array prepare_binding{}; static std::array prepare_latest{}; static uint64_t prepare_clock = 1000; static uint32_t commit_binding_delay = 0; static uint8_t prepare_boot = 1; static unsigned prepare_network_reads = 0; static platform::esp::arduino_common::geocaching::browse_runtime::WebReadResult draft_result = platform::esp::arduino_common::geocaching::browse_runtime::WebReadResult::Pending; namespace platform::esp::arduino_common::geocaching::browse_runtime { bool webLease(bool, bool) { return true; } WebReadResult requestWebPublicationStop(const uint8_t* id, const uint8_t* author) { ++job_stop_calls; assert(!std::memcmp(id, job_operation.data(), 16)); for (unsigned i = 0; i < 64; ++i) assert(author[i] == i); return job_stop_result; } WebReadResult readWebPublicationJob(const uint8_t* id, const uint8_t* author, uint8_t* output, size_t capacity, size_t& written) { ++job_current_reads; assert(!std::memcmp(id, job_operation.data(), 16)); for (unsigned i = 0; i < 64; ++i) assert(author[i] == i); if (job_current_result != WebReadResult::Ready) return job_current_result; std::array hash{}; ::geocaching::protocol::CmpWriter writer(output, capacity); assert(writer.array(8) && writer.binary({id, 16}) && writer.unsignedInteger(3) && writer.binary({hash.data(), 32}) && writer.binary({hash.data(), 32}) && writer.unsignedInteger(1) && writer.unsignedInteger(1) && writer.unsignedInteger(1) && writer.nil()); written = writer.size(); return WebReadResult::Ready; } WebReadResult readWebPublicationHistory(const uint8_t* id, const uint8_t* author, uint8_t* output, size_t capacity, size_t& written) { ++job_history_reads; if (job_history_result != WebReadResult::Ready) return job_history_result; const auto current = job_current_result; job_current_result = WebReadResult::Ready; const auto result = readWebPublicationJob(id, author, output, capacity, written); --job_current_reads; job_current_result = current; return result; } bool publicationContext(uint8_t* boot, uint64_t& clock) { std::memset(boot, prepare_boot, 16); clock = prepare_clock; return true; } WebReadResult readWebPublicationBinding(const uint8_t*, uint64_t, const uint8_t*, const uint8_t*, uint8_t* output, size_t capacity) { assert(capacity == 88 || capacity == 56); if (capacity == 56) prepare_clock += commit_binding_delay; if (prepare_binding_result == WebReadResult::Ready) std::memcpy(output, prepare_binding.data(), capacity); return prepare_binding_result; } bool queueWebDraftPublication(const uint8_t*, uint64_t generation, const uint8_t*, const uint8_t* author, uint32_t revision, const uint8_t* binding, const uint8_t* operation) { ++job_queue_calls; assert(generation > 0 && revision > 0 && !std::memcmp(binding, prepare_binding.data(), 56)); assert(!std::memcmp(operation, job_operation.data(), 16)); for (unsigned i = 0; i < 64; ++i) assert(author[i] == i); assert(platform::esp::arduino_common::storage::fake_files.at(hostlink::management::kPublicationTicketPath)[4] == 1); if (job_queue_admit) { ++job_admissions; job_current_result = WebResult::Ready; } return job_queue_admit; } WebReadResult readWebLatestRevision(const uint8_t*, const uint8_t*, uint8_t* output, size_t capacity) { ++prepare_network_reads; assert(capacity == 36); std::memcpy(output, prepare_latest.data(), prepare_latest.size()); return prepare_latest_result; } WebReadResult readWebDirectories(uint32_t offset, uint8_t* output, size_t capacity, size_t& written) { ::geocaching::protocol::CmpWriter writer(output, capacity); assert(writer.array(3) && writer.unsignedInteger(offset) && writer.unsignedInteger(0) && writer.nil()); written = writer.size(); return WebReadResult::Ready; } bool releaseWebDraftSave() { return draft_put_release; } WebReadResult checkWebDraftEditOwner(const uint8_t*, uint64_t, const uint8_t* author, const uint8_t*) { for (unsigned i = 0; i < 64; ++i) assert(author[i] == i); return draft_put_check; } WebReadResult queueWebDraftFile(const uint8_t*, uint32_t, uint64_t, const uint8_t*, const uint8_t* author, uint32_t generation) { for (unsigned i = 0; i < 64; ++i) assert(author[i] == i); assert(generation == 7); ++draft_put_queues; return draft_put_queue; } WebReadResult readWebDraftCatalog(uint32_t, bool, uint64_t, uint8_t* output, size_t, size_t& written) { output[0] = 0x94; output[1] = 1; output[2] = 0; output[3] = 0; output[4] = 0xc0; written = 5; return WebReadResult::Ready; } WebReadResult readWebDraftChunk(const uint8_t id[16], uint32_t offset, uint16_t count, uint64_t expected, uint8_t* output, size_t capacity, size_t& written) { ++draft_reads; draft_expected = expected; if (draft_result != WebReadResult::Ready) return draft_result; const uint8_t data[] = {1, 2, 3}; ::geocaching::protocol::CmpWriter writer(output, capacity); assert(offset == 0 && count == 320); assert(writer.array(5) && writer.binary({id, 16}) && writer.unsignedInteger(7) && writer.unsignedInteger(sizeof(data)) && writer.unsignedInteger(offset) && writer.binary({data, sizeof(data)})); written = writer.size(); return WebReadResult::Ready; } } // namespace platform::esp::arduino_common::geocaching::browse_runtime struct FakeAuthor final : chat::IMeshAdapter { mutable std::vector message; chat::DeviceAuthorResult failure = chat::DeviceAuthorResult::Ok; chat::DeviceAuthorResult readDeviceAuthorKey(uint8_t* public_key, uint32_t& generation) const override { if (failure != chat::DeviceAuthorResult::Ok) return failure; for (unsigned index = 0; index < 64; ++index) public_key[index] = static_cast(index); generation = 7; return chat::DeviceAuthorResult::Ok; } chat::DeviceAuthorResult proveDeviceAuthor(const uint8_t* session, const uint8_t* challenge, uint32_t expected, uint8_t* scratch, size_t capacity, uint8_t* key, uint32_t& generation, uint8_t* signature) const override { const auto result = readDeviceAuthorKey(key, generation); if (result != chat::DeviceAuthorResult::Ok) return result; if (expected != generation) return chat::DeviceAuthorResult::StaleIdentity; size_t written; if (!chat::deviceAuthorProofMessage(session, challenge, key, generation, scratch, capacity, written)) return chat::DeviceAuthorResult::InvalidInput; message.assign(scratch, scratch + written); std::memset(signature, 0x66, 64); return chat::DeviceAuthorResult::Ok; } bool sendText(chat::ChannelId, const std::string&, chat::MessageId*, chat::NodeId) override { return false; } bool pollIncomingText(chat::MeshIncomingText*) override { return false; } bool sendAppData(chat::ChannelId, uint32_t, const uint8_t*, size_t, chat::NodeId, bool, chat::MessageId, bool) override { return false; } bool pollIncomingData(chat::MeshIncomingData*) override { return false; } void applyConfig(const chat::MeshConfig&) override {} bool isReady() const override { return true; } bool pollIncomingRawPacket(uint8_t*, size_t&, size_t) override { return false; } }; namespace hostlink { bool enqueue_event(uint8_t type, const uint8_t* payload, size_t length, bool, uint16_t sequence) { assert(type == static_cast(FrameType::ManagementResponse)); response.assign(payload, payload + length); response_sequence = sequence; return true; } } // namespace hostlink int main() { namespace web_config = hostlink::management::web_config; using namespace hostlink; std::array session{}, request{}; session[0] = 1; request[0] = 2; PendingCommand command; command.type = PendingCommandType::Management; command.sequence = 92; const uint8_t empty[] = {0x90}; size_t written; auto dispatch = [&](uint16_t op, const uint8_t* body, size_t length) {assert(management::encodeEnvelope(session.data(),request.data(),op,body,length,command.payload,sizeof(command.payload),written));command.payload_len=static_cast(written);assert(management::execute_management(command)==ErrorCode::Ok);management::EnvelopeView view;assert(management::decodeEnvelope(response.data(),response.size(),view));assert(response_sequence==92);assert(!std::memcmp(view.session,session.data(),16));return view; }; auto caps = dispatch(0, empty, sizeof(empty)); assert(caps.code == 0); geocaching::protocol::CmpReader cap_reader({caps.body, caps.body_size}); size_t cap_fields = 0, operation_count = 0; uint64_t cap_value = 0; assert(cap_reader.array(cap_fields, 6) && cap_fields == 6); for (unsigned i = 0; i < 4; ++i) assert(cap_reader.unsignedInteger(cap_value)); assert(cap_reader.array(operation_count, 32) && operation_count == 20); bool has_job_get = false, has_unimplemented_commit = false, has_job_stop = false; for (size_t i = 0; i < operation_count; ++i) { assert(cap_reader.unsignedInteger(cap_value)); has_job_get |= cap_value == 48; has_job_stop |= cap_value == 50; has_unimplemented_commit |= cap_value == 68; } assert(cap_reader.unsignedInteger(cap_value) && cap_value == 7 && cap_reader.finished() && has_job_get && has_unimplemented_commit && has_job_stop); assert(dispatch(1, empty, sizeof(empty)).code == 5); FakeAuthor author; app::fake_messaging.adapter = &author; job_operation.fill(0x5b); uint8_t job_body[32]; geocaching::protocol::CmpWriter job_request(job_body, sizeof(job_body)); assert(job_request.array(2) && job_request.binary({job_operation.data(), 16}) && job_request.unsignedInteger(7)); auto job_reply = dispatch(48, job_body, job_request.size()); assert(job_reply.code == 0 && job_current_reads == 1 && job_history_reads == 0); geocaching::protocol::CmpReader job_projection({job_reply.body, job_reply.body_size}); size_t job_fields = 0; geocaching::ByteView returned_job; uint64_t phase = 0; assert(job_projection.array(job_fields, 8) && job_fields == 8 && job_projection.binary(returned_job, 16) && !std::memcmp(returned_job.data, job_operation.data(), 16) && job_projection.unsignedInteger(phase) && phase == 3); assert(dispatch(50, job_body, job_request.size()).code == 0 && job_stop_calls == 1); const auto reads_before_stop_pending = job_current_reads; job_stop_result = WebResult::Pending; assert(dispatch(50, job_body, job_request.size()).code == 5 && job_current_reads == reads_before_stop_pending); job_stop_result = WebResult::Forbidden; assert(dispatch(50, job_body, job_request.size()).code == 2); job_stop_result = WebResult::NotFound; assert(dispatch(50, job_body, job_request.size()).code == 10); job_stop_result = WebResult::StaleIdentity; assert(dispatch(50, job_body, job_request.size()).code == 9); job_stop_result = WebResult::Ready; job_current_result = WebResult::NotFound; assert(dispatch(48, job_body, job_request.size()).code == 0 && job_history_reads == 1); job_history_result = WebResult::Pending; assert(dispatch(48, job_body, job_request.size()).code == 5); job_history_result = WebResult::NotFound; assert(dispatch(48, job_body, job_request.size()).code == 10); job_history_result = WebResult::Forbidden; assert(dispatch(48, job_body, job_request.size()).code == 2); job_history_result = WebResult::Invalid; assert(dispatch(48, job_body, job_request.size()).code == 6); job_current_result = WebResult::StaleIdentity; const auto history_reads_before_stale = job_history_reads; assert(dispatch(48, job_body, job_request.size()).code == 9 && job_history_reads == history_reads_before_stale); job_body[job_request.size() - 1] = 8; const auto reads_before_stale = job_current_reads; const auto stops_before_stale = job_stop_calls; assert(dispatch(50, job_body, job_request.size()).code == 9 && job_stop_calls == stops_before_stale); assert(dispatch(48, job_body, job_request.size()).code == 9 && job_current_reads == reads_before_stale); assert(dispatch(48, empty, sizeof(empty)).code == 3); assert(dispatch(50, empty, sizeof(empty)).code == 3 && job_stop_calls == stops_before_stale); std::memset(job_body + 3, 0, 16); job_body[job_request.size() - 1] = 7; assert(dispatch(48, job_body, job_request.size()).code == 3 && job_current_reads == reads_before_stale); auto author_info = dispatch(1, empty, sizeof(empty)); assert(author_info.code == 0); geocaching::protocol::CmpReader facts({author_info.body, author_info.body_size}); size_t fact_count; uint64_t generation; geocaching::ByteView public_key, identity_hash, destination; assert(facts.array(fact_count, 4) && fact_count == 4 && facts.binary(public_key, 64) && facts.binary(identity_hash, 16) && facts.binary(destination, 16) && facts.unsignedInteger(generation) && facts.finished()); assert(public_key.size == 64 && generation == 7 && identity_hash.size == 16 && destination.size == 16); uint8_t nonce[32], expected_key[64], proof_body[64], expected_message[160]; std::memset(nonce, 0x77, sizeof(nonce)); for (unsigned index = 0; index < 64; ++index) expected_key[index] = static_cast(index); geocaching::protocol::CmpWriter proof_writer(proof_body, sizeof(proof_body)); assert(proof_writer.array(2) && proof_writer.binary({nonce, 32}) && proof_writer.unsignedInteger(7)); auto proof = dispatch(2, proof_body, proof_writer.size()); assert(proof.code == 0 && proof.body[0] == 0x95); size_t expected_size; assert(chat::deviceAuthorProofMessage(session.data(), nonce, expected_key, 7, expected_message, sizeof(expected_message), expected_size)); assert(author.message == std::vector(expected_message, expected_message + expected_size)); geocaching::protocol::CmpReader proof_reader({proof.body, proof.body_size}); geocaching::ByteView signature; assert(proof_reader.array(fact_count, 5) && proof_reader.binary(public_key, 64) && proof_reader.binary(identity_hash, 16) && proof_reader.binary(destination, 16) && proof_reader.unsignedInteger(generation) && proof_reader.binary(signature, 64) && proof_reader.finished()); assert(!std::memcmp(public_key.data, expected_key, 64) && generation == 7 && signature.size == 64 && signature.data[0] == 0x66); proof_body[proof_writer.size() - 1] = 8; assert(dispatch(2, proof_body, proof_writer.size()).code == 9); assert(dispatch(2, empty, sizeof(empty)).code == 3); uint8_t draft_request[64]; geocaching::protocol::CmpWriter draft_writer(draft_request, sizeof(draft_request)); assert(draft_writer.array(5) && draft_writer.binary({session.data(), 16}) && draft_writer.unsignedInteger(0) && draft_writer.unsignedInteger(320) && draft_writer.unsignedInteger(0) && draft_writer.unsignedInteger(7)); assert(dispatch(65, draft_request, draft_writer.size()).code == 5 && draft_reads == 1 && draft_expected == 0); draft_result = platform::esp::arduino_common::geocaching::browse_runtime::WebReadResult::Ready; assert(dispatch(65, draft_request, draft_writer.size()).code == 0 && draft_reads == 2); draft_request[draft_writer.size() - 1] = 8; assert(dispatch(65, draft_request, draft_writer.size()).code == 9 && draft_reads == 2); assert(dispatch(65, empty, sizeof(empty)).code == 3 && draft_reads == 2); const uint8_t list_first[] = {0x93, 0, 0xc0, 7}; assert(dispatch(64, list_first, sizeof(list_first)).code == 0); const uint8_t list_stale_author[] = {0x93, 0, 0xc0, 8}; assert(dispatch(64, list_stale_author, sizeof(list_stale_author)).code == 9); const uint8_t list_unbound_continuation[] = {0x93, 1, 0xc0, 7}; assert(dispatch(64, list_unbound_continuation, sizeof(list_unbound_continuation)).code == 3); assert(dispatch(64, empty, sizeof(empty)).code == 3); const uint8_t directory_first[] = {0x91, 0}, directory_outside[] = {0x91, 5}; assert(dispatch(70, directory_first, sizeof(directory_first)).code == 0); assert(dispatch(70, directory_outside, sizeof(directory_outside)).code == 3); assert(dispatch(70, empty, sizeof(empty)).code == 3); web_config::Metadata upload; std::memcpy(upload.owner, session.data(), 16); std::memcpy(upload.id, session.data(), 16); upload.kind = 2; upload.phase = 1; upload.total = upload.offset = 5; platform::esp::arduino_common::storage::fake_files[web_config::candidate_path(2)] = "draft"; uint8_t upload_scratch[128]; assert(web_config::hash_file(web_config::candidate_path(2), upload_scratch, upload.hash, 5)); assert(web_config::save(upload, upload_scratch)); uint8_t put_body[80]; geocaching::protocol::CmpWriter put_writer(put_body, sizeof(put_body)); assert(put_writer.array(4) && put_writer.binary({upload.id, 16}) && put_writer.binary({request.data(), 16}) && put_writer.unsignedInteger(0) && put_writer.unsignedInteger(7)); draft_put_check = WebResult::Forbidden; assert(dispatch(66, put_body, put_writer.size()).code == 2 && draft_put_queues == 0); assert(web_config::load(upload, upload_scratch) && upload.phase == 2 && !std::memcmp(upload.base_hash, request.data(), 16)); auto changed_target = std::array{}; std::memcpy(changed_target.data(), put_body, put_writer.size()); changed_target[21] ^= 1; assert(dispatch(66, changed_target.data(), put_writer.size()).code == 4); draft_put_check = WebResult::Ready; assert(dispatch(66, put_body, put_writer.size()).code == 5 && draft_put_queues == 1); assert(web_config::load(upload, upload_scratch) && upload.phase == 3); uint8_t abort_body[20]; geocaching::protocol::CmpWriter abort_writer(abort_body, sizeof(abort_body)); assert(abort_writer.array(1) && abort_writer.binary({upload.id, 16})); assert(dispatch(36, abort_body, abort_writer.size()).code == 4); uint8_t replacement_body[80]; std::array replacement_id{}; replacement_id.fill(9); geocaching::protocol::CmpWriter replacement_writer(replacement_body, sizeof(replacement_body)); assert(replacement_writer.array(4) && replacement_writer.binary({replacement_id.data(), 16}) && replacement_writer.unsignedInteger(2) && replacement_writer.unsignedInteger(5) && replacement_writer.binary({upload.hash, 32})); assert(dispatch(32, replacement_body, replacement_writer.size()).code == 5); auto changed_author = std::array{}; std::memcpy(changed_author.data(), put_body, put_writer.size()); changed_author[put_writer.size() - 1] = 8; assert(dispatch(66, changed_author.data(), put_writer.size()).code == 9); draft_put_check = WebResult::Unavailable; assert(dispatch(66, put_body, put_writer.size()).code == 6); assert(web_config::load(upload, upload_scratch) && upload.phase == 3); platform::esp::arduino_common::storage::fake_files.erase(web_config::candidate_path(2)); draft_put_check = WebResult::AlreadySaved; auto committed_draft = dispatch(66, put_body, put_writer.size()); assert(committed_draft.code == 0 && draft_put_queues == 1); geocaching::protocol::CmpReader committed_reader({committed_draft.body, committed_draft.body_size}); geocaching::ByteView committed_transfer, committed_id; uint64_t committed_generation = 0; assert(committed_reader.array(fact_count, 3) && fact_count == 3 && committed_reader.binary(committed_transfer, 16) && committed_reader.binary(committed_id, 16) && committed_reader.unsignedInteger(committed_generation) && committed_reader.finished()); assert(committed_generation == 1 && !std::memcmp(committed_id.data, request.data(), 16)); assert(web_config::load(upload, upload_scratch) && upload.phase == 4); draft_put_check = WebResult::Unavailable; assert(dispatch(66, put_body, put_writer.size()).code == 0 && draft_put_queues == 1); assert(dispatch(36, abort_body, abort_writer.size()).code == 4); session[0] = 8; assert(dispatch(66, put_body, put_writer.size()).code == 2); session[0] = 1; upload.id[0] = 3; upload.phase = 1; platform::esp::arduino_common::storage::fake_files[web_config::candidate_path(2)] = "draft"; assert(web_config::save(upload, upload_scratch)); put_body[3] = 3; abort_body[3] = 3; draft_put_check = WebResult::Ready; draft_put_queue = WebResult::Invalid; assert(dispatch(66, put_body, put_writer.size()).code == 3); assert(web_config::load(upload, upload_scratch) && upload.phase == 3); draft_put_release = true; assert(dispatch(66, put_body, put_writer.size()).code == 3); assert(web_config::load(upload, upload_scratch) && upload.phase == 2); assert(dispatch(36, abort_body, abort_writer.size()).code == 0); author.failure = chat::DeviceAuthorResult::StorageError; assert(dispatch(1, empty, sizeof(empty)).code == 6); app::fake_messaging.adapter = nullptr; auto unsupported = dispatch(18, empty, sizeof(empty)); assert(unsupported.code == 1); const uint8_t read[] = {0x93, 0, 32, 0xc0}; auto snapshot = dispatch(17, read, sizeof(read)); assert(snapshot.code == 0); geocaching::protocol::CmpReader reader({snapshot.body, snapshot.body_size}); size_t fields; uint64_t offset, total; geocaching::ByteView hash, chunk; assert(reader.array(fields, 4) && fields == 4 && reader.unsignedInteger(offset) && reader.unsignedInteger(total) && reader.binary(hash, 32) && reader.binary(chunk, 320) && reader.finished()); assert(offset == 0 && total == platform::esp::arduino_common::storage::fake_file.size() && chunk.size == 32); uint8_t saved_hash[32]; std::memcpy(saved_hash, hash.data, 32); const uint8_t no_hash[] = {0x93, 1, 1, 0xc0}; assert(dispatch(17, no_hash, sizeof(no_hash)).code == 3); uint8_t continued[64]; geocaching::protocol::CmpWriter writer(continued, sizeof(continued)); assert(writer.array(3) && writer.unsignedInteger(32) && writer.unsignedInteger(32) && writer.binary({saved_hash, 32})); assert(dispatch(17, continued, writer.size()).code == 0); platform::esp::arduino_common::storage::fake_file[0] ^= 1; assert(dispatch(17, continued, writer.size()).code == 4); platform::esp::arduino_common::storage::fake_available = false; assert(dispatch(17, read, sizeof(read)).code == 6); using namespace platform::esp::arduino_common::storage; fake_available = true; app::fake_messaging.adapter = &author; author.failure = chat::DeviceAuthorResult::Ok; prepare_binding.fill(0); std::memset(prepare_binding.data(), 0xa5, 32); std::memset(prepare_binding.data() + 32, 0x44, 16); prepare_binding[52] = 1; std::array prepare_id{}; prepare_id[0] = 0x23; std::array prepare_body{}; auto prepare = [&](uint64_t edit_generation = 1, uint32_t identity_generation = 7) { geocaching::protocol::CmpWriter encoder(prepare_body.data(), prepare_body.size()); assert(encoder.array(5) && encoder.binary({request.data(), 16}) && encoder.unsignedInteger(edit_generation) && encoder.binary({session.data(), 16}) && encoder.unsignedInteger(identity_generation) && encoder.binary({prepare_id.data(), 16})); return dispatch(67, prepare_body.data(), encoder.size()); }; assert(prepare().code == 5); std::array ticket{}; assert(management::loadPublicationTicket(ticket.data(), ticket.size()) && ticket[4] == 3); const auto checking_disk = fake_files.at(management::kPublicationTicketPath); assert(prepare().code == 5 && fake_files.at(management::kPublicationTicketPath) == checking_disk); assert(prepare(2).code == 4); const auto network_calls = prepare_network_reads; assert(prepare(1, 8).code == 9 && prepare_network_reads == network_calls); prepare_latest_result = WebResult::Ready; prepare_latest[0] = 1; assert(prepare().code == 4); // A different publication already exists in the directory. prepare_latest_result = WebResult::Absent; auto prepared = prepare(); assert(prepared.code == 0); geocaching::protocol::CmpReader confirmation({prepared.body, prepared.body_size}); geocaching::ByteView confirmed_token, confirmed_draft, confirmed_author, confirmed_directory, confirmed_epoch, confirmed_hash; uint64_t confirmed_generation = 0, from_revision = 0, to_revision = 0, remaining = 0; assert(confirmation.array(fields, 10) && fields == 10 && confirmation.binary(confirmed_token, 16) && confirmation.binary(confirmed_draft, 16) && confirmation.unsignedInteger(confirmed_generation) && confirmation.binary(confirmed_author, 64) && confirmation.binary(confirmed_directory, 16) && confirmation.binary(confirmed_epoch, 16) && confirmation.binary(confirmed_hash, 32) && confirmation.unsignedInteger(from_revision) && confirmation.unsignedInteger(to_revision) && confirmation.unsignedInteger(remaining) && confirmation.finished()); assert(confirmed_token.size == 16 && !std::memcmp(confirmed_token.data, prepare_id.data(), 16) && confirmed_draft.size == 16 && !std::memcmp(confirmed_draft.data, request.data(), 16) && confirmed_generation == 1 && confirmed_directory.size == 16 && !std::memcmp(confirmed_directory.data, session.data(), 16) && confirmed_epoch.size == 16 && !std::memcmp(confirmed_epoch.data, prepare_binding.data() + 32, 16) && confirmed_hash.size == 32 && !std::memcmp(confirmed_hash.data, prepare_binding.data(), 32) && from_revision == 0 && to_revision == 1 && remaining == 120000); for (unsigned i = 0; i < 64; ++i) assert(confirmed_author.data[i] == i); const auto completed_calls = prepare_network_reads; prepare_clock += 100; assert(prepare().code == 0 && prepare_network_reads == completed_calls); session[0] ^= 1; assert(prepare().code == 2); session[0] ^= 1; prepare_clock = 121000; assert(prepare().code == 10); prepare_id[0] += 1; prepare_latest_result = WebResult::Pending; assert(prepare().code == 5); prepare_binding[0] ^= 1; assert(prepare().code == 4); prepare_binding[0] ^= 1; prepare_boot = 2; assert(prepare().code == 4); prepare_id[0] += 1; fake_write_failure = true; const auto protected_disk = fake_files.at(management::kPublicationTicketPath); assert(prepare().code == 6 && fake_files.at(management::kPublicationTicketPath) == protected_disk); fake_write_failure = false; assert(prepare().code == 5); assert(management::loadPublicationTicket(ticket.data(), ticket.size())); assert(management::completePublicationCheck(ticket.data(), management::kPublicationTicketBytes, prepare_clock)); assert(management::advancePublicationTicket(ticket.data(), management::kPublicationTicketBytes, management::PublicationTicketPhase::Intent)); assert(management::savePublicationTicket(ticket.data(), management::kPublicationTicketBytes)); prepare_id[0] += 1; prepare_clock += 500000; assert(prepare().code == 4); // Unresolved commit intent is never overwritten by PREPARE. fake_files[management::kPublicationTicketPath][53] ^= 1; assert(prepare().code == 6); fake_files.erase(management::kPublicationTicketPath); prepare_id[0] += 1; prepare_binding[48] = 1; prepare_binding[52] = 2; std::memset(prepare_binding.data() + 56, 0xb1, 32); prepare_latest_result = WebResult::Ready; prepare_latest[0] = 2; assert(prepare(2).code == 4); prepare_latest[0] = 1; assert(prepare(2).code == 4); // Same revision number is insufficient without the predecessor hash. std::memset(prepare_latest.data() + 4, 0xb1, 32); assert(prepare(2).code == 0); ++prepare_id[0]; session[0] ^= 1; assert(prepare(2).code == 2); session[0] ^= 1; assert(prepare(2).code == 0); // The same owner may replace an unsubmitted review without waiting for expiry. job_operation = prepare_id; job_current_result = job_history_result = WebResult::NotFound; std::array commit_body{}; geocaching::protocol::CmpWriter commit_encoder(commit_body.data(), commit_body.size()); assert(commit_encoder.array(3) && commit_encoder.binary({prepare_id.data(), 16}) && commit_encoder.binary({prepare_binding.data(), 32}) && commit_encoder.unsignedInteger(7)); const auto commit = [&] { return dispatch(68, commit_body.data(), commit_encoder.size()); }; job_history_result = WebResult::Ready; assert(commit().code == 4 && !job_queue_calls); // A retained ID cannot be reused by a new ticket. job_history_result = WebResult::NotFound; const auto clock_before_expiry = prepare_clock; commit_binding_delay = 120000; assert(commit().code == 10 && !job_queue_calls); commit_binding_delay = 0; prepare_clock = clock_before_expiry; session[0] ^= 1; assert(commit().code == 2 && !job_queue_calls); session[0] ^= 1; commit_body[21] ^= 1; assert(commit().code == 4 && !job_queue_calls); commit_body[21] ^= 1; commit_body[commit_encoder.size() - 1] = 8; assert(commit().code == 9 && !job_queue_calls); commit_body[commit_encoder.size() - 1] = 7; prepare_binding[0] ^= 1; assert(commit().code == 4 && !job_queue_calls); prepare_binding[0] ^= 1; fake_write_failure = true; assert(commit().code == 6 && !job_queue_calls); fake_write_failure = false; job_queue_admit = false; assert(commit().code == 5 && job_queue_calls == 1 && !job_admissions); assert(management::loadPublicationTicket(ticket.data(), ticket.size()) && ticket[4] == 1); job_queue_admit = true; auto accepted = commit(); assert(accepted.code == 0 && job_queue_calls == 2 && job_admissions == 1); geocaching::protocol::CmpReader admission({accepted.body, accepted.body_size}); geocaching::ByteView operation_id, draft_id, selected_directory, content_hash; uint64_t admitted_generation = 0, admitted_revision = 0, admitted = 0; assert(admission.array(fields, 7) && fields == 7 && admission.binary(operation_id, 16) && admission.binary(draft_id, 16) && admission.unsignedInteger(admitted_generation) && admission.binary(selected_directory, 16) && admission.binary(content_hash, 32) && admission.unsignedInteger(admitted_revision) && admission.unsignedInteger(admitted) && admission.finished()); assert(!std::memcmp(operation_id.data, prepare_id.data(), 16) && !std::memcmp(draft_id.data, request.data(), 16) && admitted_generation == 2 && !std::memcmp(content_hash.data, prepare_binding.data(), 32) && admitted_revision == 2 && admitted == 1); assert(management::loadPublicationTicket(ticket.data(), ticket.size()) && ticket[4] == 2); const auto admitted_disk = fake_files.at(management::kPublicationTicketPath); assert(commit().code == 0 && job_admissions == 1 && fake_files.at(management::kPublicationTicketPath) == admitted_disk); ++prepare_boot; prepare_clock += 500000; job_current_result = WebResult::NotFound; job_history_result = WebResult::Ready; assert(commit().code == 0 && job_admissions == 1); // A saved result survives boot/expiry. job_history_result = WebResult::NotFound; assert(commit().code == 5 && job_admissions == 1); // Unknown admission is never requeued. const auto old_prepare_id = prepare_id; ++prepare_id[0]; assert(prepare(2).code == 4 && fake_files.at(management::kPublicationTicketPath) == admitted_disk); job_history_result = WebResult::Ready; assert(prepare(2).code == 0); // A verified terminal result allows the next prepare. assert(job_admissions == 1 && job_operation == old_prepare_id); management::web_config::Metadata signature_metadata; signature_metadata.owner[0] = 22; signature_metadata.id[0] = 33; signature_metadata.hash[0] = 44; signature_metadata.kind = 5; signature_metadata.phase = 1; signature_metadata.total = signature_metadata.offset = 100; std::array arena{}; assert(management::web_config::save(signature_metadata, arena.data())); std::array signature_operation{}; signature_operation[0] = 55; geocaching::protocol::CmpWriter signature_writer(arena.data() + 41, arena.size() - 41); assert(signature_writer.array(5) && signature_writer.binary({signature_metadata.id, 16}) && signature_writer.binary({signature_metadata.hash, 32}) && signature_writer.unsignedInteger(100) && signature_writer.unsignedInteger(7) && signature_writer.binary({signature_operation.data(), 16})); management::EnvelopeView signature_request; signature_request.session = signature_metadata.owner; signature_request.code = uint16_t(management::Operation::CacheSignBegin); signature_request.body = arena.data() + 41; signature_request.body_size = signature_writer.size(); size_t signature_written = 0; assert(management::cache_signature(signature_request, arena.data() + 41, arena.size() - 41, signature_written) == management::Status::Ok); assert(local_signature_queues == 1 && local_signature_operation == signature_operation && local_signature_hash[0] == 44); { signature_metadata.kind = 6; assert(management::web_config::save(signature_metadata, arena.data() + 41)); std::array proof_raw{}; std::array epoch{}; epoch.fill(9); std::array proof_signature{}, directory_key{}; ::geocaching::protocol::CmpWriter proof_writer(proof_raw.data(), proof_raw.size()); assert(proof_writer.array(8) && proof_writer.unsignedInteger(1) && proof_writer.binary({epoch.data(), 16}) && proof_writer.binary({epoch.data(), 16}) && proof_writer.binary({signature_metadata.hash, 32}) && proof_writer.unsignedInteger(1) && proof_writer.binary({signature_metadata.hash, 32}) && proof_writer.unsignedInteger(0) && proof_writer.unsignedInteger(1800000000)); ::geocaching::protocol::CmpWriter confirmation_writer(arena.data() + 41, arena.size() - 41); assert(confirmation_writer.array(7) && confirmation_writer.binary({signature_metadata.id, 16}) && confirmation_writer.binary({signature_metadata.hash, 32}) && confirmation_writer.unsignedInteger(7) && confirmation_writer.binary({proof_raw.data(), proof_writer.size()}) && confirmation_writer.binary({proof_signature.data(), 64}) && confirmation_writer.binary({directory_key.data(), 64}) && confirmation_writer.binary({epoch.data(), 16})); signature_request.code = uint16_t(management::Operation::CacheConfirmBegin); signature_request.body_size = confirmation_writer.size(); assert(management::cache_confirmation(signature_request, arena.data() + 41, arena.size() - 41, signature_written) == management::Status::Ok && local_signature_queues == 2); } }