feat: add calibration workflow input boundary
This commit is contained in:
parent
311b303744
commit
e1cf83a158
9 changed files with 865 additions and 6 deletions
122
docs/architecture/calibration_workflow.md
Normal file
122
docs/architecture/calibration_workflow.md
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
# Calibration Workflow
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
```text
|
||||||
|
CALIBRATION_WORKFLOW=IN_PROGRESS
|
||||||
|
CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN
|
||||||
|
CURRENT_WORKFLOW_NEXT=EVIDENCE_MATERIALIZATION_V1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authority
|
||||||
|
|
||||||
|
Calibration Science v1, Calibration Tooling v1 and Calibration Bootstrap v1
|
||||||
|
remain FROZEN scientific and import authorities.
|
||||||
|
|
||||||
|
This workflow is bounded orchestration only. It does not introduce a solver,
|
||||||
|
Project DB schema version, Task kind, Sparse SfM execution or reconstructed
|
||||||
|
scientific evidence.
|
||||||
|
|
||||||
|
## Frozen flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
physical calibration acquisition
|
||||||
|
-> session.l3dcal
|
||||||
|
-> external Calibration Evidence Solver v1
|
||||||
|
-> immutable solver bundle
|
||||||
|
-> campaign-state evidence
|
||||||
|
-> Calibration Workflow
|
||||||
|
-> Calibration Tooling v1
|
||||||
|
-> L3DCALB1 v1
|
||||||
|
-> Calibration Bootstrap v1
|
||||||
|
-> selected execution READY
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Boundary v1
|
||||||
|
|
||||||
|
`CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN`.
|
||||||
|
|
||||||
|
The implementation is exposed through:
|
||||||
|
|
||||||
|
```text
|
||||||
|
include/lardon3d/calibration_workflow.h
|
||||||
|
src/calibration_workflow.cpp
|
||||||
|
```
|
||||||
|
|
||||||
|
Input Boundary v1 performs no Project DB mutation.
|
||||||
|
|
||||||
|
It accepts only bounded regular files and rejects special files and symlinks
|
||||||
|
before potentially blocking reads. File access follows the nonblocking,
|
||||||
|
close-on-exec regular-file discipline.
|
||||||
|
|
||||||
|
The bounded input set is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
session.l3dcal
|
||||||
|
session.l3dcal.bundle/detection.json
|
||||||
|
session.l3dcal.bundle/solve.json
|
||||||
|
session.l3dcal.bundle/evidence.json
|
||||||
|
session.l3dcal.bundle/producer.json
|
||||||
|
L3DCAL_CAMPAIGN_STATE_V1
|
||||||
|
```
|
||||||
|
|
||||||
|
It verifies:
|
||||||
|
|
||||||
|
- regular bounded files;
|
||||||
|
- SHA-256 identities;
|
||||||
|
- strict session syntax;
|
||||||
|
- structurally valid canonical JSON bundle members;
|
||||||
|
- exact session SHA binding through `producer.json`;
|
||||||
|
- decoder/version consistency;
|
||||||
|
- exact optical-state SHA equality;
|
||||||
|
- exact optical-state token equality;
|
||||||
|
- campaign-state identity consistency.
|
||||||
|
|
||||||
|
Malformed JSON, oversize files, symlinks, FIFOs, session digest mismatch and
|
||||||
|
optical-state mismatch are rejected.
|
||||||
|
|
||||||
|
Input Boundary v1 does not:
|
||||||
|
|
||||||
|
- open or mutate Project DB;
|
||||||
|
- construct `Lardon3DCalibrationToolingEvidence`;
|
||||||
|
- call Calibration Tooling;
|
||||||
|
- produce `L3DCALB1`;
|
||||||
|
- invoke Calibration Bootstrap;
|
||||||
|
- change selected-execution state.
|
||||||
|
|
||||||
|
## Campaign optical-state evidence
|
||||||
|
|
||||||
|
Project DB v23 retains exact explicit optical configuration identity, including
|
||||||
|
body, objective and focal state, but Calibration Science v1 requires a broader
|
||||||
|
scientific key including focus, stabilization and processing/decode state.
|
||||||
|
|
||||||
|
No equality may be inferred between those domains.
|
||||||
|
|
||||||
|
`L3DCAL_CAMPAIGN_STATE_V1` therefore provides immutable external evidence for
|
||||||
|
the complete Science v1 optical state. A later coordinator stage must verify
|
||||||
|
this evidence against both the calibration session and each selected Capture's
|
||||||
|
explicit Project DB optical configuration.
|
||||||
|
|
||||||
|
Absence or disagreement remains `CALIBRATION_UNAVAILABLE`.
|
||||||
|
|
||||||
|
## Current next boundary
|
||||||
|
|
||||||
|
```text
|
||||||
|
CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1
|
||||||
|
```
|
||||||
|
|
||||||
|
The next stage parses the already validated bundle into bounded in-memory
|
||||||
|
Science v1 evidence:
|
||||||
|
|
||||||
|
- exact per-view evidence;
|
||||||
|
- exact repeated full-solve parameters;
|
||||||
|
- fit parameters;
|
||||||
|
- global and hold-out validation metrics;
|
||||||
|
- coordinate-equivalence evidence;
|
||||||
|
- immutable provenance digests.
|
||||||
|
|
||||||
|
It still performs no Project DB mutation.
|
||||||
|
|
||||||
|
Only after that boundary passes may the workflow bind the exact selected
|
||||||
|
execution, campaign representations and optical assignments and invoke the
|
||||||
|
FROZEN Tooling/Bootstrap path.
|
||||||
|
|
@ -527,6 +527,8 @@ The solver session contract now also requires explicit measured `white_border >=
|
||||||
|
|
||||||
`CALIBRATION_SOLVER_BUNDLE_REPAIR_V1=PASS/FROZEN`: retained per-view fields are now present in the actual solver output, `producer.json` is valid canonical JSON, solver-configuration records contain real line feeds, and the deterministic self-test checks these contents rather than byte identity alone.
|
`CALIBRATION_SOLVER_BUNDLE_REPAIR_V1=PASS/FROZEN`: retained per-view fields are now present in the actual solver output, `producer.json` is valid canonical JSON, solver-configuration records contain real line feeds, and the deterministic self-test checks these contents rather than byte identity alone.
|
||||||
|
|
||||||
|
`CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN`: the workflow now has a bounded non-mutating input boundary for session, solver bundle and campaign optical-state evidence. It rejects special/symlink/oversize files, invalid JSON, provenance digest mismatches and incompatible optical state. The next boundary is Evidence Materialization v1.
|
||||||
|
|
||||||
A bounded Tooling correction aligned planarity handling with Calibration Science v1: Science v1
|
A bounded Tooling correction aligned planarity handling with Calibration Science v1: Science v1
|
||||||
defines a categorical physical planarity attestation, not a numeric flatness threshold. Tooling
|
defines a categorical physical planarity attestation, not a numeric flatness threshold. Tooling
|
||||||
therefore rejects invented finite `target_flatness_mm` values. `L3DCALB1` v1 and Calibration
|
therefore rejects invented finite `target_flatness_mm` values. `L3DCALB1` v1 and Calibration
|
||||||
|
|
|
||||||
79
include/lardon3d/calibration_workflow.h
Normal file
79
include/lardon3d/calibration_workflow.h
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
#ifndef LARDON3D_CALIBRATION_WORKFLOW_H
|
||||||
|
#define LARDON3D_CALIBRATION_WORKFLOW_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_VERSION = 1,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_MAX_FILE_BYTES = 128 * 1024 * 1024,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS = 4096,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_OPTICAL_STATE_TOKEN_CAPACITY = 1025,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_OK = 0,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_CAPACITY,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH,
|
||||||
|
} Lardon3DCalibrationWorkflowResult;
|
||||||
|
|
||||||
|
/* `campaign_state_path` is a canonical external acquisition manifest:
|
||||||
|
*
|
||||||
|
* L3DCAL_CAMPAIGN_STATE_V1
|
||||||
|
* execution <selected-execution-id>
|
||||||
|
* optical_configuration <explicit-v23-configuration-id>
|
||||||
|
* optical_state <sha256> <complete-Science-v1-state-token>
|
||||||
|
* capture <zero-based-selected-item-index> <capture-id>
|
||||||
|
*
|
||||||
|
* Capture rows are contiguous and ordered. The later DB-binding stage proves
|
||||||
|
* that these IDs are exactly the selected execution and that each Capture owns
|
||||||
|
* the declared explicit optical configuration; this input boundary performs no
|
||||||
|
* DB access and therefore never treats the manifest alone as that proof. */
|
||||||
|
typedef struct {
|
||||||
|
const char *session_path;
|
||||||
|
const char *detection_path;
|
||||||
|
const char *solve_path;
|
||||||
|
const char *evidence_path;
|
||||||
|
const char *producer_path;
|
||||||
|
const char *campaign_state_path;
|
||||||
|
} Lardon3DCalibrationWorkflowInputFiles;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
unsigned char session_sha256[32];
|
||||||
|
unsigned char detection_sha256[32];
|
||||||
|
unsigned char solve_sha256[32];
|
||||||
|
unsigned char evidence_sha256[32];
|
||||||
|
unsigned char producer_sha256[32];
|
||||||
|
unsigned char campaign_state_sha256[32];
|
||||||
|
unsigned char optical_state_sha256[32];
|
||||||
|
unsigned char solver_executable_sha256[32];
|
||||||
|
unsigned char solver_configuration_sha256[32];
|
||||||
|
uint64_t selected_execution_id;
|
||||||
|
uint64_t optical_configuration_id;
|
||||||
|
uint32_t capture_count;
|
||||||
|
uint64_t capture_ids[LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS];
|
||||||
|
char optical_state_token[LARDON3D_CALIBRATION_WORKFLOW_OPTICAL_STATE_TOKEN_CAPACITY];
|
||||||
|
} Lardon3DCalibrationWorkflowInputBoundary;
|
||||||
|
|
||||||
|
/* Validate only the immutable external input boundary. This function performs
|
||||||
|
* no Project DB access, no calibration solve, no Tooling import and no
|
||||||
|
* selected-execution mutation. Every path is opened O_NONBLOCK/O_NOFOLLOW,
|
||||||
|
* must resolve to a bounded regular file, and is SHA-256 checked before its
|
||||||
|
* syntax/provenance is consumed. */
|
||||||
|
Lardon3DCalibrationWorkflowResult lardon3d_calibration_workflow_validate_input_boundary(
|
||||||
|
const Lardon3DCalibrationWorkflowInputFiles *files,
|
||||||
|
Lardon3DCalibrationWorkflowInputBoundary *boundary);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
13
meson.build
13
meson.build
|
|
@ -180,6 +180,7 @@ lardon3d_app = executable(
|
||||||
'src/optical_profiles.c',
|
'src/optical_profiles.c',
|
||||||
'src/calibration_bootstrap.c',
|
'src/calibration_bootstrap.c',
|
||||||
'src/calibration_tooling.c',
|
'src/calibration_tooling.c',
|
||||||
|
'src/calibration_workflow.cpp',
|
||||||
'src/sparse_sfm_geometry.cpp',
|
'src/sparse_sfm_geometry.cpp',
|
||||||
'src/sparse_sfm_incremental.cpp',
|
'src/sparse_sfm_incremental.cpp',
|
||||||
'src/sparse_sfm_bundle_adjustment.cpp',
|
'src/sparse_sfm_bundle_adjustment.cpp',
|
||||||
|
|
@ -895,6 +896,18 @@ calibration_tooling_test = executable(
|
||||||
|
|
||||||
test('calibration-tooling', calibration_tooling_test, timeout: 30)
|
test('calibration-tooling', calibration_tooling_test, timeout: 30)
|
||||||
|
|
||||||
|
calibration_workflow_test = executable(
|
||||||
|
'test-calibration-workflow',
|
||||||
|
sources: [
|
||||||
|
'tests/test_calibration_workflow.cpp',
|
||||||
|
'src/calibration_workflow.cpp',
|
||||||
|
],
|
||||||
|
include_directories: include_directories('include'),
|
||||||
|
dependencies: [openssl],
|
||||||
|
)
|
||||||
|
|
||||||
|
test('calibration-workflow', calibration_workflow_test, timeout: 30)
|
||||||
|
|
||||||
optical_profiles_test = executable(
|
optical_profiles_test = executable(
|
||||||
'test-optical-profiles',
|
'test-optical-profiles',
|
||||||
sources: [
|
sources: [
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
CURRENT_PROJECT_DB_SCHEMA=v25
|
CURRENT_PROJECT_DB_SCHEMA=v25
|
||||||
PRODUCTION_TASK_KINDS=16
|
PRODUCTION_TASK_KINDS=16
|
||||||
USER_FACING_UI_LANGUAGE_NORMALIZATION=PASS
|
USER_FACING_UI_LANGUAGE_NORMALIZATION=PASS
|
||||||
CURRENT_IMPLEMENTATION_CURSOR=1_CALIBRATION_WORKFLOW_COORDINATOR
|
CURRENT_IMPLEMENTATION_CURSOR=1_CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authority
|
## Authority
|
||||||
|
|
@ -31,6 +31,7 @@ Calibration solver white-border evidence PASS/FROZEN
|
||||||
Calibration solver producer identity PASS/FROZEN
|
Calibration solver producer identity PASS/FROZEN
|
||||||
Calibration solver per-view evidence PASS/FROZEN
|
Calibration solver per-view evidence PASS/FROZEN
|
||||||
Calibration solver bundle repair PASS/FROZEN
|
Calibration solver bundle repair PASS/FROZEN
|
||||||
|
Calibration workflow input boundary PASS/FROZEN
|
||||||
Calibration Tooling planarity alignment PASS/FROZEN
|
Calibration Tooling planarity alignment PASS/FROZEN
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,17 @@ CALIBRATION_SOLVER_PRODUCER_IDENTITY_V1=PASS/FROZEN
|
||||||
CALIBRATION_SOLVER_PER_VIEW_EVIDENCE_V1=PASS/FROZEN
|
CALIBRATION_SOLVER_PER_VIEW_EVIDENCE_V1=PASS/FROZEN
|
||||||
CALIBRATION_SOLVER_BUNDLE_REPAIR_V1=PASS/FROZEN
|
CALIBRATION_SOLVER_BUNDLE_REPAIR_V1=PASS/FROZEN
|
||||||
CALIBRATION_WORKFLOW=IN_PROGRESS
|
CALIBRATION_WORKFLOW=IN_PROGRESS
|
||||||
CURRENT_CALIBRATION_NEXT=WORKFLOW_COORDINATOR
|
CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN
|
||||||
|
CURRENT_CALIBRATION_NEXT=WORKFLOW_EVIDENCE_MATERIALIZATION_V1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authority
|
## Authority
|
||||||
|
|
||||||
`docs/architecture/calibration_science_v1.md`, `docs/architecture/calibration_bootstrap.md` and `docs/architecture/calibration_solver_preflight_v1.md` are the current specialized calibration documents.
|
`docs/architecture/calibration_science_v1.md`, `docs/architecture/calibration_bootstrap.md` and `docs/architecture/calibration_solver_preflight_v1.md` are the current specialized calibration documents.
|
||||||
|
|
||||||
`docs/architecture/calibration_tooling.md` is the specialized Tooling authority. The public API remains `include/lardon3d/calibration_tooling.h`.
|
`docs/architecture/calibration_tooling.md` is the specialized Tooling authority.
|
||||||
|
|
||||||
|
`docs/architecture/calibration_workflow.md` is the specialized workflow authority. The public API remains `include/lardon3d/calibration_tooling.h`.
|
||||||
|
|
||||||
A bounded corrective review established that Calibration Science v1 defines target planarity as a categorical physical attestation, not a numeric flatness tolerance. Tooling preserves its public structure layout while requiring `target_flatness_mm` to be NaN, so callers cannot invent a millimetre measurement. The canonical session's `planarity PASS <sha256>` evidence is bound through immutable initialization evidence.
|
A bounded corrective review established that Calibration Science v1 defines target planarity as a categorical physical attestation, not a numeric flatness tolerance. Tooling preserves its public structure layout while requiring `target_flatness_mm` to be NaN, so callers cannot invent a millimetre measurement. The canonical session's `planarity PASS <sha256>` evidence is bound through immutable initialization evidence.
|
||||||
|
|
||||||
|
|
@ -54,7 +57,7 @@ dedicated physical calibration acquisition
|
||||||
-> real Sparse SfM
|
-> real Sparse SfM
|
||||||
```
|
```
|
||||||
|
|
||||||
The current implementation gap is the workflow coordinator. It consumes the immutable `session.l3dcal` plus `detection.json`, `solve.json` and `evidence.json`, binds them to the exact selected execution and optical state, constructs the bounded Tooling evidence and never manufactures missing physical evidence.
|
The workflow coordinator is now implemented through its first bounded checkpoint. Input Boundary v1 validates immutable files, hashes, formats and complete optical-state equality without Project DB mutation. The current implementation gap is Evidence Materialization v1. It consumes the immutable `session.l3dcal` plus `detection.json`, `solve.json` and `evidence.json`, binds them to the exact selected execution and optical state, constructs the bounded Tooling evidence and never manufactures missing physical evidence.
|
||||||
|
|
||||||
## REQUIRED_PRODUCT_TARGET
|
## REQUIRED_PRODUCT_TARGET
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
IMPLEMENTATION_ORDER=DEPENDENCY_DRIVEN
|
IMPLEMENTATION_ORDER=DEPENDENCY_DRIVEN
|
||||||
IMPLEMENTATION_AUTHORIZATION=NO
|
IMPLEMENTATION_AUTHORIZATION=NO
|
||||||
STEP_0_USER_FACING_LANGUAGE_NORMALIZATION=PASS
|
STEP_0_USER_FACING_LANGUAGE_NORMALIZATION=PASS
|
||||||
CURRENT_NEXT=1_CALIBRATION_WORKFLOW_COORDINATOR
|
CURRENT_NEXT=1_CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authority
|
## Authority
|
||||||
|
|
@ -20,7 +20,7 @@ Implementation remains unauthorized until the human explicitly authorizes a tran
|
||||||
Default dependency order:
|
Default dependency order:
|
||||||
|
|
||||||
0. user-facing repository/UI language normalization where appropriate — PASS;
|
0. user-facing repository/UI language normalization where appropriate — PASS;
|
||||||
1. final usable calibration workflow — IN PROGRESS; current next sub-boundary: workflow coordinator;
|
1. final usable calibration workflow — IN PROGRESS; Input Boundary v1 PASS/FROZEN; current next sub-boundary: Evidence Materialization v1;
|
||||||
2. dedicated physical calibrated real campaign;
|
2. dedicated physical calibrated real campaign;
|
||||||
3. real Sparse SfM proof;
|
3. real Sparse SfM proof;
|
||||||
4. durable Dense/OpenMVS orchestration;
|
4. durable Dense/OpenMVS orchestration;
|
||||||
|
|
|
||||||
483
src/calibration_workflow.cpp
Normal file
483
src/calibration_workflow.cpp
Normal file
|
|
@ -0,0 +1,483 @@
|
||||||
|
#include <lardon3d/calibration_workflow.h>
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <charconv>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <locale>
|
||||||
|
#include <map>
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr size_t kSha256Bytes = 32;
|
||||||
|
constexpr size_t kJsonDepthMax = 64;
|
||||||
|
constexpr size_t kJsonKeyMax = 128;
|
||||||
|
|
||||||
|
struct MappedFile {
|
||||||
|
int fd = -1;
|
||||||
|
const unsigned char *data = nullptr;
|
||||||
|
size_t size = 0;
|
||||||
|
unsigned char sha256[kSha256Bytes]{};
|
||||||
|
|
||||||
|
~MappedFile() {
|
||||||
|
if (data && size) munmap(const_cast<unsigned char *>(data), size);
|
||||||
|
if (fd >= 0) close(fd);
|
||||||
|
}
|
||||||
|
MappedFile() = default;
|
||||||
|
MappedFile(const MappedFile &) = delete;
|
||||||
|
MappedFile &operator=(const MappedFile &) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool digest_bytes(const unsigned char *data, size_t size, unsigned char output[32]) {
|
||||||
|
unsigned int length = 0;
|
||||||
|
return EVP_Digest(data, size, output, &length, EVP_sha256(), nullptr) == 1 && length == 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DCalibrationWorkflowResult map_regular_file(const char *path, MappedFile *out) {
|
||||||
|
if (!path || !*path || !out) return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT;
|
||||||
|
int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW);
|
||||||
|
if (fd < 0) {
|
||||||
|
if (errno == ELOOP) return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
|
||||||
|
}
|
||||||
|
struct stat st{};
|
||||||
|
if (fstat(fd, &st) != 0) {
|
||||||
|
close(fd);
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
|
||||||
|
}
|
||||||
|
if (!S_ISREG(st.st_mode)) {
|
||||||
|
close(fd);
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
|
||||||
|
}
|
||||||
|
if (st.st_size <= 0) {
|
||||||
|
close(fd);
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
|
||||||
|
}
|
||||||
|
if (static_cast<uint64_t>(st.st_size) > LARDON3D_CALIBRATION_WORKFLOW_MAX_FILE_BYTES) {
|
||||||
|
close(fd);
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
|
||||||
|
}
|
||||||
|
const size_t size = static_cast<size_t>(st.st_size);
|
||||||
|
void *mapped = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||||
|
if (mapped == MAP_FAILED) {
|
||||||
|
close(fd);
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
|
||||||
|
}
|
||||||
|
out->fd = fd;
|
||||||
|
out->data = static_cast<const unsigned char *>(mapped);
|
||||||
|
out->size = size;
|
||||||
|
if (!digest_bytes(out->data, out->size, out->sha256))
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hex_digit(char c, unsigned *value) {
|
||||||
|
if (c >= '0' && c <= '9') *value = static_cast<unsigned>(c - '0');
|
||||||
|
else if (c >= 'a' && c <= 'f') *value = static_cast<unsigned>(c - 'a' + 10);
|
||||||
|
else if (c >= 'A' && c <= 'F') *value = static_cast<unsigned>(c - 'A' + 10);
|
||||||
|
else return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_sha256(std::string_view text, unsigned char output[32]) {
|
||||||
|
if (text.size() != 64) return false;
|
||||||
|
for (size_t i = 0; i < 32; ++i) {
|
||||||
|
unsigned hi = 0, lo = 0;
|
||||||
|
if (!hex_digit(text[2 * i], &hi) || !hex_digit(text[2 * i + 1], &lo)) return false;
|
||||||
|
output[i] = static_cast<unsigned char>((hi << 4u) | lo);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nonzero_sha(const unsigned char value[32]) {
|
||||||
|
unsigned char any = 0;
|
||||||
|
for (size_t i = 0; i < 32; ++i) any |= value[i];
|
||||||
|
return any != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool token_ok(std::string_view value) {
|
||||||
|
if (value.empty() || value.size() >= LARDON3D_CALIBRATION_WORKFLOW_OPTICAL_STATE_TOKEN_CAPACITY)
|
||||||
|
return false;
|
||||||
|
for (unsigned char c : value) {
|
||||||
|
if (!(std::isalnum(c) || c == '_' || c == '-' || c == '.' || c == ':')) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_u64_any(std::string_view text, uint64_t *value) {
|
||||||
|
if (!value || text.empty()) return false;
|
||||||
|
uint64_t parsed = 0;
|
||||||
|
auto result = std::from_chars(text.data(), text.data() + text.size(), parsed, 10);
|
||||||
|
if (result.ec != std::errc() || result.ptr != text.data() + text.size()) return false;
|
||||||
|
*value = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_u64_positive(std::string_view text, uint64_t *value) {
|
||||||
|
return parse_u64_any(text, value) && *value != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_u32_any(std::string_view text, uint32_t *value) {
|
||||||
|
uint64_t parsed = 0;
|
||||||
|
if (!parse_u64_any(text, &parsed) || parsed > UINT32_MAX) return false;
|
||||||
|
*value = static_cast<uint32_t>(parsed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_finite_double(std::string_view text, double *value) {
|
||||||
|
if (!value || text.empty() || text.size() > 128) return false;
|
||||||
|
std::istringstream stream{std::string(text)};
|
||||||
|
stream.imbue(std::locale::classic());
|
||||||
|
double parsed = 0.0;
|
||||||
|
stream >> parsed;
|
||||||
|
if (!stream || !stream.eof() || !std::isfinite(parsed)) return false;
|
||||||
|
*value = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string_view> words(std::string_view line) {
|
||||||
|
std::vector<std::string_view> out;
|
||||||
|
size_t at = 0;
|
||||||
|
while (at < line.size()) {
|
||||||
|
while (at < line.size() && (line[at] == ' ' || line[at] == '\t')) ++at;
|
||||||
|
if (at == line.size()) break;
|
||||||
|
size_t end = at;
|
||||||
|
while (end < line.size() && line[end] != ' ' && line[end] != '\t') ++end;
|
||||||
|
out.push_back(line.substr(at, end - at));
|
||||||
|
at = end;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SessionIdentity {
|
||||||
|
std::string decoder;
|
||||||
|
std::string decoder_version;
|
||||||
|
std::string optical_state_token;
|
||||||
|
unsigned char optical_state_sha256[32]{};
|
||||||
|
};
|
||||||
|
|
||||||
|
bool parse_session(std::string_view text, SessionIdentity *identity) {
|
||||||
|
if (!identity || text.empty() || text.back() != '\n' || text.find('\0') != std::string_view::npos ||
|
||||||
|
text.find('\r') != std::string_view::npos)
|
||||||
|
return false;
|
||||||
|
size_t at = 0;
|
||||||
|
size_t line_number = 0;
|
||||||
|
bool target = false, measurement = false, white_border = false, planarity = false;
|
||||||
|
bool decoder = false, optical_state = false, image = false;
|
||||||
|
while (at < text.size()) {
|
||||||
|
size_t end = text.find('\n', at);
|
||||||
|
if (end == std::string_view::npos) return false;
|
||||||
|
std::string_view line = text.substr(at, end - at);
|
||||||
|
at = end + 1;
|
||||||
|
++line_number;
|
||||||
|
if (line_number == 1) {
|
||||||
|
if (line != "L3DCAL_SESSION_V1") return false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.empty()) continue;
|
||||||
|
auto w = words(line);
|
||||||
|
if (w.empty()) return false;
|
||||||
|
const auto tag = w[0];
|
||||||
|
if (tag == "target") {
|
||||||
|
uint32_t squares_x = 0, squares_y = 0; double square = 0, marker = 0;
|
||||||
|
if (target || w.size() != 8 || !token_ok(w[1]) || w[3] != "DICT_5X5_100" ||
|
||||||
|
!parse_u32_any(w[4], &squares_x) || !parse_u32_any(w[5], &squares_y) ||
|
||||||
|
!parse_finite_double(w[6], &square) || !parse_finite_double(w[7], &marker) ||
|
||||||
|
squares_x != 9 || squares_y != 7 || square != 30.0 || marker != 21.0) return false;
|
||||||
|
unsigned char hash[32]; if (!parse_sha256(w[2], hash)) return false; target = true;
|
||||||
|
} else if (tag == "measurement") {
|
||||||
|
if (measurement || w.size() != 13 || !token_ok(w[1])) return false;
|
||||||
|
double v = 0; if (!parse_finite_double(w[2], &v) || v <= 0 || v > .1) return false;
|
||||||
|
for (size_t i = 3; i < w.size(); ++i) if (!parse_finite_double(w[i], &v)) return false;
|
||||||
|
measurement = true;
|
||||||
|
} else if (tag == "white_border") {
|
||||||
|
double v = 0; if (white_border || w.size() != 2 || !parse_finite_double(w[1], &v) || v < 30.0) return false;
|
||||||
|
white_border = true;
|
||||||
|
} else if (tag == "planarity") {
|
||||||
|
unsigned char hash[32];
|
||||||
|
if (planarity || w.size() != 3 || w[1] != "PASS" || !parse_sha256(w[2], hash)) return false;
|
||||||
|
planarity = true;
|
||||||
|
} else if (tag == "decoder") {
|
||||||
|
if (decoder || w.size() != 3 || !token_ok(w[1]) || !token_ok(w[2])) return false;
|
||||||
|
identity->decoder.assign(w[1]); identity->decoder_version.assign(w[2]); decoder = true;
|
||||||
|
} else if (tag == "optical_state") {
|
||||||
|
if (optical_state || w.size() != 3 || !parse_sha256(w[1], identity->optical_state_sha256) ||
|
||||||
|
!token_ok(w[2]) || w[2] == "UNKNOWN") return false;
|
||||||
|
identity->optical_state_token.assign(w[2]); optical_state = true;
|
||||||
|
} else if (tag == "image") {
|
||||||
|
unsigned char hash[32]; uint32_t orientation = 0;
|
||||||
|
if (w.size() != 4 || w[1].empty() || w[1].size() > 4096 || !parse_sha256(w[2], hash) ||
|
||||||
|
!parse_u32_any(w[3], &orientation) ||
|
||||||
|
(orientation != 90 && orientation != 180 && orientation != 270 && w[3] != "0")) return false;
|
||||||
|
image = true;
|
||||||
|
} else if (tag == "pre_solve" || tag == "clipping") {
|
||||||
|
unsigned char hash[32]; double v = 0;
|
||||||
|
if (w.size() != 3 || !parse_sha256(w[1], hash) || !parse_finite_double(w[2], &v)) return false;
|
||||||
|
} else if (tag == "coordinate") {
|
||||||
|
unsigned char hash[32]; uint32_t orientation = 0, width = 0, height = 0, count = 0; double dx = 0, dy = 0;
|
||||||
|
if (w.size() != 10 || !parse_sha256(w[1], hash) || !token_ok(w[2]) || !token_ok(w[3]) ||
|
||||||
|
!parse_u32_any(w[4], &orientation) || !parse_u32_any(w[5], &width) || !parse_u32_any(w[6], &height) ||
|
||||||
|
!parse_u32_any(w[7], &count) || !parse_finite_double(w[8], &dx) || !parse_finite_double(w[9], &dy) ||
|
||||||
|
width == 0 || height == 0 || count < 20 || count > 48 ||
|
||||||
|
(orientation != 90 && orientation != 180 && orientation != 270 && w[4] != "0")) return false;
|
||||||
|
} else if (tag == "coordinate_point") {
|
||||||
|
unsigned char hash[32]; double a = 0, b = 0, c = 0, d = 0;
|
||||||
|
if (w.size() != 7 || !parse_sha256(w[1], hash) || !token_ok(w[2]) ||
|
||||||
|
!parse_finite_double(w[3], &a) || !parse_finite_double(w[4], &b) ||
|
||||||
|
!parse_finite_double(w[5], &c) || !parse_finite_double(w[6], &d)) return false;
|
||||||
|
} else if (tag == "distance") {
|
||||||
|
unsigned char hash[32]; double meters = 0; uint32_t band = 0;
|
||||||
|
if (w.size() != 4 || !parse_sha256(w[1], hash) || !parse_finite_double(w[2], &meters) ||
|
||||||
|
!parse_u32_any(w[3], &band) || meters <= 0 || band > 2) return false;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target && measurement && white_border && planarity && decoder && optical_state && image;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CampaignState {
|
||||||
|
uint64_t execution_id = 0;
|
||||||
|
uint64_t optical_configuration_id = 0;
|
||||||
|
std::string optical_state_token;
|
||||||
|
unsigned char optical_state_sha256[32]{};
|
||||||
|
std::vector<uint64_t> captures;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool parse_campaign_state(std::string_view text, CampaignState *state) {
|
||||||
|
if (!state || text.empty() || text.back() != '\n' || text.find('\0') != std::string_view::npos ||
|
||||||
|
text.find('\r') != std::string_view::npos) return false;
|
||||||
|
std::vector<std::string_view> lines;
|
||||||
|
size_t at = 0;
|
||||||
|
while (at < text.size()) {
|
||||||
|
size_t end = text.find('\n', at); if (end == std::string_view::npos) return false;
|
||||||
|
if (end > at) lines.push_back(text.substr(at, end - at));
|
||||||
|
at = end + 1;
|
||||||
|
}
|
||||||
|
if (lines.size() < 5 || lines[0] != "L3DCAL_CAMPAIGN_STATE_V1") return false;
|
||||||
|
auto execution = words(lines[1]);
|
||||||
|
auto configuration = words(lines[2]);
|
||||||
|
auto optical = words(lines[3]);
|
||||||
|
if (execution.size() != 2 || execution[0] != "execution" || !parse_u64_positive(execution[1], &state->execution_id) ||
|
||||||
|
configuration.size() != 2 || configuration[0] != "optical_configuration" ||
|
||||||
|
!parse_u64_positive(configuration[1], &state->optical_configuration_id) || optical.size() != 3 ||
|
||||||
|
optical[0] != "optical_state" || !parse_sha256(optical[1], state->optical_state_sha256) ||
|
||||||
|
!token_ok(optical[2]) || optical[2] == "UNKNOWN") return false;
|
||||||
|
state->optical_state_token.assign(optical[2]);
|
||||||
|
for (size_t i = 4; i < lines.size(); ++i) {
|
||||||
|
auto capture = words(lines[i]);
|
||||||
|
uint64_t index = 0, capture_id = 0;
|
||||||
|
if (capture.size() != 3 || capture[0] != "capture" || !parse_u64_any(capture[1], &index) ||
|
||||||
|
!parse_u64_positive(capture[2], &capture_id) || index != state->captures.size()) return false;
|
||||||
|
if (state->captures.size() >= LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS) return false;
|
||||||
|
state->captures.push_back(capture_id);
|
||||||
|
}
|
||||||
|
return !state->captures.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
class JsonParser {
|
||||||
|
public:
|
||||||
|
explicit JsonParser(std::string_view text) : text_(text) {}
|
||||||
|
bool parse(std::map<std::string, std::string> *top_scalars) {
|
||||||
|
top_scalars_ = top_scalars;
|
||||||
|
skip_ws();
|
||||||
|
if (!parse_object(0, true)) return false;
|
||||||
|
skip_ws();
|
||||||
|
return at_ == text_.size();
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
bool parse_object(size_t depth, bool top) {
|
||||||
|
if (depth > kJsonDepthMax || !take('{')) return false;
|
||||||
|
skip_ws(); if (take('}')) return true;
|
||||||
|
std::set<std::string> keys;
|
||||||
|
while (true) {
|
||||||
|
std::string key;
|
||||||
|
if (!parse_string(&key, true) || key.size() > kJsonKeyMax || !keys.insert(key).second) return false;
|
||||||
|
skip_ws(); if (!take(':')) return false; skip_ws();
|
||||||
|
if (!parse_value(depth + 1, top ? &key : nullptr)) return false;
|
||||||
|
skip_ws(); if (take('}')) return true; if (!take(',')) return false; skip_ws();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool parse_array(size_t depth) {
|
||||||
|
if (depth > kJsonDepthMax || !take('[')) return false;
|
||||||
|
skip_ws(); if (take(']')) return true;
|
||||||
|
while (true) {
|
||||||
|
if (!parse_value(depth + 1, nullptr)) return false;
|
||||||
|
skip_ws(); if (take(']')) return true; if (!take(',')) return false; skip_ws();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool parse_value(size_t depth, const std::string *top_key) {
|
||||||
|
if (depth > kJsonDepthMax || at_ >= text_.size()) return false;
|
||||||
|
if (text_[at_] == '{') return parse_object(depth, false);
|
||||||
|
if (text_[at_] == '[') return parse_array(depth);
|
||||||
|
if (text_[at_] == '"') {
|
||||||
|
std::string value;
|
||||||
|
if (!parse_string(top_key ? &value : nullptr, top_key != nullptr)) return false;
|
||||||
|
if (top_key) (*top_scalars_)[*top_key] = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
size_t start = at_;
|
||||||
|
if (parse_literal("true") || parse_literal("false") || parse_literal("null") || parse_number()) {
|
||||||
|
if (top_key) (*top_scalars_)[*top_key] = std::string(text_.substr(start, at_ - start));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool parse_string(std::string *out, bool capture) {
|
||||||
|
if (!take('"')) return false;
|
||||||
|
if (capture && out) out->clear();
|
||||||
|
while (at_ < text_.size()) {
|
||||||
|
unsigned char c = static_cast<unsigned char>(text_[at_++]);
|
||||||
|
if (c == '"') return true;
|
||||||
|
if (c < 0x20) return false;
|
||||||
|
if (c == '\\') {
|
||||||
|
if (at_ >= text_.size()) return false;
|
||||||
|
char e = text_[at_++];
|
||||||
|
if (e == 'u') {
|
||||||
|
if (at_ + 4 > text_.size()) return false;
|
||||||
|
for (size_t i = 0; i < 4; ++i) { unsigned v = 0; if (!hex_digit(text_[at_ + i], &v)) return false; }
|
||||||
|
at_ += 4;
|
||||||
|
} else if (std::string_view("\"\\/bfnrt").find(e) == std::string_view::npos) return false;
|
||||||
|
if (capture) return false;
|
||||||
|
} else if (capture && out) {
|
||||||
|
out->push_back(static_cast<char>(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool parse_number() {
|
||||||
|
size_t p = at_;
|
||||||
|
if (p < text_.size() && text_[p] == '-') ++p;
|
||||||
|
if (p >= text_.size()) return false;
|
||||||
|
if (text_[p] == '0') ++p;
|
||||||
|
else {
|
||||||
|
if (text_[p] < '1' || text_[p] > '9') return false;
|
||||||
|
while (p < text_.size() && std::isdigit(static_cast<unsigned char>(text_[p]))) ++p;
|
||||||
|
}
|
||||||
|
if (p < text_.size() && text_[p] == '.') {
|
||||||
|
++p; size_t digits = p; while (p < text_.size() && std::isdigit(static_cast<unsigned char>(text_[p]))) ++p;
|
||||||
|
if (p == digits) return false;
|
||||||
|
}
|
||||||
|
if (p < text_.size() && (text_[p] == 'e' || text_[p] == 'E')) {
|
||||||
|
++p; if (p < text_.size() && (text_[p] == '+' || text_[p] == '-')) ++p;
|
||||||
|
size_t digits = p; while (p < text_.size() && std::isdigit(static_cast<unsigned char>(text_[p]))) ++p;
|
||||||
|
if (p == digits) return false;
|
||||||
|
}
|
||||||
|
at_ = p; return true;
|
||||||
|
}
|
||||||
|
bool parse_literal(std::string_view literal) {
|
||||||
|
if (text_.substr(at_, literal.size()) != literal) return false;
|
||||||
|
at_ += literal.size(); return true;
|
||||||
|
}
|
||||||
|
void skip_ws() { while (at_ < text_.size() && (text_[at_] == ' ' || text_[at_] == '\n' || text_[at_] == '\r' || text_[at_] == '\t')) ++at_; }
|
||||||
|
bool take(char c) { if (at_ >= text_.size() || text_[at_] != c) return false; ++at_; return true; }
|
||||||
|
std::string_view text_;
|
||||||
|
size_t at_ = 0;
|
||||||
|
std::map<std::string, std::string> *top_scalars_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool scalar(const std::map<std::string, std::string>& values, const char *key, std::string *out) {
|
||||||
|
auto it = values.find(key); if (it == values.end()) return false; *out = it->second; return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_json_top(std::string_view text, std::map<std::string, std::string> *top) {
|
||||||
|
if (text.empty() || text.find('\0') != std::string_view::npos) return false;
|
||||||
|
JsonParser parser(text); return parser.parse(top);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool require_format(const std::map<std::string, std::string>& top, const char *expected) {
|
||||||
|
auto it = top.find("format"); return it != top.end() && it->second == expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" Lardon3DCalibrationWorkflowResult lardon3d_calibration_workflow_validate_input_boundary(
|
||||||
|
const Lardon3DCalibrationWorkflowInputFiles *files,
|
||||||
|
Lardon3DCalibrationWorkflowInputBoundary *boundary) {
|
||||||
|
if (!files || !boundary || !files->session_path || !files->detection_path || !files->solve_path ||
|
||||||
|
!files->evidence_path || !files->producer_path || !files->campaign_state_path)
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT;
|
||||||
|
std::memset(boundary, 0, sizeof(*boundary));
|
||||||
|
|
||||||
|
MappedFile session, detection, solve, evidence, producer, campaign;
|
||||||
|
MappedFile *mapped[] = {&session, &detection, &solve, &evidence, &producer, &campaign};
|
||||||
|
const char *paths[] = {files->session_path, files->detection_path, files->solve_path,
|
||||||
|
files->evidence_path, files->producer_path, files->campaign_state_path};
|
||||||
|
for (size_t i = 0; i < 6; ++i) {
|
||||||
|
Lardon3DCalibrationWorkflowResult r = map_regular_file(paths[i], mapped[i]);
|
||||||
|
if (r != LARDON3D_CALIBRATION_WORKFLOW_OK) return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionIdentity session_identity;
|
||||||
|
if (!parse_session(std::string_view(reinterpret_cast<const char *>(session.data), session.size), &session_identity))
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
|
||||||
|
CampaignState campaign_state;
|
||||||
|
if (!parse_campaign_state(std::string_view(reinterpret_cast<const char *>(campaign.data), campaign.size), &campaign_state))
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
|
||||||
|
|
||||||
|
std::map<std::string, std::string> detection_top, solve_top, evidence_top, producer_top;
|
||||||
|
if (!parse_json_top(std::string_view(reinterpret_cast<const char *>(detection.data), detection.size), &detection_top) ||
|
||||||
|
!parse_json_top(std::string_view(reinterpret_cast<const char *>(solve.data), solve.size), &solve_top) ||
|
||||||
|
!parse_json_top(std::string_view(reinterpret_cast<const char *>(evidence.data), evidence.size), &evidence_top) ||
|
||||||
|
!parse_json_top(std::string_view(reinterpret_cast<const char *>(producer.data), producer.size), &producer_top))
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
|
||||||
|
if (!require_format(detection_top, "L3DCAL_DETECTION_V1") || !require_format(solve_top, "L3DCAL_SOLVE_V1") ||
|
||||||
|
!require_format(evidence_top, "L3DCAL_EVIDENCE_BUNDLE_V1") || !require_format(producer_top, "L3DCAL_PRODUCER_V1"))
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
|
||||||
|
|
||||||
|
std::string detection_decoder, detection_version, evidence_optical, evidence_state;
|
||||||
|
std::string producer_executable, producer_configuration, producer_session, producer_optical, producer_threads;
|
||||||
|
if (!scalar(detection_top, "decoder", &detection_decoder) || !scalar(detection_top, "decoder_version", &detection_version) ||
|
||||||
|
!scalar(evidence_top, "optical_sha256", &evidence_optical) || !scalar(evidence_top, "optical_state", &evidence_state) ||
|
||||||
|
!scalar(producer_top, "solver_executable_sha256", &producer_executable) ||
|
||||||
|
!scalar(producer_top, "solver_configuration_sha256", &producer_configuration) ||
|
||||||
|
!scalar(producer_top, "session_sha256", &producer_session) || !scalar(producer_top, "optical_sha256", &producer_optical) ||
|
||||||
|
!scalar(producer_top, "threads", &producer_threads))
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
|
||||||
|
|
||||||
|
unsigned char evidence_optical_sha[32], producer_optical_sha[32], producer_session_sha[32];
|
||||||
|
unsigned char executable_sha[32], configuration_sha[32];
|
||||||
|
if (!parse_sha256(evidence_optical, evidence_optical_sha) || !parse_sha256(producer_optical, producer_optical_sha) ||
|
||||||
|
!parse_sha256(producer_session, producer_session_sha) || !parse_sha256(producer_executable, executable_sha) ||
|
||||||
|
!parse_sha256(producer_configuration, configuration_sha) || producer_threads != "1" ||
|
||||||
|
!nonzero_sha(executable_sha) || !nonzero_sha(configuration_sha))
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
|
||||||
|
|
||||||
|
if (detection_decoder != session_identity.decoder || detection_version != session_identity.decoder_version ||
|
||||||
|
std::memcmp(evidence_optical_sha, session_identity.optical_state_sha256, 32) != 0 ||
|
||||||
|
evidence_state != session_identity.optical_state_token ||
|
||||||
|
std::memcmp(producer_optical_sha, session_identity.optical_state_sha256, 32) != 0 ||
|
||||||
|
std::memcmp(producer_session_sha, session.sha256, 32) != 0 ||
|
||||||
|
std::memcmp(campaign_state.optical_state_sha256, session_identity.optical_state_sha256, 32) != 0 ||
|
||||||
|
campaign_state.optical_state_token != session_identity.optical_state_token)
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
|
||||||
|
|
||||||
|
std::memcpy(boundary->session_sha256, session.sha256, 32);
|
||||||
|
std::memcpy(boundary->detection_sha256, detection.sha256, 32);
|
||||||
|
std::memcpy(boundary->solve_sha256, solve.sha256, 32);
|
||||||
|
std::memcpy(boundary->evidence_sha256, evidence.sha256, 32);
|
||||||
|
std::memcpy(boundary->producer_sha256, producer.sha256, 32);
|
||||||
|
std::memcpy(boundary->campaign_state_sha256, campaign.sha256, 32);
|
||||||
|
std::memcpy(boundary->optical_state_sha256, session_identity.optical_state_sha256, 32);
|
||||||
|
std::memcpy(boundary->solver_executable_sha256, executable_sha, 32);
|
||||||
|
std::memcpy(boundary->solver_configuration_sha256, configuration_sha, 32);
|
||||||
|
boundary->selected_execution_id = campaign_state.execution_id;
|
||||||
|
boundary->optical_configuration_id = campaign_state.optical_configuration_id;
|
||||||
|
boundary->capture_count = static_cast<uint32_t>(campaign_state.captures.size());
|
||||||
|
for (size_t i = 0; i < campaign_state.captures.size(); ++i) boundary->capture_ids[i] = campaign_state.captures[i];
|
||||||
|
std::memcpy(boundary->optical_state_token, session_identity.optical_state_token.data(), session_identity.optical_state_token.size());
|
||||||
|
boundary->optical_state_token[session_identity.optical_state_token.size()] = '\0';
|
||||||
|
return LARDON3D_CALIBRATION_WORKFLOW_OK;
|
||||||
|
}
|
||||||
156
tests/test_calibration_workflow.cpp
Normal file
156
tests/test_calibration_workflow.cpp
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
#include <lardon3d/calibration_workflow.h>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#define CHECK(x) do { if (!(x)) { std::fprintf(stderr, "workflow failure %d: %s\n", __LINE__, #x); return false; } } while (0)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string hex_repeat(char c) { return std::string(64, c); }
|
||||||
|
|
||||||
|
bool write_text(const std::filesystem::path& path, const std::string& text) {
|
||||||
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||||
|
out << text;
|
||||||
|
return static_cast<bool>(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string sha256_file(const std::filesystem::path& path) {
|
||||||
|
std::ifstream in(path, std::ios::binary);
|
||||||
|
std::ostringstream bytes; bytes << in.rdbuf();
|
||||||
|
std::string data = bytes.str();
|
||||||
|
unsigned char digest[32]{}; unsigned int size = 0;
|
||||||
|
if (!in || EVP_Digest(data.data(), data.size(), digest, &size, EVP_sha256(), nullptr) != 1 || size != 32) return {};
|
||||||
|
static const char digits[] = "0123456789abcdef";
|
||||||
|
std::string hex(64, '0');
|
||||||
|
for (size_t i = 0; i < 32; ++i) { hex[2*i] = digits[digest[i] >> 4]; hex[2*i+1] = digits[digest[i] & 15]; }
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Fixture {
|
||||||
|
std::filesystem::path root;
|
||||||
|
std::filesystem::path session, detection, solve, evidence, producer, campaign;
|
||||||
|
Lardon3DCalibrationWorkflowInputFiles files{};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string session_text(const std::string& optical) {
|
||||||
|
const std::string generator = hex_repeat('2'), planarity = hex_repeat('3'), image = hex_repeat('4');
|
||||||
|
std::ostringstream s;
|
||||||
|
s << "L3DCAL_SESSION_V1\n"
|
||||||
|
<< "target board " << generator << " DICT_5X5_100 9 7 30 21\n"
|
||||||
|
<< "measurement caliper 0.1 30 30 30 30 30 30 30 30 30 30\n"
|
||||||
|
<< "white_border 30\n"
|
||||||
|
<< "planarity PASS " << planarity << "\n"
|
||||||
|
<< "decoder qualified_decoder 1\n"
|
||||||
|
<< "optical_state " << optical << " body_objective_zoom_focus_stabilization_format_pipeline\n"
|
||||||
|
<< "image /nonexistent " << image << " 0\n"
|
||||||
|
<< "pre_solve " << image << " 0.1\n"
|
||||||
|
<< "clipping " << image << " 0.0\n"
|
||||||
|
<< "coordinate " << image << " qualified_decoder 1 0 100 100 20 0 0\n";
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
s << "coordinate_point " << image << " center " << i << " 0 " << i << " 0\n";
|
||||||
|
s << "distance " << image << " 0.4 1\n";
|
||||||
|
return s.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool make_valid(Fixture *f) {
|
||||||
|
char temp[] = "/tmp/lardon3d-calibration-workflow-XXXXXX";
|
||||||
|
char *root = mkdtemp(temp); if (!root) return false;
|
||||||
|
f->root = root;
|
||||||
|
f->session = f->root / "session.l3dcal";
|
||||||
|
f->detection = f->root / "detection.json";
|
||||||
|
f->solve = f->root / "solve.json";
|
||||||
|
f->evidence = f->root / "evidence.json";
|
||||||
|
f->producer = f->root / "producer.json";
|
||||||
|
f->campaign = f->root / "campaign.l3dcal";
|
||||||
|
const std::string optical = hex_repeat('1');
|
||||||
|
if (!write_text(f->session, session_text(optical))) return false;
|
||||||
|
const std::string session_sha = sha256_file(f->session); if (session_sha.empty()) return false;
|
||||||
|
if (!write_text(f->detection,
|
||||||
|
"{\n\"format\":\"L3DCAL_DETECTION_V1\",\n\"decoder\":\"qualified_decoder\",\n\"decoder_version\":\"1\",\n\"views\":[]\n}\n")) return false;
|
||||||
|
if (!write_text(f->solve, "{\n\"format\":\"L3DCAL_SOLVE_V1\",\n\"runs\":[],\n\"fit_params\":[]\n}\n")) return false;
|
||||||
|
if (!write_text(f->evidence,
|
||||||
|
"{\n\"format\":\"L3DCAL_EVIDENCE_BUNDLE_V1\",\n\"optical_sha256\":\"" + optical +
|
||||||
|
"\",\n\"optical_state\":\"body_objective_zoom_focus_stabilization_format_pipeline\",\n\"validation_flags\":\"0xf\",\n\"residuals\":[]\n}\n")) return false;
|
||||||
|
if (!write_text(f->producer,
|
||||||
|
"{\n\"format\":\"L3DCAL_PRODUCER_V1\",\n\"solver_executable_sha256\":\"" + hex_repeat('a') +
|
||||||
|
"\",\n\"solver_configuration_sha256\":\"" + hex_repeat('b') +
|
||||||
|
"\",\n\"session_sha256\":\"" + session_sha +
|
||||||
|
"\",\n\"opencv_version\":\"5.0.0\",\n\"opencv_build_sha256\":\"" + hex_repeat('c') +
|
||||||
|
"\",\n\"threads\":1,\n\"rng_seed\":1278432342,\n\"optical_sha256\":\"" + optical + "\"\n}\n")) return false;
|
||||||
|
if (!write_text(f->campaign,
|
||||||
|
"L3DCAL_CAMPAIGN_STATE_V1\nexecution 42\noptical_configuration 7\noptical_state " + optical +
|
||||||
|
" body_objective_zoom_focus_stabilization_format_pipeline\ncapture 0 101\ncapture 1 102\n")) return false;
|
||||||
|
f->files = {f->session.c_str(), f->detection.c_str(), f->solve.c_str(), f->evidence.c_str(), f->producer.c_str(), f->campaign.c_str()};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool valid_and_identity() {
|
||||||
|
Fixture f; CHECK(make_valid(&f));
|
||||||
|
Lardon3DCalibrationWorkflowInputBoundary boundary{};
|
||||||
|
CHECK(lardon3d_calibration_workflow_validate_input_boundary(&f.files, &boundary) == LARDON3D_CALIBRATION_WORKFLOW_OK);
|
||||||
|
CHECK(boundary.selected_execution_id == 42 && boundary.optical_configuration_id == 7 && boundary.capture_count == 2);
|
||||||
|
CHECK(boundary.capture_ids[0] == 101 && boundary.capture_ids[1] == 102);
|
||||||
|
CHECK(std::strcmp(boundary.optical_state_token, "body_objective_zoom_focus_stabilization_format_pipeline") == 0);
|
||||||
|
std::filesystem::remove_all(f.root); return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool malformed_and_provenance() {
|
||||||
|
Fixture f; CHECK(make_valid(&f));
|
||||||
|
Lardon3DCalibrationWorkflowInputBoundary boundary{};
|
||||||
|
CHECK(write_text(f.solve, "{\"format\":\"L3DCAL_SOLVE_V1\",}"));
|
||||||
|
CHECK(lardon3d_calibration_workflow_validate_input_boundary(&f.files, &boundary) == LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE);
|
||||||
|
CHECK(boundary.selected_execution_id == 0);
|
||||||
|
CHECK(write_text(f.solve, "{\"format\":\"L3DCAL_SOLVE_V1\",\"runs\":[]}"));
|
||||||
|
std::string producer; { std::ifstream in(f.producer); std::ostringstream x; x << in.rdbuf(); producer = x.str(); }
|
||||||
|
const std::string actual = sha256_file(f.session); CHECK(!actual.empty());
|
||||||
|
const size_t at = producer.find(actual); CHECK(at != std::string::npos); producer.replace(at, 64, hex_repeat('0'));
|
||||||
|
CHECK(write_text(f.producer, producer));
|
||||||
|
CHECK(lardon3d_calibration_workflow_validate_input_boundary(&f.files, &boundary) == LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH);
|
||||||
|
std::filesystem::remove_all(f.root);
|
||||||
|
CHECK(make_valid(&f));
|
||||||
|
std::string campaign; { std::ifstream in(f.campaign); std::ostringstream x; x << in.rdbuf(); campaign = x.str(); }
|
||||||
|
const size_t opt = campaign.find(hex_repeat('1')); CHECK(opt != std::string::npos); campaign.replace(opt, 64, hex_repeat('9'));
|
||||||
|
CHECK(write_text(f.campaign, campaign));
|
||||||
|
CHECK(lardon3d_calibration_workflow_validate_input_boundary(&f.files, &boundary) == LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH);
|
||||||
|
std::filesystem::remove_all(f.root); return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool file_boundary() {
|
||||||
|
Fixture f; CHECK(make_valid(&f));
|
||||||
|
Lardon3DCalibrationWorkflowInputBoundary boundary{};
|
||||||
|
const std::filesystem::path fifo = f.root / "fifo";
|
||||||
|
CHECK(mkfifo(fifo.c_str(), 0600) == 0);
|
||||||
|
auto saved = f.files.producer_path; f.files.producer_path = fifo.c_str();
|
||||||
|
CHECK(lardon3d_calibration_workflow_validate_input_boundary(&f.files, &boundary) == LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE);
|
||||||
|
f.files.producer_path = saved;
|
||||||
|
const std::filesystem::path link = f.root / "producer-link";
|
||||||
|
CHECK(symlink(f.producer.c_str(), link.c_str()) == 0); saved = f.files.producer_path; f.files.producer_path = link.c_str();
|
||||||
|
CHECK(lardon3d_calibration_workflow_validate_input_boundary(&f.files, &boundary) == LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE);
|
||||||
|
f.files.producer_path = saved;
|
||||||
|
const std::filesystem::path huge = f.root / "huge";
|
||||||
|
int fd = open(huge.c_str(), O_CREAT | O_WRONLY | O_CLOEXEC, 0600); CHECK(fd >= 0);
|
||||||
|
CHECK(ftruncate(fd, static_cast<off_t>(LARDON3D_CALIBRATION_WORKFLOW_MAX_FILE_BYTES) + 1) == 0); CHECK(close(fd) == 0);
|
||||||
|
saved = f.files.producer_path; f.files.producer_path = huge.c_str();
|
||||||
|
CHECK(lardon3d_calibration_workflow_validate_input_boundary(&f.files, &boundary) == LARDON3D_CALIBRATION_WORKFLOW_CAPACITY);
|
||||||
|
f.files.producer_path = saved;
|
||||||
|
std::filesystem::remove_all(f.root); return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
if (!valid_and_identity() || !malformed_and_provenance() || !file_boundary()) return 1;
|
||||||
|
std::puts("CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue