diff --git a/README.md b/README.md index 70821df..2e382c5 100644 --- a/README.md +++ b/README.md @@ -340,6 +340,7 @@ The final product contracts for these areas are being defined separately before - [Track Builder](docs/architecture/track_builder.md) - [Sparse SfM](docs/architecture/sparse_sfm.md) - [Calibration Bootstrap v1](docs/architecture/calibration_bootstrap.md) +- [Calibration Publication v2](docs/architecture/calibration_publication_v2.md) - [Calibration Science v1](docs/architecture/calibration_science_v1.md) - [Calibration Solver Preflight v1](docs/architecture/calibration_solver_preflight_v1.md) - [Photo Quality Triage](docs/architecture/photo_quality_triage.md) diff --git a/docs/README.md b/docs/README.md index a645603..79ea6eb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ REAL_S21_TRACKS=PASS/FROZEN REAL_A6000_PRE_SFM=PASS/FROZEN PRODUCT_DEFINITION_V1=PASS/FROZEN PROMPT_TREE=CURRENT -CURRENT_NEXT=CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION +CURRENT_NEXT=CALIBRATION_V2_WORKFLOW_READY ``` This index separates current authority, historical evidence and future product-definition work. diff --git a/docs/architecture/calibration_publication_v2.md b/docs/architecture/calibration_publication_v2.md new file mode 100644 index 0000000..baf1289 --- /dev/null +++ b/docs/architecture/calibration_publication_v2.md @@ -0,0 +1,89 @@ +# Calibration Publication v2 + +**PASS / FROZEN — additive heterogeneous calibration publication boundary.** + +## Authority and scope + +Calibration Science v2 is the frozen scientific authority. This document +defines only the additive heterogeneous publication bridge: + +```text +caller-owned validated group evidence +-> Calibration Tooling v2 +-> L3DCALB2 +-> Calibration Bootstrap v2 +-> existing SparseCalibrationScope(image_id -> calibration_id) +``` + +It does not change `L3DCALB1`, Calibration Tooling v1, or Calibration Bootstrap +v1. It does not run a solver, define optical applicability thresholds, resolve +READY outside publication, or add persistence schema. + +## L3DCALB2 format + +The artifact is deterministic, fixed-width, and little-endian. Integers use +the stated unsigned width. Binary64 fields contain IEEE-754 bits; producers +canonicalize negative zero and importers reject non-finite values. Native +structures, padding, locale text, and trailing bytes are forbidden. The whole +artifact is at most 600,000 bytes, with 1..4,096 groups and 1..4,096 entries; +the byte-size bound may impose a lower combined maximum. SHA-256 fields must be +nonzero. + +```text +magic 8 bytes = ASCII "L3DCALB2" +format_version u32 = 2 +model_kind u32 = PINHOLE(1) +model_version u32 = 1 +group_count u32, 1..4096 +entry_count u32, 1..4096 + +for each group in strictly increasing +(group_identity_sha256, group_version) order: + group_identity_sha256 32 bytes + group_version u32, nonzero + optical_state_sha256 32 bytes + target_sha256 32 bytes + solver_executable_sha256 32 bytes + solver_configuration_sha256 32 bytes + initialization_evidence_sha256 32 bytes + validation_evidence_sha256 32 bytes + member_count u32, nonzero + + for each member in strictly increasing selected_item_index order: + selected_item_index u32 + image_id u64, nonzero + representation_sha256 32 bytes + width, height u32, u32, nonzero + fx, fy, cx, cy f64, f64, f64, f64 + k1, k2, p1, p2 f64, f64, f64, f64 + support_images u32, nonzero + support_observations u32, nonzero + reprojection_rmse_px finite nonnegative f64 + maximum_parameter_delta finite nonnegative f64 + validation_flags u32 = 0x0f +``` + +The pinhole parameters and four validation bits have exactly the v1 meaning. +V2 introduces no new solver threshold. Positive focal lengths and principal +points inside the declared dimensions are required. Every selected item index +from zero through `entry_count - 1` occurs exactly once across the artifact, +and every `image_id` occurs exactly once. Group order and member order are +validated, not silently normalized. + +## Provenance and publication + +Each complete serialized group record, including its exact membership and all +group-local evidence, is independently SHA-256 hashed. That group digest is the +`IMPORTED_TRUSTED` provenance fingerprint for each calibration in that group. +The independently supplied whole-artifact SHA-256 protects transport and binds +the combined publication without replacing group-local provenance. + +Bootstrap validates the whole artifact and then verifies every entry against +the immutable selected execution's exact item index, `image_id`, and current +representation asset SHA-256 before its first write. It creates or reuses the +existing content-addressed Sparse calibrations per entry, creates or reuses one +scope over every selected image across all groups, and attaches only that +complete scope. A pre-publication error attaches no scope. A later failure may +retain immutable calibration evidence but cannot make the execution READY. +Exact retries reuse calibrations and the scope and converge on the same +attachment. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index a929613..f84bf2a 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -24,7 +24,7 @@ SOURCE_COMMENT_AUDIT PASS PRODUCT_DEFINITION PASS/FROZEN PROMPT_TREE CURRENT USER_FACING_UI_LANGUAGE_NORMALIZATION PASS -CURRENT_NEXT CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION +CURRENT_NEXT CALIBRATION_V2_WORKFLOW_READY ``` The current Project DB head is additive: @@ -534,8 +534,11 @@ v26 heterogeneous-optics foundation now durably records Capture geometric state applicability while retaining that per-image scope model. Unknown state remains retained but `CALIBRATION_REQUIRED`, and exact compatibility resolves none/one/many candidates to `CALIBRATION_REQUIRED`/resolved/`SELECTION_REQUIRED`. The next dependency is heterogeneous -calibration publication. Device-specific autofocus envelopes remain blocked until physical evidence -validates them. +calibration publication. `CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION=PASS/FROZEN`: +additive L3DCALB2/Tooling/Bootstrap v2 now retains independent group-local provenance, validates +complete exact selected-image coverage, and attaches one existing per-image scope only after complete +publication. The next dependency is heterogeneous Workflow v2 truthful READY. Device-specific +autofocus envelopes remain blocked until physical evidence validates them. Calibration Tooling v1 consumes an already acquired Science v1 evidence bundle, validates the bounded contract and produces deterministic `L3DCALB1` v1. diff --git a/include/lardon3d/calibration_bootstrap_v2.h b/include/lardon3d/calibration_bootstrap_v2.h new file mode 100644 index 0000000..9ad9300 --- /dev/null +++ b/include/lardon3d/calibration_bootstrap_v2.h @@ -0,0 +1,64 @@ +#ifndef LARDON3D_CALIBRATION_BOOTSTRAP_V2_H +#define LARDON3D_CALIBRATION_BOOTSTRAP_V2_H + +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + LARDON3D_CALIBRATION_BOOTSTRAP_V2_ARTIFACT_VERSION = 2, + LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_BYTES = 600000, + LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_GROUPS = 4096, + LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_ENTRIES = 4096, +}; + +typedef enum { + LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK = 0, + LARDON3D_CALIBRATION_BOOTSTRAP_V2_INVALID_ARGUMENT, + LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT, + LARDON3D_CALIBRATION_BOOTSTRAP_V2_PROVENANCE_MISMATCH, + LARDON3D_CALIBRATION_BOOTSTRAP_V2_SELECTION_CONFLICT, + LARDON3D_CALIBRATION_BOOTSTRAP_V2_DB_ERROR, + LARDON3D_CALIBRATION_BOOTSTRAP_V2_OUT_OF_MEMORY, +} Lardon3DCalibrationBootstrapV2Result; + +typedef struct { + unsigned char artifact_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE]; + Lardon3DSparseCalibrationScope scope; + uint32_t calibration_count; + uint32_t group_count; +} Lardon3DCalibrationBootstrapV2Output; + +/* Import one complete L3DCALB2 artifact for an immutable selected execution. + * All pointers are required and borrowed only for the call; artifact_size is + * bounded by V2_MAX_BYTES and must match expected_artifact_sha256 before any + * parsing or database access. The fixed-width little-endian format accepts + * only the existing eight-parameter PINHOLE model and complete, nonoverlapping + * coverage of selected item indexes, image IDs, and representation SHA-256s. + * + * Each serialized group is hashed independently and that digest becomes every + * member calibration's IMPORTED_TRUSTED provenance fingerprint. Thus target, + * optical-state, solver, initialization, validation, and exact membership + * evidence remain group-local even when one scope contains heterogeneous + * groups. Validation completes before publication. Existing immutable + * calibrations and one exact all-image scope are reused on retry; the scope is + * attached only after complete publication, so a failure cannot make the + * execution READY. No Capture identity, solver result beyond intrinsics, or + * compatibility relation is inferred by this C ABI. */ +Lardon3DCalibrationBootstrapV2Result lardon3d_calibration_bootstrap_v2_import( + Lardon3DProjectDb *database, uint64_t execution_id, + const unsigned char *artifact, size_t artifact_size, + const unsigned char expected_artifact_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], + Lardon3DCalibrationBootstrapV2Output *output); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/lardon3d/calibration_tooling_v2.h b/include/lardon3d/calibration_tooling_v2.h new file mode 100644 index 0000000..1f9ce86 --- /dev/null +++ b/include/lardon3d/calibration_tooling_v2.h @@ -0,0 +1,103 @@ +#ifndef LARDON3D_CALIBRATION_TOOLING_V2_H +#define LARDON3D_CALIBRATION_TOOLING_V2_H + +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + LARDON3D_CALIBRATION_TOOLING_V2_VERSION = 2, + LARDON3D_CALIBRATION_TOOLING_V2_VALIDATION_FLAGS = 15, +}; + +typedef enum { + LARDON3D_CALIBRATION_TOOLING_V2_OK = 0, + LARDON3D_CALIBRATION_TOOLING_V2_INVALID_ARGUMENT, + LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED, + LARDON3D_CALIBRATION_TOOLING_V2_CAPACITY, + LARDON3D_CALIBRATION_TOOLING_V2_ENCODING_ERROR, + LARDON3D_CALIBRATION_TOOLING_V2_IMPORT_ERROR, +} Lardon3DCalibrationToolingV2Result; + +/* One selected-image member of a caller-declared scientific group. The item + * index is the immutable selected-execution order, not an operational group + * index. Parameters use the exact v1/OpenCV pinhole meaning. All storage is + * caller-owned and borrowed only while the API call is active. */ +typedef struct { + uint32_t selected_item_index; + uint64_t image_id; + unsigned char representation_sha256[32]; + uint32_t width; + uint32_t height; + double fx, fy, cx, cy, k1, k2, p1, p2; + uint32_t support_images; + uint32_t support_observations; + double reprojection_rmse_px; + double maximum_parameter_delta; + uint32_t validation_flags; +} Lardon3DCalibrationToolingV2Entry; + +/* Group identity is the exact bounded state identifier plus its explicit + * version. The six nonzero digests independently bind optical applicability, + * physical target, solver executable/configuration, initialization, and + * validation evidence. Entries must be strictly increasing by selected item + * index; groups must be strictly increasing by (group_identity_sha256, + * group_version). These canonical orders make identical evidence byte-stable + * without hiding caller mistakes by silently reordering it. */ +typedef struct { + unsigned char group_identity_sha256[32]; + uint32_t group_version; + unsigned char optical_state_sha256[32]; + unsigned char target_sha256[32]; + unsigned char solver_executable_sha256[32]; + unsigned char solver_configuration_sha256[32]; + unsigned char initialization_evidence_sha256[32]; + unsigned char validation_evidence_sha256[32]; + const Lardon3DCalibrationToolingV2Entry *entries; + size_t entry_count; +} Lardon3DCalibrationToolingV2Group; + +/* A bounded heterogeneous publication manifest. Every item index in + * [0, entry_count) must occur exactly once across all groups, and every image + * ID must be globally unique. Tooling validates only explicit evidence and + * model-domain safety; it does not run a solver, invent acceptance thresholds, + * or decide optical compatibility. */ +typedef struct { + const Lardon3DCalibrationToolingV2Group *groups; + size_t group_count; + size_t entry_count; +} Lardon3DCalibrationToolingV2Evidence; + +/* Validate borrowed evidence and its borrowed nested group/entry arrays for + * bounded counts, canonical ordering, and exact selected-index/image coverage. + * Validation does not mutate evidence, artifact bytes, or Project DB state. + * INVALID_ARGUMENT reports a missing top-level pointer/groups array or + * out-of-range top-level counts; EVIDENCE_REJECTED reports invalid nested or + * scientific evidence within an otherwise bounded top-level manifest. */ +Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_validate( + const Lardon3DCalibrationToolingV2Evidence *evidence); + +/* Encode deterministic L3DCALB2 into caller storage and return its SHA-256. + * `written` may be NULL; no partial artifact is reported as written. */ +Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_produce( + const Lardon3DCalibrationToolingV2Evidence *evidence, unsigned char *artifact, + size_t artifact_capacity, size_t *written, unsigned char artifact_sha256[32]); + +/* Validate, encode, then call the production v2 bootstrap importer. The DB is + * untouched when validation or encoding fails. */ +Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_import( + Lardon3DProjectDb *database, uint64_t execution_id, + const Lardon3DCalibrationToolingV2Evidence *evidence, unsigned char *artifact, + size_t artifact_capacity, size_t *written, + Lardon3DCalibrationBootstrapV2Output *output); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/meson.build b/meson.build index 98024e4..bdbe033 100644 --- a/meson.build +++ b/meson.build @@ -179,7 +179,9 @@ lardon3d_app = executable( 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/optical_profiles.c', 'src/calibration_bootstrap.c', + 'src/calibration_bootstrap_v2.c', 'src/calibration_tooling.c', + 'src/calibration_tooling_v2.c', 'src/calibration_workflow.cpp', 'src/calibration_workflow_materialize.cpp', 'src/calibration_workflow_bind.cpp', @@ -899,6 +901,21 @@ calibration_tooling_test = executable( test('calibration-tooling', calibration_tooling_test, timeout: 30) +calibration_publication_v2_test = executable( + 'test-calibration-publication-v2', + sources: [ + 'tests/test_calibration_publication_v2.c', + 'src/calibration_tooling_v2.c', 'src/calibration_bootstrap_v2.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', + 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', + ], + c_args: ['-DLARDON3D_PROJECT_DB_TESTING'], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl, cc.find_library('m')], +) + +test('calibration-publication-v2', calibration_publication_v2_test, timeout: 30) + calibration_workflow_test = executable( 'test-calibration-workflow', sources: [ diff --git a/prompt/02_CURRENT_FROZEN_STATE.md b/prompt/02_CURRENT_FROZEN_STATE.md index 968c186..f3f1f4c 100644 --- a/prompt/02_CURRENT_FROZEN_STATE.md +++ b/prompt/02_CURRENT_FROZEN_STATE.md @@ -6,7 +6,7 @@ CURRENT_PROJECT_DB_SCHEMA=v26 PRODUCTION_TASK_KINDS=16 USER_FACING_UI_LANGUAGE_NORMALIZATION=PASS -CURRENT_IMPLEMENTATION_CURSOR=CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION +CURRENT_IMPLEMENTATION_CURSOR=CALIBRATION_V2_WORKFLOW_READY ``` ## Authority @@ -37,6 +37,7 @@ Calibration workflow selected-execution binding PASS/FROZEN Calibration workflow Tooling/Bootstrap READY PASS/FROZEN Calibration Science v2 heterogeneous optics PASS/FROZEN Calibration v2 optical-state foundation PASS/FROZEN +Calibration v2 heterogeneous publication PASS/FROZEN Adaptive capture settings semantics PLANNED Autofocus v2 foundation PLANNED Calibration Tooling planarity alignment PASS/FROZEN diff --git a/prompt/12_CALIBRATION.md b/prompt/12_CALIBRATION.md index 0cd5c72..383d6f6 100644 --- a/prompt/12_CALIBRATION.md +++ b/prompt/12_CALIBRATION.md @@ -19,10 +19,11 @@ CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1=PASS/FROZEN CALIBRATION_WORKFLOW_TOOLING_BOOTSTRAP_READY_V1=PASS/FROZEN CALIBRATION_SCIENCE_V2=PASS/FROZEN CALIBRATION_V2_HETEROGENEOUS_OPTICS=PASS/FROZEN -CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION=PLANNED +CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION=PASS/FROZEN +CALIBRATION_V2_WORKFLOW_READY=PLANNED ADAPTIVE_CAPTURE_SETTINGS_CONTRACT=PLANNED AUTOFOCUS_V2_FOUNDATION=PLANNED -CURRENT_CALIBRATION_NEXT=CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION +CURRENT_CALIBRATION_NEXT=CALIBRATION_V2_WORKFLOW_READY ``` ## Authority diff --git a/prompt/31_IMPLEMENTATION_ORDER.md b/prompt/31_IMPLEMENTATION_ORDER.md index 60efe3a..169a38d 100644 --- a/prompt/31_IMPLEMENTATION_ORDER.md +++ b/prompt/31_IMPLEMENTATION_ORDER.md @@ -6,7 +6,7 @@ IMPLEMENTATION_ORDER=DEPENDENCY_DRIVEN IMPLEMENTATION_AUTHORIZATION=NO STEP_0_USER_FACING_LANGUAGE_NORMALIZATION=PASS -CURRENT_NEXT=CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION +CURRENT_NEXT=CALIBRATION_V2_WORKFLOW_READY ``` ## Authority @@ -23,23 +23,24 @@ Default dependency order: 1. final usable Calibration Science/Tooling/Bootstrap/Workflow v1 compatibility path — PASS/FROZEN; 2. Calibration Science v2 design for heterogeneous cameras/lenses/focals, adaptive capture settings and autofocus — PASS/FROZEN; 3. Calibration v2 heterogeneous-optics persistence foundation — PASS/FROZEN; -4. Heterogeneous calibration publication / Tooling / Bootstrap evolution — CURRENT; -5. physical autofocus/optical applicability validation and dedicated calibrated real campaign; -6. real Sparse SfM proof; -7. durable Dense/OpenMVS orchestration; -8. mesh / refinement / texturing / export; -9. viewer foundation; -10. offline Coverage Analysis scientific contract and implementation; -11. multi-campaign registration / fusion; -12. generic live acquisition adapter foundation; -13. A6000 HDMI integration; -14. S21 integration; -15. live camera localization; -16. live coverage overlay; -17. actionable Capture Guidance; -18. video ingestion / deterministic keyframes; -19. final integration, UX, restart and performance proof; -20. Product Definition v1 Definition-of-Done closure. +4. Heterogeneous calibration publication / Tooling / Bootstrap evolution — PASS/FROZEN; +5. Heterogeneous Workflow v2 truthful READY proof — CURRENT; +6. physical autofocus/optical applicability validation and dedicated calibrated real campaign; +7. real Sparse SfM proof; +8. durable Dense/OpenMVS orchestration; +9. mesh / refinement / texturing / export; +10. viewer foundation; +11. offline Coverage Analysis scientific contract and implementation; +12. multi-campaign registration / fusion; +13. generic live acquisition adapter foundation; +14. A6000 HDMI integration; +15. S21 integration; +16. live camera localization; +17. live coverage overlay; +18. actionable Capture Guidance; +19. video ingestion / deterministic keyframes; +20. final integration, UX, restart and performance proof; +21. Product Definition v1 Definition-of-Done closure. ## Adjustment rule diff --git a/src/calibration_bootstrap_v2.c b/src/calibration_bootstrap_v2.c new file mode 100644 index 0000000..79481d7 --- /dev/null +++ b/src/calibration_bootstrap_v2.c @@ -0,0 +1,312 @@ +#include + +#include +#include +#include +#include +#include + +enum { + BOOTSTRAP_V2_HEADER_SIZE = 28, + BOOTSTRAP_V2_GROUP_SIZE = 232, + BOOTSTRAP_V2_ENTRY_SIZE = 144, + BOOTSTRAP_V2_VALIDATION_FLAGS = 15, +}; + +static const unsigned char bootstrap_v2_magic[8] = {'L', '3', 'D', 'C', 'A', 'L', 'B', '2'}; + +typedef struct { + const unsigned char *bytes; + size_t remaining; +} BootstrapV2Reader; + +typedef struct { + uint32_t selected_item_index; + unsigned char representation_sha256[32]; + Lardon3DSparseCalibration calibration; + Lardon3DSparseCalibrationMember member; +} BootstrapV2Entry; + +static bool take(BootstrapV2Reader *reader, void *output, size_t count) { + if (count > reader->remaining) return false; + if (output) memcpy(output, reader->bytes, count); + reader->bytes += count; + reader->remaining -= count; + return true; +} + +static bool read_u32(BootstrapV2Reader *reader, uint32_t *output) { + unsigned char bytes[4]; + if (!take(reader, bytes, sizeof(bytes))) return false; + *output = (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8) | + ((uint32_t)bytes[2] << 16) | ((uint32_t)bytes[3] << 24); + return true; +} + +static bool read_u64(BootstrapV2Reader *reader, uint64_t *output) { + unsigned char bytes[8]; + if (!take(reader, bytes, sizeof(bytes))) return false; + *output = 0; + for (size_t index = 0; index < sizeof(bytes); ++index) + *output |= (uint64_t)bytes[index] << (8u * index); + return true; +} + +static bool read_f64(BootstrapV2Reader *reader, double *output) { + uint64_t bits = 0; + if (!read_u64(reader, &bits)) return false; + memcpy(output, &bits, sizeof(bits)); + if (!isfinite(*output)) return false; + if (*output == 0.0) *output = 0.0; + return true; +} + +static bool sha256(const unsigned char *bytes, size_t size, unsigned char output[32]) { + unsigned int output_size = 0; + return EVP_Digest(bytes, size, output, &output_size, EVP_sha256(), NULL) == 1 && + output_size == 32; +} + +static bool nonzero_digest(const unsigned char value[32]) { + unsigned char any = 0; + for (size_t index = 0; index < 32; ++index) any |= value[index]; + return any != 0; +} + +static Lardon3DCalibrationBootstrapV2Result db_result(Lardon3DProjectDbResult result) { + if (result == LARDON3D_PROJECT_DB_CONSTRAINT || result == LARDON3D_PROJECT_DB_NOT_FOUND) + return LARDON3D_CALIBRATION_BOOTSTRAP_V2_SELECTION_CONFLICT; + return LARDON3D_CALIBRATION_BOOTSTRAP_V2_DB_ERROR; +} + +Lardon3DCalibrationBootstrapV2Result lardon3d_calibration_bootstrap_v2_import( + Lardon3DProjectDb *database, uint64_t execution_id, + const unsigned char *artifact, size_t artifact_size, + const unsigned char expected_artifact_sha256[32], + Lardon3DCalibrationBootstrapV2Output *output) { + if (!database || execution_id == 0 || !artifact || !expected_artifact_sha256 || !output || + artifact_size < BOOTSTRAP_V2_HEADER_SIZE || + artifact_size > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_BYTES) + return LARDON3D_CALIBRATION_BOOTSTRAP_V2_INVALID_ARGUMENT; + memset(output, 0, sizeof(*output)); + + unsigned char artifact_sha256[32]; + if (!sha256(artifact, artifact_size, artifact_sha256)) + return LARDON3D_CALIBRATION_BOOTSTRAP_V2_DB_ERROR; + if (memcmp(artifact_sha256, expected_artifact_sha256, 32) != 0) + return LARDON3D_CALIBRATION_BOOTSTRAP_V2_PROVENANCE_MISMATCH; + + BootstrapV2Reader reader = {artifact, artifact_size}; + unsigned char magic[8]; + uint32_t version, model_kind, model_version, group_count, entry_count; + if (!take(&reader, magic, sizeof(magic)) || !read_u32(&reader, &version) || + !read_u32(&reader, &model_kind) || !read_u32(&reader, &model_version) || + !read_u32(&reader, &group_count) || !read_u32(&reader, &entry_count) || + memcmp(magic, bootstrap_v2_magic, sizeof(magic)) != 0 || + version != LARDON3D_CALIBRATION_BOOTSTRAP_V2_ARTIFACT_VERSION || + model_kind != LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE || + model_version != LARDON3D_SPARSE_SFM_CALIBRATION_VERSION || group_count == 0 || + group_count > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_GROUPS || entry_count == 0 || + entry_count > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_ENTRIES) + return LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + size_t expected_size = BOOTSTRAP_V2_HEADER_SIZE + (size_t)group_count * BOOTSTRAP_V2_GROUP_SIZE; + if (entry_count > (SIZE_MAX - expected_size) / BOOTSTRAP_V2_ENTRY_SIZE) + return LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + expected_size += (size_t)entry_count * BOOTSTRAP_V2_ENTRY_SIZE; + if (artifact_size != expected_size) + return LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + + BootstrapV2Entry *entries = calloc(entry_count, sizeof(*entries)); + bool *covered = calloc(entry_count, sizeof(*covered)); + if (!entries || !covered) { + free(entries); + free(covered); + return LARDON3D_CALIBRATION_BOOTSTRAP_V2_OUT_OF_MEMORY; + } + + Lardon3DCalibrationBootstrapV2Result result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK; + size_t parsed_entries = 0; + unsigned char previous_group_identity[32] = {0}; + uint32_t previous_group_version = 0; + for (uint32_t group_index = 0; + group_index < group_count && result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK; + ++group_index) { + const unsigned char *group_start = reader.bytes; + unsigned char group_identity[32], evidence[6][32]; + uint32_t group_version, member_count; + if (!take(&reader, group_identity, 32) || !read_u32(&reader, &group_version)) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + break; + } + for (size_t index = 0; index < 6; ++index) + if (!take(&reader, evidence[index], 32)) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + break; + } + if (result != LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK || + !read_u32(&reader, &member_count)) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + break; + } + int group_order = group_index == 0 ? -1 : + memcmp(previous_group_identity, group_identity, 32); + if (!nonzero_digest(group_identity) || group_version == 0 || member_count == 0 || + member_count > entry_count - parsed_entries || group_order > 0 || + (group_order == 0 && previous_group_version >= group_version)) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + break; + } + for (size_t index = 0; index < 6; ++index) + if (!nonzero_digest(evidence[index])) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + break; + } + memcpy(previous_group_identity, group_identity, 32); + previous_group_version = group_version; + size_t group_first_entry = parsed_entries; + uint32_t previous_item_index = 0; + for (uint32_t member_index = 0; + member_index < member_count && result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK; + ++member_index) { + BootstrapV2Entry *entry = &entries[parsed_entries]; + uint32_t width, height, support_images, support_observations, validation_flags; + double reprojection_rmse_px, maximum_parameter_delta; + if (!read_u32(&reader, &entry->selected_item_index) || + !read_u64(&reader, &entry->member.image_id) || + !take(&reader, entry->representation_sha256, 32) || !read_u32(&reader, &width) || + !read_u32(&reader, &height) || !read_f64(&reader, &entry->calibration.fx) || + !read_f64(&reader, &entry->calibration.fy) || + !read_f64(&reader, &entry->calibration.cx) || + !read_f64(&reader, &entry->calibration.cy) || + !read_f64(&reader, &entry->calibration.k1) || + !read_f64(&reader, &entry->calibration.k2) || + !read_f64(&reader, &entry->calibration.p1) || + !read_f64(&reader, &entry->calibration.p2) || + !read_u32(&reader, &support_images) || + !read_u32(&reader, &support_observations) || + !read_f64(&reader, &reprojection_rmse_px) || + !read_f64(&reader, &maximum_parameter_delta) || + !read_u32(&reader, &validation_flags) || + entry->selected_item_index >= entry_count || covered[entry->selected_item_index] || + (member_index > 0 && previous_item_index >= entry->selected_item_index) || + entry->member.image_id == 0 || !nonzero_digest(entry->representation_sha256) || + width == 0 || height == 0 || entry->calibration.fx <= 0.0 || + entry->calibration.fy <= 0.0 || entry->calibration.cx < 0.0 || + entry->calibration.cy < 0.0 || entry->calibration.cx >= (double)width || + entry->calibration.cy >= (double)height || support_images == 0 || + support_observations == 0 || reprojection_rmse_px < 0.0 || + maximum_parameter_delta < 0.0 || validation_flags != BOOTSTRAP_V2_VALIDATION_FLAGS) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + break; + } + for (size_t prior = 0; prior < parsed_entries; ++prior) + if (entries[prior].member.image_id == entry->member.image_id) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + break; + } + covered[entry->selected_item_index] = true; + previous_item_index = entry->selected_item_index; + entry->calibration.model_kind = model_kind; + entry->calibration.model_version = model_version; + entry->calibration.width = width; + entry->calibration.height = height; + entry->calibration.provenance_kind = + LARDON3D_SPARSE_SFM_PROVENANCE_IMPORTED_TRUSTED; + ++parsed_entries; + } + if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) { + unsigned char group_fingerprint[32]; + size_t group_size = (size_t)(reader.bytes - group_start); + if (!sha256(group_start, group_size, group_fingerprint)) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_DB_ERROR; + break; + } + /* Group-local hashing preserves independently acquired provenance: an + * unrelated group's addition cannot redefine these calibrations. */ + for (size_t index = group_first_entry; index < parsed_entries; ++index) + memcpy(entries[index].calibration.provenance_fingerprint, group_fingerprint, 32); + } + } + if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK && + (parsed_entries != entry_count || reader.remaining != 0)) + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + for (uint32_t index = 0; + index < entry_count && result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK; ++index) + if (!covered[index]) result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT; + + Lardon3DProjectDbSelectedExecution execution; + Lardon3DProjectDbResult db_status = LARDON3D_PROJECT_DB_OK; + if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) { + db_status = lardon3d_project_db_load_selected_execution(database, execution_id, &execution); + if (db_status != LARDON3D_PROJECT_DB_OK) + result = db_result(db_status); + else if (execution.item_count != entry_count || + (execution.stage != LARDON3D_SELECTED_EXECUTION_CALIBRATION && + execution.stage != LARDON3D_SELECTED_EXECUTION_READY)) + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_SELECTION_CONFLICT; + } + for (size_t index = 0; + index < parsed_entries && result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK; ++index) { + BootstrapV2Entry *entry = &entries[index]; + Lardon3DProjectDbSelectedExecutionItem selected; + Lardon3DProjectDbImage image; + Lardon3DProjectDbImageAsset asset; + db_status = lardon3d_project_db_load_selected_execution_item( + database, execution_id, entry->selected_item_index, &selected); + if (db_status != LARDON3D_PROJECT_DB_OK || !selected.has_image || + selected.image_id != entry->member.image_id) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_SELECTION_CONFLICT; + break; + } + db_status = lardon3d_project_db_load_image(database, entry->member.image_id, &image, &asset); + if (db_status != LARDON3D_PROJECT_DB_OK || + memcmp(asset.sha256, entry->representation_sha256, 32) != 0) { + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_SELECTION_CONFLICT; + break; + } + } + + /* All bytes and immutable selected bindings are verified before this first + * write. Publication uses the existing short, content-addressed DB APIs; a + * crash may retain reusable calibrations but READY remains guarded by the + * single complete-scope attachment below. */ + for (size_t index = 0; + index < parsed_entries && result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK; ++index) { + Lardon3DSparseCalibration stored; + db_status = lardon3d_sparse_calibration_create(database, &entries[index].calibration, + &stored); + if (db_status != LARDON3D_PROJECT_DB_OK) { + result = db_result(db_status); + break; + } + entries[index].member.calibration_id = stored.calibration_id; + memcpy(entries[index].member.calibration_hash, stored.scientific_hash, 32); + } + + Lardon3DSparseCalibrationScope scope; + if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) { + Lardon3DSparseCalibrationMember *members = calloc(entry_count, sizeof(*members)); + if (!members) + result = LARDON3D_CALIBRATION_BOOTSTRAP_V2_OUT_OF_MEMORY; + else { + for (size_t index = 0; index < entry_count; ++index) members[index] = entries[index].member; + db_status = lardon3d_sparse_calibration_scope_create(database, members, entry_count, &scope); + free(members); + if (db_status != LARDON3D_PROJECT_DB_OK) result = db_result(db_status); + } + } + if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) { + db_status = lardon3d_project_db_assign_selected_calibration_scope(database, execution_id, + scope.scope_id); + if (db_status != LARDON3D_PROJECT_DB_OK) result = db_result(db_status); + } + if (result == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) { + memcpy(output->artifact_sha256, artifact_sha256, 32); + output->scope = scope; + output->calibration_count = entry_count; + output->group_count = group_count; + } + free(entries); + free(covered); + return result; +} diff --git a/src/calibration_tooling_v2.c b/src/calibration_tooling_v2.c new file mode 100644 index 0000000..076f6b4 --- /dev/null +++ b/src/calibration_tooling_v2.c @@ -0,0 +1,198 @@ +#include + +#include +#include +#include +#include + +enum { + TOOLING_V2_HEADER_SIZE = 28, + TOOLING_V2_GROUP_SIZE = 232, + TOOLING_V2_ENTRY_SIZE = 144, +}; + +static bool nonzero_digest(const unsigned char value[32]) { + unsigned char any = 0; + for (size_t index = 0; index < 32; ++index) any |= value[index]; + return any != 0; +} + +static void put_u32(unsigned char *output, uint32_t value) { + for (size_t index = 0; index < 4; ++index) + output[index] = (unsigned char)(value >> (8u * index)); +} + +static void put_u64(unsigned char *output, uint64_t value) { + for (size_t index = 0; index < 8; ++index) + output[index] = (unsigned char)(value >> (8u * index)); +} + +static void put_f64(unsigned char *output, double value) { + uint64_t bits = 0; + /* The format has one representation of zero so equal scientific values do + * not acquire different artifact identities through a negative sign bit. */ + if (value == 0.0) value = 0.0; + memcpy(&bits, &value, sizeof(bits)); + put_u64(output, bits); +} + +static bool sha256(const unsigned char *bytes, size_t size, unsigned char output[32]) { + unsigned int output_size = 0; + return EVP_Digest(bytes, size, output, &output_size, EVP_sha256(), NULL) == 1 && + output_size == 32; +} + +static bool entry_valid(const Lardon3DCalibrationToolingV2Entry *entry) { + const double parameters[] = {entry->fx, entry->fy, entry->cx, entry->cy, + entry->k1, entry->k2, entry->p1, entry->p2}; + if (entry->image_id == 0 || !nonzero_digest(entry->representation_sha256) || + entry->width == 0 || entry->height == 0 || entry->support_images == 0 || + entry->support_observations == 0 || + entry->validation_flags != LARDON3D_CALIBRATION_TOOLING_V2_VALIDATION_FLAGS) + return false; + for (size_t index = 0; index < sizeof(parameters) / sizeof(parameters[0]); ++index) + if (!isfinite(parameters[index])) return false; + return entry->fx > 0.0 && entry->fy > 0.0 && entry->cx >= 0.0 && entry->cy >= 0.0 && + entry->cx < (double)entry->width && entry->cy < (double)entry->height && + isfinite(entry->reprojection_rmse_px) && entry->reprojection_rmse_px >= 0.0 && + isfinite(entry->maximum_parameter_delta) && entry->maximum_parameter_delta >= 0.0; +} + +Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_validate( + const Lardon3DCalibrationToolingV2Evidence *evidence) { + if (!evidence || !evidence->groups || evidence->group_count == 0 || + evidence->group_count > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_GROUPS || + evidence->entry_count == 0 || + evidence->entry_count > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_ENTRIES) + return LARDON3D_CALIBRATION_TOOLING_V2_INVALID_ARGUMENT; + + bool covered[LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_ENTRIES] = {false}; + size_t total_entries = 0; + for (size_t group_index = 0; group_index < evidence->group_count; ++group_index) { + const Lardon3DCalibrationToolingV2Group *group = &evidence->groups[group_index]; + if (!group->entries || group->entry_count == 0 || group->group_version == 0 || + !nonzero_digest(group->group_identity_sha256) || + !nonzero_digest(group->optical_state_sha256) || !nonzero_digest(group->target_sha256) || + !nonzero_digest(group->solver_executable_sha256) || + !nonzero_digest(group->solver_configuration_sha256) || + !nonzero_digest(group->initialization_evidence_sha256) || + !nonzero_digest(group->validation_evidence_sha256)) + return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED; + if (group_index > 0) { + const Lardon3DCalibrationToolingV2Group *previous = &evidence->groups[group_index - 1]; + int order = memcmp(previous->group_identity_sha256, group->group_identity_sha256, 32); + if (order > 0 || (order == 0 && previous->group_version >= group->group_version)) + return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED; + } + if (group->entry_count > evidence->entry_count - total_entries) + return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED; + total_entries += group->entry_count; + for (size_t entry_index = 0; entry_index < group->entry_count; ++entry_index) { + const Lardon3DCalibrationToolingV2Entry *entry = &group->entries[entry_index]; + if (!entry_valid(entry) || entry->selected_item_index >= evidence->entry_count || + covered[entry->selected_item_index] || + (entry_index > 0 && + group->entries[entry_index - 1].selected_item_index >= entry->selected_item_index)) + return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED; + covered[entry->selected_item_index] = true; + /* Scientific images cannot belong to two groups under different item + * indexes; item coverage alone would not detect that identity conflict. */ + for (size_t prior_group = 0; prior_group <= group_index; ++prior_group) { + const Lardon3DCalibrationToolingV2Group *candidate_group = + &evidence->groups[prior_group]; + size_t limit = prior_group == group_index ? entry_index : candidate_group->entry_count; + for (size_t prior_entry = 0; prior_entry < limit; ++prior_entry) + if (candidate_group->entries[prior_entry].image_id == entry->image_id) + return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED; + } + } + } + if (total_entries != evidence->entry_count) + return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED; + for (size_t index = 0; index < evidence->entry_count; ++index) + if (!covered[index]) return LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED; + return LARDON3D_CALIBRATION_TOOLING_V2_OK; +} + +Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_produce( + const Lardon3DCalibrationToolingV2Evidence *evidence, unsigned char *artifact, + size_t artifact_capacity, size_t *written, unsigned char artifact_sha256[32]) { + if (written) *written = 0; + if (!artifact || !artifact_sha256) return LARDON3D_CALIBRATION_TOOLING_V2_INVALID_ARGUMENT; + Lardon3DCalibrationToolingV2Result result = + lardon3d_calibration_tooling_v2_validate(evidence); + if (result != LARDON3D_CALIBRATION_TOOLING_V2_OK) return result; + if (evidence->group_count > (SIZE_MAX - TOOLING_V2_HEADER_SIZE) / TOOLING_V2_GROUP_SIZE) + return LARDON3D_CALIBRATION_TOOLING_V2_CAPACITY; + size_t size = TOOLING_V2_HEADER_SIZE + evidence->group_count * TOOLING_V2_GROUP_SIZE; + if (evidence->entry_count > (SIZE_MAX - size) / TOOLING_V2_ENTRY_SIZE) + return LARDON3D_CALIBRATION_TOOLING_V2_CAPACITY; + size += evidence->entry_count * TOOLING_V2_ENTRY_SIZE; + if (size > LARDON3D_CALIBRATION_BOOTSTRAP_V2_MAX_BYTES || artifact_capacity < size) + return LARDON3D_CALIBRATION_TOOLING_V2_CAPACITY; + + size_t at = 0; + memcpy(artifact + at, "L3DCALB2", 8); + at += 8; +#define PUT32(value) do { put_u32(artifact + at, (uint32_t)(value)); at += 4; } while (0) + PUT32(LARDON3D_CALIBRATION_TOOLING_V2_VERSION); + PUT32(LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE); + PUT32(LARDON3D_SPARSE_SFM_CALIBRATION_VERSION); + PUT32(evidence->group_count); + PUT32(evidence->entry_count); + for (size_t group_index = 0; group_index < evidence->group_count; ++group_index) { + const Lardon3DCalibrationToolingV2Group *group = &evidence->groups[group_index]; + const unsigned char *digests[] = { + group->group_identity_sha256, group->optical_state_sha256, group->target_sha256, + group->solver_executable_sha256, group->solver_configuration_sha256, + group->initialization_evidence_sha256, group->validation_evidence_sha256}; + memcpy(artifact + at, digests[0], 32); at += 32; + PUT32(group->group_version); + for (size_t digest_index = 1; digest_index < 7; ++digest_index) { + memcpy(artifact + at, digests[digest_index], 32); + at += 32; + } + PUT32(group->entry_count); + for (size_t entry_index = 0; entry_index < group->entry_count; ++entry_index) { + const Lardon3DCalibrationToolingV2Entry *entry = &group->entries[entry_index]; + PUT32(entry->selected_item_index); + put_u64(artifact + at, entry->image_id); at += 8; + memcpy(artifact + at, entry->representation_sha256, 32); at += 32; + PUT32(entry->width); PUT32(entry->height); + const double parameters[] = {entry->fx, entry->fy, entry->cx, entry->cy, + entry->k1, entry->k2, entry->p1, entry->p2}; + for (size_t index = 0; index < 8; ++index) { + put_f64(artifact + at, parameters[index]); at += 8; + } + PUT32(entry->support_images); PUT32(entry->support_observations); + put_f64(artifact + at, entry->reprojection_rmse_px); at += 8; + put_f64(artifact + at, entry->maximum_parameter_delta); at += 8; + PUT32(entry->validation_flags); + } + } +#undef PUT32 + if (at != size || !sha256(artifact, size, artifact_sha256)) + return LARDON3D_CALIBRATION_TOOLING_V2_ENCODING_ERROR; + if (written) *written = size; + return LARDON3D_CALIBRATION_TOOLING_V2_OK; +} + +Lardon3DCalibrationToolingV2Result lardon3d_calibration_tooling_v2_import( + Lardon3DProjectDb *database, uint64_t execution_id, + const Lardon3DCalibrationToolingV2Evidence *evidence, unsigned char *artifact, + size_t artifact_capacity, size_t *written, + Lardon3DCalibrationBootstrapV2Output *output) { + unsigned char artifact_sha256[32]; + size_t local_written = 0; + size_t *encoded_size = written ? written : &local_written; + Lardon3DCalibrationToolingV2Result result = lardon3d_calibration_tooling_v2_produce( + evidence, artifact, artifact_capacity, encoded_size, artifact_sha256); + if (result != LARDON3D_CALIBRATION_TOOLING_V2_OK) return result; + if (!database || execution_id == 0 || !output) + return LARDON3D_CALIBRATION_TOOLING_V2_INVALID_ARGUMENT; + return lardon3d_calibration_bootstrap_v2_import( + database, execution_id, artifact, *encoded_size, artifact_sha256, output) == + LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK + ? LARDON3D_CALIBRATION_TOOLING_V2_OK + : LARDON3D_CALIBRATION_TOOLING_V2_IMPORT_ERROR; +} diff --git a/tests/test_calibration_publication_v2.c b/tests/test_calibration_publication_v2.c new file mode 100644 index 0000000..269c08a --- /dev/null +++ b/tests/test_calibration_publication_v2.c @@ -0,0 +1,348 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define CHECK(condition) do { if (!(condition)) { \ + fprintf(stderr, "calibration publication v2 failure at line %d: %s\n", \ + __LINE__, #condition); \ + return false; \ +} } while (0) + +enum { + ARTIFACT_SIZE = 780, + ARTIFACT_HEADER_SIZE = 28, + GROUP_RECORD_SIZE = 376, + GROUP_MEMBER_OFFSET = 232, + MEMBER_SELECTED_INDEX_OFFSET = 0, + MEMBER_IMAGE_ID_OFFSET = 4, + MEMBER_FX_OFFSET = 52, +}; + +static bool sql(const char *path, const char *text) { + sqlite3 *database = NULL; + if (sqlite3_open(path, &database) != SQLITE_OK) return false; + int result = sqlite3_exec(database, text, NULL, NULL, NULL); + return sqlite3_close(database) == SQLITE_OK && result == SQLITE_OK; +} + +static bool scalar(const char *path, const char *text, int *value) { + sqlite3 *database = NULL; + sqlite3_stmt *statement = NULL; + if (sqlite3_open(path, &database) != SQLITE_OK || + sqlite3_prepare_v2(database, text, -1, &statement, NULL) != SQLITE_OK) { + if (statement) sqlite3_finalize(statement); + if (database) sqlite3_close(database); + return false; + } + bool ok = sqlite3_step(statement) == SQLITE_ROW; + if (ok) *value = sqlite3_column_int(statement, 0); + return sqlite3_finalize(statement) == SQLITE_OK && + sqlite3_close(database) == SQLITE_OK && ok; +} + +static bool digest(const unsigned char *bytes, size_t size, unsigned char output[32]) { + unsigned int output_size = 0; + return EVP_Digest(bytes, size, output, &output_size, EVP_sha256(), NULL) == 1 && + output_size == 32; +} + +static void fill_digest(unsigned char output[32], unsigned char seed) { + memset(output, seed, 32); +} + +static void write_u32_le(unsigned char output[4], uint32_t value) { + for (size_t index = 0; index < 4; ++index) + output[index] = (unsigned char)(value >> (8u * index)); +} + +static void write_u64_le(unsigned char output[8], uint64_t value) { + for (size_t index = 0; index < 8; ++index) + output[index] = (unsigned char)(value >> (8u * index)); +} + +/* Synthetic evidence deliberately represents two independent optical groups; + * it tests publication mechanics and does not claim a physical calibration. */ +static void evidence_fixture(Lardon3DCalibrationToolingV2Evidence *evidence, + Lardon3DCalibrationToolingV2Group groups[2], + Lardon3DCalibrationToolingV2Entry entries[2]) { + memset(evidence, 0, sizeof(*evidence)); + memset(groups, 0, 2 * sizeof(*groups)); + memset(entries, 0, 2 * sizeof(*entries)); + for (size_t index = 0; index < 2; ++index) { + Lardon3DCalibrationToolingV2Group *group = &groups[index]; + Lardon3DCalibrationToolingV2Entry *entry = &entries[index]; + fill_digest(group->group_identity_sha256, (unsigned char)(1 + index)); + group->group_version = 1; + fill_digest(group->optical_state_sha256, (unsigned char)(10 + index)); + fill_digest(group->target_sha256, (unsigned char)(20 + index)); + fill_digest(group->solver_executable_sha256, (unsigned char)(30 + index)); + fill_digest(group->solver_configuration_sha256, (unsigned char)(40 + index)); + fill_digest(group->initialization_evidence_sha256, (unsigned char)(50 + index)); + fill_digest(group->validation_evidence_sha256, (unsigned char)(60 + index)); + group->entries = entry; + group->entry_count = 1; + + entry->selected_item_index = (uint32_t)index; + entry->image_id = index + 1; + fill_digest(entry->representation_sha256, (unsigned char)(0x11 * (index + 1))); + entry->width = index == 0 ? 4000 : 6000; + entry->height = index == 0 ? 2250 : 4000; + entry->fx = index == 0 ? 3000.0 : 4500.0; + entry->fy = index == 0 ? 3001.0 : 4502.0; + entry->cx = index == 0 ? 2000.0 : 3000.0; + entry->cy = index == 0 ? 1100.0 : 2000.0; + entry->k1 = index == 0 ? 0.01 : 0.02; + entry->k2 = index == 0 ? -0.01 : -0.02; + entry->p1 = 0.001; + entry->p2 = -0.001; + entry->support_images = 50; + entry->support_observations = 2000; + entry->reprojection_rmse_px = 0.4; + entry->maximum_parameter_delta = 0.01; + entry->validation_flags = LARDON3D_CALIBRATION_TOOLING_V2_VALIDATION_FLAGS; + } + evidence->groups = groups; + evidence->group_count = 2; + evidence->entry_count = 2; +} + +static bool validation_cases(void) { + Lardon3DCalibrationToolingV2Evidence evidence; + Lardon3DCalibrationToolingV2Group groups[2]; + Lardon3DCalibrationToolingV2Entry entries[2]; + evidence_fixture(&evidence, groups, entries); + CHECK(lardon3d_calibration_tooling_v2_validate(&evidence) == + LARDON3D_CALIBRATION_TOOLING_V2_OK); + + evidence.group_count = 1; + CHECK(lardon3d_calibration_tooling_v2_validate(&evidence) == + LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED); + evidence.group_count = 2; + entries[1].image_id = entries[0].image_id; + CHECK(lardon3d_calibration_tooling_v2_validate(&evidence) == + LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED); + entries[1].image_id = 2; + Lardon3DCalibrationToolingV2Group swapped[2] = {groups[1], groups[0]}; + evidence.groups = swapped; + CHECK(lardon3d_calibration_tooling_v2_validate(&evidence) == + LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED); + evidence.groups = groups; + entries[1].fx = NAN; + CHECK(lardon3d_calibration_tooling_v2_validate(&evidence) == + LARDON3D_CALIBRATION_TOOLING_V2_EVIDENCE_REJECTED); + return true; +} + +static bool create_execution(const char *path, Lardon3DProjectDb **database, + Lardon3DProjectDbSelectedExecution *execution) { + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + CHECK(lardon3d_project_db_open(path, database, error) == LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(*database); + *database = NULL; + CHECK(sql(path, + "INSERT INTO scansets(scanset_id,name,created_at,updated_at) VALUES(1,'v2',1,1);" + "INSERT INTO tasks VALUES(1,'quality','photo_quality.triage',1,5,5,100,1,0,0,0,0,1);" + "INSERT INTO tasks VALUES(2,'campaign','acquisition_campaign.run',1,5,5,100,1,0,0,0,0,1);" + "INSERT INTO photo_quality_triage_tasks VALUES(1,1,2,1,X'01');" + "INSERT INTO photo_quality_triage_results VALUES" + "(1,1,0,1,0,1,0,100,100,100,100,1,1,0,0,1,1,0,'GOOD')," + "(1,2,1,1,0,1,0,100,100,100,100,1,1,0,0,1,1,0,'GOOD');" + "INSERT INTO acquisition_campaign_tasks VALUES(2,1,2,2,X'02');" + "INSERT INTO captures VALUES(1,1,1),(2,1,2);" + "INSERT INTO acquisition_campaign_captures VALUES(2,11,1),(2,12,2);" + "INSERT INTO image_assets VALUES" + "(1,X'1111111111111111111111111111111111111111111111111111111111111111'," + "'assets/images/11/1111111111111111111111111111111111111111111111111111111111111111'," + "1,1,1)," + "(2,X'2222222222222222222222222222222222222222222222222222222222222222'," + "'assets/images/22/2222222222222222222222222222222222222222222222222222222222222222'," + "1,1,2);" + "INSERT INTO images VALUES" + "(1,1,1,'one.jpg','/one.jpg',NULL,1)," + "(2,1,2,'two.jpg','/two.jpg',NULL,2);" + "INSERT INTO capture_images VALUES(1,1),(2,2);")); + CHECK(lardon3d_project_db_open(path, database, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbSelectedExecutionItem items[2] = { + {.item_index = 0, .quality_group_id = 1, .campaign_group_id = 11, + .capture_id = 1, + .representation_source = LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE}, + {.item_index = 1, .quality_group_id = 2, .campaign_group_id = 12, + .capture_id = 2, + .representation_source = LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE}}; + CHECK(lardon3d_project_db_create_selected_execution(*database, 1, 2, items, 2, 10, + execution) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_record_selected_representation( + *database, execution->execution_id, 0, 1, 1) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_record_selected_representation( + *database, execution->execution_id, 1, 2, 2) == LARDON3D_PROJECT_DB_OK); + return true; +} + +static bool malformed_artifact_is_prepublication( + const char *path, Lardon3DProjectDb *database, uint64_t execution_id, + const unsigned char artifact[ARTIFACT_SIZE]) { + unsigned char artifact_sha256[32]; + Lardon3DCalibrationBootstrapV2Output output; + Lardon3DProjectDbSelectedExecution execution; + int count = -1; + memset(&output, 0xa5, sizeof(output)); + CHECK(digest(artifact, ARTIFACT_SIZE, artifact_sha256)); + CHECK(lardon3d_calibration_bootstrap_v2_import( + database, execution_id, artifact, ARTIFACT_SIZE, artifact_sha256, &output) == + LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT); + const unsigned char zero_output[sizeof(output)] = {0}; + CHECK(memcmp(&output, zero_output, sizeof(output)) == 0); + CHECK(scalar(path, "SELECT COUNT(*) FROM sparse_calibrations", &count) && count == 0); + CHECK(scalar(path, "SELECT COUNT(*) FROM sparse_calibration_scopes", &count) && count == 0); + CHECK(lardon3d_project_db_load_selected_execution(database, execution_id, &execution) == + LARDON3D_PROJECT_DB_OK && + execution.stage == LARDON3D_SELECTED_EXECUTION_CALIBRATION && + !execution.has_calibration_scope); + return true; +} + +static bool production_import(void) { + char directory[] = "/tmp/lardon3d-calibration-publication-v2-XXXXXX"; + CHECK(mkdtemp(directory) != NULL); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/project.db", directory) > 0); + Lardon3DProjectDb *database = NULL; + Lardon3DProjectDbSelectedExecution execution; + CHECK(create_execution(path, &database, &execution)); + + Lardon3DCalibrationToolingV2Evidence evidence; + Lardon3DCalibrationToolingV2Group groups[2]; + Lardon3DCalibrationToolingV2Entry entries[2]; + evidence_fixture(&evidence, groups, entries); + unsigned char artifact_a[ARTIFACT_SIZE + 1], artifact_b[ARTIFACT_SIZE]; + unsigned char hash_a[32], hash_b[32]; + size_t size_a = 0, size_b = 0; + CHECK(lardon3d_calibration_tooling_v2_produce( + &evidence, artifact_a, ARTIFACT_SIZE, &size_a, hash_a) == + LARDON3D_CALIBRATION_TOOLING_V2_OK); + CHECK(lardon3d_calibration_tooling_v2_produce( + &evidence, artifact_b, sizeof(artifact_b), &size_b, hash_b) == + LARDON3D_CALIBRATION_TOOLING_V2_OK); + CHECK(size_a == ARTIFACT_SIZE && size_b == size_a && + memcmp(artifact_a, artifact_b, size_a) == 0 && memcmp(hash_a, hash_b, 32) == 0); + + /* These checksum-valid corruptions cross the production bootstrap parser + * directly. Offsets select fields in the two canonical single-member group + * records; they do not duplicate the parser's validation decisions. */ + unsigned char malformed[ARTIFACT_SIZE]; + const size_t first_member = ARTIFACT_HEADER_SIZE + GROUP_MEMBER_OFFSET; + const size_t second_member = + ARTIFACT_HEADER_SIZE + GROUP_RECORD_SIZE + GROUP_MEMBER_OFFSET; + memcpy(malformed, artifact_b, sizeof(malformed)); + write_u64_le(malformed + first_member + MEMBER_FX_OFFSET, UINT64_C(0x7ff8000000000000)); + CHECK(malformed_artifact_is_prepublication( + path, database, execution.execution_id, malformed)); + + memcpy(malformed, artifact_b, sizeof(malformed)); + write_u32_le(malformed + second_member + MEMBER_SELECTED_INDEX_OFFSET, 0); + CHECK(malformed_artifact_is_prepublication( + path, database, execution.execution_id, malformed)); + + memcpy(malformed, artifact_b, sizeof(malformed)); + write_u64_le(malformed + second_member + MEMBER_IMAGE_ID_OFFSET, 1); + CHECK(malformed_artifact_is_prepublication( + path, database, execution.execution_id, malformed)); + + memcpy(malformed, artifact_b, sizeof(malformed)); + write_u32_le(malformed + second_member + MEMBER_SELECTED_INDEX_OFFSET, 2); + CHECK(malformed_artifact_is_prepublication( + path, database, execution.execution_id, malformed)); + + Lardon3DCalibrationBootstrapV2Output output; + unsigned char wrong_hash[32] = {0}; + CHECK(lardon3d_calibration_bootstrap_v2_import( + database, execution.execution_id, artifact_a, size_a, wrong_hash, &output) == + LARDON3D_CALIBRATION_BOOTSTRAP_V2_PROVENANCE_MISMATCH); + unsigned char trailing_hash[32]; + artifact_a[size_a] = 0; + CHECK(digest(artifact_a, size_a + 1, trailing_hash)); + CHECK(lardon3d_calibration_bootstrap_v2_import( + database, execution.execution_id, artifact_a, size_a + 1, trailing_hash, &output) == + LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT); + + unsigned char reversed[ARTIFACT_SIZE], temporary[GROUP_RECORD_SIZE], reversed_hash[32]; + memcpy(reversed, artifact_b, sizeof(reversed)); + memcpy(temporary, reversed + 28, sizeof(temporary)); + memcpy(reversed + 28, reversed + 28 + GROUP_RECORD_SIZE, GROUP_RECORD_SIZE); + memcpy(reversed + 28 + GROUP_RECORD_SIZE, temporary, GROUP_RECORD_SIZE); + CHECK(digest(reversed, sizeof(reversed), reversed_hash)); + CHECK(lardon3d_calibration_bootstrap_v2_import( + database, execution.execution_id, reversed, sizeof(reversed), reversed_hash, &output) == + LARDON3D_CALIBRATION_BOOTSTRAP_V2_MALFORMED_ARTIFACT); + + fill_digest(entries[1].representation_sha256, 0x33); + CHECK(lardon3d_calibration_tooling_v2_import( + database, execution.execution_id, &evidence, artifact_a, ARTIFACT_SIZE, + &size_a, &output) == LARDON3D_CALIBRATION_TOOLING_V2_IMPORT_ERROR); + fill_digest(entries[1].representation_sha256, 0x22); + int count = -1; + CHECK(scalar(path, "SELECT COUNT(*) FROM sparse_calibrations", &count) && count == 0); + CHECK(scalar(path, "SELECT COUNT(*) FROM sparse_calibration_scopes", &count) && count == 0); + CHECK(lardon3d_project_db_load_selected_execution(database, execution.execution_id, + &execution) == LARDON3D_PROJECT_DB_OK && + execution.stage == LARDON3D_SELECTED_EXECUTION_CALIBRATION && + !execution.has_calibration_scope); + + entries[1].image_id = 99; + CHECK(lardon3d_calibration_tooling_v2_import( + database, execution.execution_id, &evidence, artifact_a, ARTIFACT_SIZE, + &size_a, &output) == LARDON3D_CALIBRATION_TOOLING_V2_IMPORT_ERROR); + entries[1].image_id = 2; + CHECK(scalar(path, "SELECT COUNT(*) FROM sparse_calibrations", &count) && count == 0); + + CHECK(lardon3d_calibration_tooling_v2_produce( + &evidence, artifact_a, ARTIFACT_SIZE, &size_a, hash_a) == + LARDON3D_CALIBRATION_TOOLING_V2_OK); + Lardon3DCalibrationBootstrapV2Result imported = lardon3d_calibration_bootstrap_v2_import( + database, execution.execution_id, artifact_a, size_a, hash_a, &output); + if (imported != LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK) + fprintf(stderr, "valid v2 bootstrap result=%d\n", (int)imported); + CHECK(imported == LARDON3D_CALIBRATION_BOOTSTRAP_V2_OK); + uint64_t scope_id = output.scope.scope_id; + CHECK(output.group_count == 2 && output.calibration_count == 2 && scope_id != 0); + CHECK(lardon3d_calibration_tooling_v2_import( + database, execution.execution_id, &evidence, artifact_a, ARTIFACT_SIZE, + NULL, &output) == LARDON3D_CALIBRATION_TOOLING_V2_OK && + output.scope.scope_id == scope_id); + + Lardon3DSparseCalibrationMember members[2]; + size_t member_count = 0; + uint64_t next_image_id = 0; + CHECK(lardon3d_sparse_calibration_scope_list_members( + database, scope_id, 0, members, 2, &member_count, &next_image_id) == + LARDON3D_PROJECT_DB_OK && + member_count == 2 && next_image_id == 2 && members[0].image_id == 1 && + members[1].image_id == 2); + Lardon3DSparseCalibration calibrations[2]; + CHECK(lardon3d_sparse_calibration_load(database, members[0].calibration_id, + &calibrations[0]) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_sparse_calibration_load(database, members[1].calibration_id, + &calibrations[1]) == LARDON3D_PROJECT_DB_OK); + CHECK(memcmp(calibrations[0].provenance_fingerprint, + calibrations[1].provenance_fingerprint, 32) != 0); + CHECK(lardon3d_project_db_load_selected_execution(database, execution.execution_id, + &execution) == LARDON3D_PROJECT_DB_OK && + execution.stage == LARDON3D_SELECTED_EXECUTION_READY && + execution.has_calibration_scope && execution.calibration_scope_id == scope_id); + + lardon3d_project_db_close(database); + CHECK(unlink(path) == 0); + CHECK(rmdir(directory) == 0); + return true; +} + +int main(void) { + return validation_cases() && production_import() ? EXIT_SUCCESS : EXIT_FAILURE; +}