feat: complete acquisition campaign ingestion

This commit is contained in:
fy59 2026-08-27 17:53:40 +02:00
parent 7886b38553
commit 6f2db41010
8 changed files with 1234 additions and 8 deletions

View file

@ -27,6 +27,9 @@ persistante, enrichissable et versionnable.
- **ScanSet / Image Catalog v1** : acquisitions, images logiques, provenance et assets SHA-256 persistants et paginés - **ScanSet / Image Catalog v1** : acquisitions, images logiques, provenance et assets SHA-256 persistants et paginés
- **Capture / Asset Provenance v1** : Captures par ScanSet, associations source/dérivé - **Capture / Asset Provenance v1** : Captures par ScanSet, associations source/dérivé
et sélection explicite d'une image logique — IMPLEMENTED / VALIDATION PENDING et sélection explicite d'une image logique — IMPLEMENTED / VALIDATION PENDING
- **Découverte et planification de campagne bornées** : racines explicites,
plan metadata-only et exécution par groupes via S3-E — IMPLEMENTED /
VALIDATION PENDING
- **Feature Store v1/v2** : ORB U8×32, SIFT/RootSIFT F32×128 et lecture typée bornée - **Feature Store v1/v2** : ORB U8×32, SIFT/RootSIFT F32×128 et lecture typée bornée
- **Image View** : vues triées et filtrées pour la TUI - **Image View** : vues triées et filtrées pour la TUI
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences - **Task** : moteur de tâches avec pause/reprise, annulation et séquences

View file

@ -56,6 +56,64 @@ chaque image v18, relie son asset comme source et rend cette même image
sélectionnée. Aucun nom de fichier, EXIF ou rapprochement de siblings n'est sélectionnée. Aucun nom de fichier, EXIF ou rapprochement de siblings n'est
interprété ; les IDs et la sémantique v18 restent inchangés. interprété ; les IDs et la sémantique v18 restent inchangés.
### Couche finale bornée de découverte et de planification de campagne
**IMPLEMENTED / VALIDATION PENDING.** Cette couche reçoit de 1 à 64 racines
absolues, lexicalement normalisées et explicitement fournies par l'appelant.
Elle ne parcourt pas récursivement : chaque racine et chacune de ses entrées
immédiates régulières est inspectée sans suivre de lien symbolique. Seuls les
fichiers `.arw`, `.jpg` et `.jpeg`, sans sensibilité à la casse, sont des
sources supportées. La découverte, les groupes et les propositions sont chacun
bornés à 4096 éléments et les chemins sont ordonnés de façon déterministe par
`strcmp`.
La planification appelle S3-D uniquement sur les métadonnées : elle ne lit pas
de pixels, n'écrit pas en base et ne matérialise aucun asset ou Capture. Une
association automatique est admise seulement pour une relation forte unique et
mutuelle. Un stem de nom de fichier identique est une proposition, jamais une
identité. Les contradictions, ambiguïtés, comparaisons insuffisantes et cas non
résolus restent des singletons. L'appelant peut confirmer explicitement de 1 à
64 groupes ; chacun est alors étiqueté `CALLER_EXPLICIT`. Une confirmation
réelle explicite reste nécessaire avant tout regroupement.
Le plan et sa progression appartiennent à l'appelant. Ils retiennent les
`capture_id` et `resume_capture_id` retournés afin de reprendre par Capture ;
aucune garantie whole-request exactly-once n'existe avant cette rétention.
L'exécution applique le lot par groupe, avec exactement un appel S3-E par
groupe figé. S3-E demeure l'unique matérialisateur et les représentations
restent explicites. Project DB demeure v19 : cette couche ne détourne ni Task,
ni checkpoint, et n'ajoute aucun sous-système de persistance.
La validation structurelle JPEG accepte un fichier ordinaire jusqu'à son EOI,
puis uniquement du remplissage nul. Si et seulement si l'image primaire porte
un segment APP2 commençant par `MPF\0`, ce remplissage peut être suivi d'une
nouvelle image JPEG structurellement validée par le même parseur ; jusqu'à huit
images au total sont admises, en mémoire constante. Chaque intervalle et la fin
physique restent limités à des octets nuls. Une image ajoutée sans preuve MPF,
un trailer non nul, une image secondaire tronquée ou sans EOI et un neuvième
élément sont corrompus. Les marqueurs ressemblants dans les payloads APP/EXIF
et les données entropy-coded ne deviennent jamais des frontières de conteneur.
Observation autorisée sur le jeu A6000 réel, en dry run : 953 ARW, 953 JPEG,
soit 1906 sources ; métadonnées OK pour les 1906 sources, indisponibles 0 et
autres erreurs 0. Les fichiers JPEG Sony sont des conteneurs MPF valides avec
une image secondaire et un remplissage final nul. Leurs métadonnées ne portent
pas d'`ImageUniqueID` utilisable en commun avec les RAW : fortes 0,
propositions candidates par stem 953, ambiguïtés 0, contradictions 0,
comparaisons insuffisantes 1814512, non résolus 1906 et groupes automatiques
planifiés 1906. L'inversion de l'ordre des racines a réussi la vérification de
déterminisme. Ce dry run n'a effectué ni écriture DB, ni matérialisation, ni
développement RAW ; il ne prouve donc pas un compte physique de 1906
acquisitions et n'affaiblit pas S3-D. Les 953 propositions nécessitent une
confirmation explicite `CALLER_EXPLICIT` avant tout regroupement. Le fixture
d'intégration couvre l'adaptateur, sa matérialisation et son retry ; aucune
mutation de campagne complète n'a été effectuée.
Restent hors S3 : identité persistante de campagne ou de Task, exécution par
scheduler/Governor, traitement de campagne complet, TUI, scrub d'assets et
réconciliation des orphelins, ainsi que vidéo et reconstruction aval lorsqu'ils
sont applicables.
### S3-D — Acquisition Pairing Evidence v1 ### S3-D — Acquisition Pairing Evidence v1
**IMPLEMENTED / VALIDATION PENDING — non FROZEN.** S3-D extrait, depuis des **IMPLEMENTED / VALIDATION PENDING — non FROZEN.** S3-D extrait, depuis des

View file

@ -0,0 +1,130 @@
#ifndef LARDON3D_ACQUISITION_CAMPAIGN_H
#define LARDON3D_ACQUISITION_CAMPAIGN_H
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/acquisition_ingest.h>
#include <lardon3d/acquisition_pairing.h>
#ifdef __cplusplus
extern "C" {
#endif
#define LARDON3D_ACQUISITION_CAMPAIGN_MAX_ROOTS 64u
#define LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES 4096u
#define LARDON3D_ACQUISITION_CAMPAIGN_MAX_GROUPS 4096u
#define LARDON3D_ACQUISITION_CAMPAIGN_MAX_PROPOSALS 4096u
#define LARDON3D_ACQUISITION_CAMPAIGN_PATH_CAPACITY LARDON3D_APP_STATE_PATH_CAPACITY
typedef enum {
LARDON3D_ACQUISITION_CAMPAIGN_OK = 0,
LARDON3D_ACQUISITION_CAMPAIGN_INVALID_ARGUMENT,
LARDON3D_ACQUISITION_CAMPAIGN_IO_ERROR,
LARDON3D_ACQUISITION_CAMPAIGN_DUPLICATE,
LARDON3D_ACQUISITION_CAMPAIGN_PATH_TOO_LONG,
LARDON3D_ACQUISITION_CAMPAIGN_LIMIT_EXCEEDED,
LARDON3D_ACQUISITION_CAMPAIGN_CONSTRAINT,
LARDON3D_ACQUISITION_CAMPAIGN_INGEST_ERROR,
LARDON3D_ACQUISITION_CAMPAIGN_INTERNAL_ERROR
} Lardon3DAcquisitionCampaignResult;
typedef struct {
char path[LARDON3D_ACQUISITION_CAMPAIGN_PATH_CAPACITY];
} Lardon3DAcquisitionCampaignRoot;
typedef struct {
char path[LARDON3D_ACQUISITION_CAMPAIGN_PATH_CAPACITY];
Lardon3DAcquisitionSourceKind source_kind;
Lardon3DAcquisitionResult metadata_result;
Lardon3DAcquisitionMetadata metadata;
} Lardon3DAcquisitionCampaignSource;
typedef struct {
size_t discovered_entry_count;
size_t supported_source_count;
size_t unsupported_entry_count;
size_t metadata_ok_count;
size_t metadata_error_count;
} Lardon3DAcquisitionCampaignDiscoverySummary;
typedef struct {
size_t source_count;
Lardon3DAcquisitionCampaignSource sources[LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES];
Lardon3DAcquisitionCampaignDiscoverySummary summary;
} Lardon3DAcquisitionCampaignDiscovery;
typedef struct {
size_t source_count;
size_t source_indices[LARDON3D_ACQUISITION_INGEST_MAX_SOURCES];
} Lardon3DAcquisitionCampaignConfirmation;
typedef enum {
LARDON3D_ACQUISITION_CAMPAIGN_PROPOSAL_CANDIDATE = 1,
LARDON3D_ACQUISITION_CAMPAIGN_PROPOSAL_AMBIGUOUS = 2
} Lardon3DAcquisitionCampaignProposalKind;
typedef struct {
size_t left_source_index;
size_t right_source_index;
Lardon3DAcquisitionCampaignProposalKind kind;
Lardon3DAcquisitionPairResult pair;
} Lardon3DAcquisitionCampaignProposal;
typedef struct {
uint32_t group_id;
Lardon3DAcquisitionGroupBasis basis;
size_t source_count;
size_t source_indices[LARDON3D_ACQUISITION_INGEST_MAX_SOURCES];
} Lardon3DAcquisitionCampaignGroup;
typedef struct {
size_t source_count;
size_t metadata_ok_count;
size_t metadata_error_count;
size_t strong_group_count;
size_t explicit_group_count;
size_t unresolved_source_count;
size_t ambiguous_pair_count;
size_t contradictory_pair_count;
size_t insufficient_pair_count;
size_t candidate_pair_count;
} Lardon3DAcquisitionCampaignPlanSummary;
typedef struct {
size_t group_count;
Lardon3DAcquisitionCampaignGroup groups[LARDON3D_ACQUISITION_CAMPAIGN_MAX_GROUPS];
size_t proposal_count;
Lardon3DAcquisitionCampaignProposal proposals[LARDON3D_ACQUISITION_CAMPAIGN_MAX_PROPOSALS];
Lardon3DAcquisitionCampaignPlanSummary summary;
} Lardon3DAcquisitionCampaignPlan;
/*
* Roots must be absolute and lexically normalized. Only their immediate regular-file
* entries are considered. Symlinks are never followed. Extensions .arw, .jpg and .jpeg
* are supported case-insensitively; all other immediate entries are counted as unsupported.
*/
Lardon3DAcquisitionCampaignResult lardon3d_acquisition_campaign_discover(
const Lardon3DAcquisitionCampaignRoot *roots, size_t root_count,
Lardon3DAcquisitionCampaignDiscovery *discovery);
/* Pure planning over caller-supplied normalized source/evidence records. */
Lardon3DAcquisitionCampaignResult lardon3d_acquisition_campaign_plan(
const Lardon3DAcquisitionCampaignSource *sources, size_t source_count,
const Lardon3DAcquisitionCampaignConfirmation *confirmations, size_t confirmation_count,
Lardon3DAcquisitionCampaignPlan *plan);
/* Materializes exactly one stable plan group. Retain capture_id before retrying with it. */
Lardon3DAcquisitionCampaignResult lardon3d_acquisition_campaign_materialize_group(
Lardon3DAppState *state, uint64_t scanset_id,
const Lardon3DAcquisitionCampaignSource *sources, size_t source_count,
const Lardon3DAcquisitionCampaignPlan *plan, uint32_t group_id,
const Lardon3DAcquisitionIngestOptions *options,
Lardon3DAcquisitionIngestOutput *output,
Lardon3DAcquisitionIngestResult *ingest_result);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -151,6 +151,7 @@ executable(
'src/raw_development.cpp', 'src/raw_development.cpp',
'src/acquisition_pairing.cpp', 'src/acquisition_pairing.cpp',
'src/acquisition_ingest.cpp', 'src/acquisition_ingest.cpp',
'src/acquisition_campaign.cpp',
'src/feature_extractor_opencv.cpp', 'src/feature_extractor_opencv.cpp',
'src/feature_store.c', 'src/feature_store.c',
'src/feature_extractor_opencv.cpp', 'src/feature_extractor_opencv.cpp',
@ -355,6 +356,27 @@ acquisition_ingest_test = executable(
test('acquisition-ingest', acquisition_ingest_test, timeout: 30) test('acquisition-ingest', acquisition_ingest_test, timeout: 30)
acquisition_campaign_test = executable(
'test-acquisition-campaign',
sources: [
'tests/test_acquisition_campaign.cpp',
'src/app_state.c',
'src/image_catalog_persistent.c',
'src/acquisition_campaign.cpp',
'src/acquisition_ingest.cpp',
'src/acquisition_pairing.cpp',
'src/raw_development.cpp',
'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/task.c',
'src/resource_governor.c',
'src/resource_snapshot.c',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv, libraw, libexif, libpng, libdeflate],
)
test('acquisition-campaign', acquisition_campaign_test, timeout: 30)
feature_store_test = executable( feature_store_test = executable(
'test-feature-store', 'test-feature-store',
sources: [ sources: [

View file

@ -0,0 +1,299 @@
#include <lardon3d/acquisition_campaign.h>
#include <algorithm>
#include <cerrno>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <dirent.h>
#include <fcntl.h>
#include <new>
#include <memory>
#include <sys/stat.h>
#include <unistd.h>
namespace {
bool bounded(const char *text, size_t capacity) {
return text != nullptr && text[0] != '\0' && std::memchr(text, '\0', capacity) != nullptr;
}
bool normalized_absolute(const char *path) {
if (!bounded(path, LARDON3D_ACQUISITION_CAMPAIGN_PATH_CAPACITY) || path[0] != '/') return false;
const size_t length = std::strlen(path);
if (length > 1u && path[length - 1u] == '/') return false;
for (const char *part = path + 1; *part != '\0';) {
const char *slash = std::strchr(part, '/');
const size_t size = slash == nullptr ? std::strlen(part) : static_cast<size_t>(slash - part);
if (size == 0u || (size == 1u && part[0] == '.') ||
(size == 2u && part[0] == '.' && part[1] == '.')) return false;
if (slash == nullptr) break;
part = slash + 1;
}
return true;
}
Lardon3DAcquisitionSourceKind extension_kind(const char *name) {
const char *dot = std::strrchr(name, '.');
if (dot == nullptr) return LARDON3D_ACQUISITION_SOURCE_UNSUPPORTED;
char extension[6]{};
const size_t length = std::strlen(dot);
if (length >= sizeof(extension)) return LARDON3D_ACQUISITION_SOURCE_UNSUPPORTED;
for (size_t i = 0; i <= length; ++i)
extension[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(dot[i])));
if (std::strcmp(extension, ".arw") == 0) return LARDON3D_ACQUISITION_SOURCE_RAW;
if (std::strcmp(extension, ".jpg") == 0 || std::strcmp(extension, ".jpeg") == 0)
return LARDON3D_ACQUISITION_SOURCE_JPEG;
return LARDON3D_ACQUISITION_SOURCE_UNSUPPORTED;
}
bool same_stem(const char *left, const char *right) {
const char *left_name = std::strrchr(left, '/'); left_name = left_name ? left_name + 1 : left;
const char *right_name = std::strrchr(right, '/'); right_name = right_name ? right_name + 1 : right;
const char *left_dot = std::strrchr(left_name, '.');
const char *right_dot = std::strrchr(right_name, '.');
const size_t left_size = left_dot ? static_cast<size_t>(left_dot - left_name) : std::strlen(left_name);
const size_t right_size = right_dot ? static_cast<size_t>(right_dot - right_name) : std::strlen(right_name);
return left_size == right_size && std::memcmp(left_name, right_name, left_size) == 0;
}
bool add_proposal(Lardon3DAcquisitionCampaignPlan &plan, size_t left, size_t right,
Lardon3DAcquisitionCampaignProposalKind kind,
const Lardon3DAcquisitionPairResult &pair) {
if (plan.proposal_count >= LARDON3D_ACQUISITION_CAMPAIGN_MAX_PROPOSALS) return false;
auto &proposal = plan.proposals[plan.proposal_count++];
proposal = {left, right, kind, pair};
return true;
}
} // namespace
extern "C" Lardon3DAcquisitionCampaignResult lardon3d_acquisition_campaign_discover(
const Lardon3DAcquisitionCampaignRoot *roots, size_t root_count,
Lardon3DAcquisitionCampaignDiscovery *discovery) {
if (discovery != nullptr) std::memset(discovery, 0, sizeof(*discovery));
if (roots == nullptr || root_count == 0u ||
root_count > LARDON3D_ACQUISITION_CAMPAIGN_MAX_ROOTS || discovery == nullptr)
return LARDON3D_ACQUISITION_CAMPAIGN_INVALID_ARGUMENT;
try {
std::unique_ptr<Lardon3DAcquisitionCampaignDiscovery> storage(
new Lardon3DAcquisitionCampaignDiscovery{});
auto &result = *storage;
for (size_t i = 0; i < root_count; ++i) {
if (!normalized_absolute(roots[i].path)) return LARDON3D_ACQUISITION_CAMPAIGN_INVALID_ARGUMENT;
for (size_t j = 0; j < i; ++j)
if (std::strcmp(roots[i].path, roots[j].path) == 0)
return LARDON3D_ACQUISITION_CAMPAIGN_DUPLICATE;
int root_fd = open(roots[i].path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
if (root_fd < 0) return LARDON3D_ACQUISITION_CAMPAIGN_IO_ERROR;
DIR *directory = fdopendir(root_fd);
if (directory == nullptr) { close(root_fd); return LARDON3D_ACQUISITION_CAMPAIGN_IO_ERROR; }
for (;;) {
errno = 0;
dirent *entry = readdir(directory);
if (entry == nullptr) {
const int read_errno = errno;
closedir(directory);
if (read_errno != 0) return LARDON3D_ACQUISITION_CAMPAIGN_IO_ERROR;
break;
}
if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) continue;
++result.summary.discovered_entry_count;
struct stat status{};
if (fstatat(root_fd, entry->d_name, &status, AT_SYMLINK_NOFOLLOW) != 0) {
closedir(directory); return LARDON3D_ACQUISITION_CAMPAIGN_IO_ERROR;
}
if (!S_ISREG(status.st_mode)) { ++result.summary.unsupported_entry_count; continue; }
int leaf_fd = openat(root_fd, entry->d_name,
O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
if (leaf_fd < 0) {
if (errno == ELOOP) { ++result.summary.unsupported_entry_count; continue; }
closedir(directory); return LARDON3D_ACQUISITION_CAMPAIGN_IO_ERROR;
}
if (fstat(leaf_fd, &status) != 0) { close(leaf_fd); closedir(directory); return LARDON3D_ACQUISITION_CAMPAIGN_IO_ERROR; }
const auto kind = extension_kind(entry->d_name);
if (!S_ISREG(status.st_mode) || kind == LARDON3D_ACQUISITION_SOURCE_UNSUPPORTED) {
++result.summary.unsupported_entry_count; close(leaf_fd); continue;
}
if (result.source_count >= LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES) {
close(leaf_fd); closedir(directory); return LARDON3D_ACQUISITION_CAMPAIGN_LIMIT_EXCEEDED;
}
auto &source = result.sources[result.source_count++];
int written = std::snprintf(source.path, sizeof(source.path), "%s/%s", roots[i].path,
entry->d_name);
if (written <= 0 || static_cast<size_t>(written) >= sizeof(source.path)) {
close(leaf_fd); closedir(directory); return LARDON3D_ACQUISITION_CAMPAIGN_PATH_TOO_LONG;
}
source.source_kind = kind;
char descriptor_path[64]{};
written = std::snprintf(descriptor_path, sizeof(descriptor_path), "/proc/self/fd/%d", leaf_fd);
if (written <= 0 || static_cast<size_t>(written) >= sizeof(descriptor_path)) {
close(leaf_fd); closedir(directory); return LARDON3D_ACQUISITION_CAMPAIGN_INTERNAL_ERROR;
}
source.metadata_result = lardon3d_acquisition_extract_metadata(descriptor_path, &source.metadata);
if (source.metadata_result == LARDON3D_ACQUISITION_OK) ++result.summary.metadata_ok_count;
else ++result.summary.metadata_error_count;
++result.summary.supported_source_count;
close(leaf_fd);
}
}
std::sort(result.sources, result.sources + result.source_count,
[](const auto &left, const auto &right) { return std::strcmp(left.path, right.path) < 0; });
for (size_t i = 1; i < result.source_count; ++i)
if (std::strcmp(result.sources[i - 1].path, result.sources[i].path) == 0)
return LARDON3D_ACQUISITION_CAMPAIGN_DUPLICATE;
*discovery = result;
return LARDON3D_ACQUISITION_CAMPAIGN_OK;
} catch (const std::bad_alloc &) { return LARDON3D_ACQUISITION_CAMPAIGN_INTERNAL_ERROR; }
catch (...) { return LARDON3D_ACQUISITION_CAMPAIGN_INTERNAL_ERROR; }
}
extern "C" Lardon3DAcquisitionCampaignResult lardon3d_acquisition_campaign_plan(
const Lardon3DAcquisitionCampaignSource *sources, size_t source_count,
const Lardon3DAcquisitionCampaignConfirmation *confirmations, size_t confirmation_count,
Lardon3DAcquisitionCampaignPlan *plan) {
if (plan != nullptr) std::memset(plan, 0, sizeof(*plan));
if (sources == nullptr || source_count == 0u ||
source_count > LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES || plan == nullptr ||
(confirmation_count != 0u && confirmations == nullptr) || confirmation_count > source_count)
return LARDON3D_ACQUISITION_CAMPAIGN_INVALID_ARGUMENT;
try {
std::unique_ptr<Lardon3DAcquisitionCampaignPlan> storage(new Lardon3DAcquisitionCampaignPlan{});
auto &result = *storage;
result.summary.source_count = source_count;
bool assigned[LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES]{};
for (size_t i = 0; i < source_count; ++i) {
if (!normalized_absolute(sources[i].path)) return LARDON3D_ACQUISITION_CAMPAIGN_INVALID_ARGUMENT;
if (i != 0u && std::strcmp(sources[i - 1].path, sources[i].path) >= 0)
return LARDON3D_ACQUISITION_CAMPAIGN_DUPLICATE;
if (sources[i].metadata_result == LARDON3D_ACQUISITION_OK) ++result.summary.metadata_ok_count;
else ++result.summary.metadata_error_count;
}
for (size_t c = 0; c < confirmation_count; ++c) {
const auto &confirmation = confirmations[c];
if (confirmation.source_count == 0u ||
confirmation.source_count > LARDON3D_ACQUISITION_INGEST_MAX_SOURCES)
return LARDON3D_ACQUISITION_CAMPAIGN_CONSTRAINT;
auto &group = result.groups[result.group_count++];
group.basis = LARDON3D_ACQUISITION_GROUP_EXPLICIT;
group.source_count = confirmation.source_count;
for (size_t m = 0; m < confirmation.source_count; ++m) {
const size_t index = confirmation.source_indices[m];
if (index >= source_count || assigned[index]) return LARDON3D_ACQUISITION_CAMPAIGN_CONSTRAINT;
for (size_t previous = 0; previous < m; ++previous)
if (confirmation.source_indices[previous] == index)
return LARDON3D_ACQUISITION_CAMPAIGN_CONSTRAINT;
group.source_indices[m] = index; assigned[index] = true;
}
std::sort(group.source_indices, group.source_indices + group.source_count);
++result.summary.explicit_group_count;
}
size_t strong_count[LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES]{};
size_t strong_partner[LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES]{};
for (size_t i = 0; i < source_count; ++i) for (size_t j = i + 1; j < source_count; ++j) {
if (sources[i].metadata_result != LARDON3D_ACQUISITION_OK ||
sources[j].metadata_result != LARDON3D_ACQUISITION_OK) continue;
Lardon3DAcquisitionPairResult pair{};
const bool stem = same_stem(sources[i].path, sources[j].path);
if (lardon3d_acquisition_compare(&sources[i].metadata, &sources[j].metadata, 0, stem,
&pair) != LARDON3D_ACQUISITION_OK)
return LARDON3D_ACQUISITION_CAMPAIGN_INTERNAL_ERROR;
if (pair.contradictions != 0u) ++result.summary.contradictory_pair_count;
if (pair.decision == LARDON3D_ACQUISITION_SAME_ACQUISITION_STRONG) {
++strong_count[i]; ++strong_count[j]; strong_partner[i] = j; strong_partner[j] = i;
} else if (pair.decision == LARDON3D_ACQUISITION_SAME_ACQUISITION_CANDIDATE) {
if (stem) {
++result.summary.candidate_pair_count;
if (!add_proposal(result, i, j,
LARDON3D_ACQUISITION_CAMPAIGN_PROPOSAL_CANDIDATE, pair))
return LARDON3D_ACQUISITION_CAMPAIGN_LIMIT_EXCEEDED;
} else {
++result.summary.insufficient_pair_count;
}
} else if (pair.decision == LARDON3D_ACQUISITION_AMBIGUOUS) {
++result.summary.ambiguous_pair_count;
if (!add_proposal(result, i, j, LARDON3D_ACQUISITION_CAMPAIGN_PROPOSAL_AMBIGUOUS, pair))
return LARDON3D_ACQUISITION_CAMPAIGN_LIMIT_EXCEEDED;
} else if (pair.decision == LARDON3D_ACQUISITION_INSUFFICIENT && stem) {
++result.summary.candidate_pair_count;
if (!add_proposal(result, i, j, LARDON3D_ACQUISITION_CAMPAIGN_PROPOSAL_CANDIDATE, pair))
return LARDON3D_ACQUISITION_CAMPAIGN_LIMIT_EXCEEDED;
} else if (pair.decision == LARDON3D_ACQUISITION_INSUFFICIENT) {
++result.summary.insufficient_pair_count;
}
}
for (size_t i = 0; i < source_count; ++i) for (size_t j = i + 1; j < source_count; ++j) {
if (sources[i].metadata_result != LARDON3D_ACQUISITION_OK ||
sources[j].metadata_result != LARDON3D_ACQUISITION_OK) continue;
Lardon3DAcquisitionPairResult pair{};
if (lardon3d_acquisition_compare(&sources[i].metadata, &sources[j].metadata, 0,
same_stem(sources[i].path, sources[j].path), &pair) !=
LARDON3D_ACQUISITION_OK)
return LARDON3D_ACQUISITION_CAMPAIGN_INTERNAL_ERROR;
if (pair.decision != LARDON3D_ACQUISITION_SAME_ACQUISITION_STRONG) continue;
if (strong_count[i] > 1u || strong_count[j] > 1u) {
++result.summary.ambiguous_pair_count;
if (!add_proposal(result, i, j, LARDON3D_ACQUISITION_CAMPAIGN_PROPOSAL_AMBIGUOUS, pair))
return LARDON3D_ACQUISITION_CAMPAIGN_LIMIT_EXCEEDED;
}
if (strong_count[i] == 1u && strong_count[j] == 1u &&
strong_partner[i] == j && strong_partner[j] == i && (assigned[i] || assigned[j]))
return LARDON3D_ACQUISITION_CAMPAIGN_CONSTRAINT;
}
for (size_t i = 0; i < source_count; ++i) {
if (assigned[i] || strong_count[i] != 1u) continue;
const size_t partner = strong_partner[i];
if (partner >= source_count || assigned[partner] || strong_count[partner] != 1u ||
strong_partner[partner] != i) continue;
auto &group = result.groups[result.group_count++];
group.basis = LARDON3D_ACQUISITION_GROUP_STRONG; group.source_count = 2u;
group.source_indices[0] = i; group.source_indices[1] = partner;
assigned[i] = assigned[partner] = true; ++result.summary.strong_group_count;
}
for (size_t i = 0; i < source_count; ++i) if (!assigned[i]) {
auto &group = result.groups[result.group_count++];
group.basis = LARDON3D_ACQUISITION_GROUP_SINGLETON; group.source_count = 1u;
group.source_indices[0] = i; assigned[i] = true; ++result.summary.unresolved_source_count;
}
std::sort(result.groups, result.groups + result.group_count, [](const auto &left, const auto &right) {
return left.source_indices[0] < right.source_indices[0];
});
for (size_t i = 0; i < result.group_count; ++i) result.groups[i].group_id = static_cast<uint32_t>(i + 1u);
*plan = result;
return LARDON3D_ACQUISITION_CAMPAIGN_OK;
} catch (const std::bad_alloc &) { return LARDON3D_ACQUISITION_CAMPAIGN_INTERNAL_ERROR; }
catch (...) { return LARDON3D_ACQUISITION_CAMPAIGN_INTERNAL_ERROR; }
}
extern "C" Lardon3DAcquisitionCampaignResult lardon3d_acquisition_campaign_materialize_group(
Lardon3DAppState *state, uint64_t scanset_id,
const Lardon3DAcquisitionCampaignSource *sources, size_t source_count,
const Lardon3DAcquisitionCampaignPlan *plan, uint32_t group_id,
const Lardon3DAcquisitionIngestOptions *options, Lardon3DAcquisitionIngestOutput *output,
Lardon3DAcquisitionIngestResult *ingest_result) {
if (output != nullptr) std::memset(output, 0, sizeof(*output));
if (ingest_result != nullptr) *ingest_result = LARDON3D_ACQUISITION_INGEST_INVALID_ARGUMENT;
if (state == nullptr || sources == nullptr || source_count == 0u ||
source_count > LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES || plan == nullptr ||
group_id == 0u || group_id > plan->group_count || options == nullptr || output == nullptr ||
ingest_result == nullptr) return LARDON3D_ACQUISITION_CAMPAIGN_INVALID_ARGUMENT;
const auto &group = plan->groups[group_id - 1u];
if (group.group_id != group_id || group.source_count == 0u ||
group.source_count > LARDON3D_ACQUISITION_INGEST_MAX_SOURCES)
return LARDON3D_ACQUISITION_CAMPAIGN_CONSTRAINT;
Lardon3DAcquisitionIngestSource inputs[LARDON3D_ACQUISITION_INGEST_MAX_SOURCES]{};
for (size_t i = 0; i < group.source_count; ++i) {
if (group.source_indices[i] >= source_count) return LARDON3D_ACQUISITION_CAMPAIGN_CONSTRAINT;
inputs[i].source_path = sources[group.source_indices[i]].path;
inputs[i].explicit_group = group.basis == LARDON3D_ACQUISITION_GROUP_EXPLICIT ? 1u : 0u;
}
Lardon3DAcquisitionIngestOptions effective = *options;
effective.grouping = group.basis == LARDON3D_ACQUISITION_GROUP_EXPLICIT
? LARDON3D_ACQUISITION_GROUP_CALLER_EXPLICIT
: LARDON3D_ACQUISITION_GROUP_AUTOMATIC;
*ingest_result = lardon3d_acquisition_ingest(state, scanset_id, inputs, group.source_count,
&effective, output);
return *ingest_result == LARDON3D_ACQUISITION_INGEST_OK
? LARDON3D_ACQUISITION_CAMPAIGN_OK
: LARDON3D_ACQUISITION_CAMPAIGN_INGEST_ERROR;
}

View file

@ -133,6 +133,184 @@ bool extract_unsigned(ExifData *data, ExifTag tag, uint32_t &value) {
return false; return false;
} }
enum class JpegValidationResult {
valid,
corrupt,
io_error,
};
constexpr size_t JPEG_MAX_CONTAINER_IMAGES = 8u;
bool read_byte(FILE *file, unsigned char &value) {
const int byte = std::fgetc(file);
if (byte == EOF) {
return false;
}
value = static_cast<unsigned char>(byte);
return true;
}
bool skip_bytes(FILE *file, uint16_t count) {
unsigned char byte = 0u;
for (uint16_t index = 0u; index < count; ++index) {
if (!read_byte(file, byte)) {
return false;
}
}
return true;
}
bool marker_has_length(unsigned char marker) {
return (marker >= 0xc0u && marker <= 0xc7u) ||
(marker >= 0xc9u && marker <= 0xcfu) ||
(marker >= 0xdau && marker <= 0xdfu) ||
(marker >= 0xe0u && marker <= 0xfeu);
}
bool read_marker(FILE *file, unsigned char &marker) {
unsigned char byte = 0u;
if (!read_byte(file, byte) || byte != 0xffu) {
return false;
}
do {
if (!read_byte(file, marker)) {
return false;
}
} while (marker == 0xffu);
return marker != 0x00u;
}
bool skip_marker_payload(FILE *file, unsigned char marker, bool &has_mpf_app2) {
unsigned char high = 0u;
unsigned char low = 0u;
if (!read_byte(file, high) || !read_byte(file, low)) {
return false;
}
const uint16_t length = static_cast<uint16_t>((static_cast<uint16_t>(high) << 8u) |
static_cast<uint16_t>(low));
if (marker == 0xdau) {
unsigned char component_count = 0u;
if (length < 3u || !read_byte(file, component_count) || component_count == 0u ||
component_count > 4u) {
return false;
}
const uint16_t expected_length =
static_cast<uint16_t>(6u + 2u * static_cast<uint16_t>(component_count));
return length == expected_length &&
skip_bytes(file, static_cast<uint16_t>(length - 3u));
}
if (marker == 0xdcu) {
unsigned char line_count_high = 0u;
unsigned char line_count_low = 0u;
return length == 4u && read_byte(file, line_count_high) &&
read_byte(file, line_count_low) &&
(line_count_high != 0u || line_count_low != 0u);
}
if (marker == 0xe2u && length >= 6u) {
unsigned char identifier[4] = {};
for (unsigned char &byte : identifier) {
if (!read_byte(file, byte)) {
return false;
}
}
if (std::memcmp(identifier, "MPF\0", sizeof(identifier)) == 0) {
has_mpf_app2 = true;
}
return skip_bytes(file, static_cast<uint16_t>(length - 6u));
}
return length >= 2u && skip_bytes(file, static_cast<uint16_t>(length - 2u));
}
JpegValidationResult validate_jpeg_image(FILE *file, bool soi_already_consumed,
bool &has_mpf_app2) {
has_mpf_app2 = false;
if (!soi_already_consumed) {
unsigned char first = 0u;
unsigned char second = 0u;
if (!read_byte(file, first) || !read_byte(file, second) || first != 0xffu ||
second != 0xd8u) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
}
bool in_entropy = false;
for (;;) {
unsigned char marker = 0u;
bool marker_from_entropy = false;
if (in_entropy) {
unsigned char byte = 0u;
do {
if (!read_byte(file, byte)) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
} while (byte != 0xffu);
do {
if (!read_byte(file, marker)) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
} while (marker == 0xffu);
if (marker == 0x00u || (marker >= 0xd0u && marker <= 0xd7u)) {
continue;
}
marker_from_entropy = true;
in_entropy = false;
} else if (!read_marker(file, marker)) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
if (marker == 0xd9u) {
return JpegValidationResult::valid;
}
if (marker == 0xd8u) {
return JpegValidationResult::corrupt;
}
if (marker == 0x01u || (marker >= 0xd0u && marker <= 0xd7u)) {
continue;
}
if (!marker_has_length(marker) ||
!skip_marker_payload(file, marker, has_mpf_app2)) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
if (marker == 0xdau || (marker == 0xdcu && marker_from_entropy)) {
in_entropy = true;
}
}
}
JpegValidationResult validate_jpeg_structure(FILE *file) {
bool primary_has_mpf = false;
for (size_t image_index = 0u; image_index < JPEG_MAX_CONTAINER_IMAGES; ++image_index) {
bool image_has_mpf = false;
const JpegValidationResult image_result =
validate_jpeg_image(file, image_index != 0u, image_has_mpf);
if (image_result != JpegValidationResult::valid) {
return image_result;
}
if (image_index == 0u) {
primary_has_mpf = image_has_mpf;
}
int byte = EOF;
do {
byte = std::fgetc(file);
} while (byte == 0);
if (byte == EOF) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::valid;
}
if (!primary_has_mpf || byte != 0xff || std::fgetc(file) != 0xd8) {
return std::ferror(file) != 0 ? JpegValidationResult::io_error
: JpegValidationResult::corrupt;
}
}
return JpegValidationResult::corrupt;
}
Lardon3DAcquisitionResult extract_jpeg(const char *path, Lardon3DAcquisitionResult extract_jpeg(const char *path,
Lardon3DAcquisitionMetadata &metadata) { Lardon3DAcquisitionMetadata &metadata) {
FILE *file = std::fopen(path, "rb"); FILE *file = std::fopen(path, "rb");
@ -140,14 +318,12 @@ Lardon3DAcquisitionResult extract_jpeg(const char *path,
return errno == ENOENT || errno == EACCES ? LARDON3D_ACQUISITION_IO_ERROR return errno == ENOENT || errno == EACCES ? LARDON3D_ACQUISITION_IO_ERROR
: LARDON3D_ACQUISITION_IO_ERROR; : LARDON3D_ACQUISITION_IO_ERROR;
} }
bool has_end_marker = false; const JpegValidationResult validation = validate_jpeg_structure(file);
if (std::fseek(file, -2L, SEEK_END) == 0) { const bool close_failed = std::fclose(file) != 0;
unsigned char tail[2] = {}; if (validation == JpegValidationResult::io_error || close_failed) {
has_end_marker = std::fread(tail, 1u, sizeof(tail), file) == sizeof(tail) && return LARDON3D_ACQUISITION_IO_ERROR;
tail[0] == 0xffu && tail[1] == 0xd9u;
} }
std::fclose(file); if (validation != JpegValidationResult::valid) {
if (!has_end_marker) {
return LARDON3D_ACQUISITION_CORRUPT_SOURCE; return LARDON3D_ACQUISITION_CORRUPT_SOURCE;
} }

View file

@ -0,0 +1,330 @@
#include <lardon3d/acquisition_campaign.h>
#include <libexif/exif-data.h>
extern "C" {
#include <lardon3d/project_db.h>
}
#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <dirent.h>
#include <memory>
#include <sys/stat.h>
#include <unistd.h>
namespace {
int failures;
#define CHECK(x) do { if (!(x)) { std::fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #x); ++failures; } } while (0)
void source(Lardon3DAcquisitionCampaignSource &s, const char *path, const char *id = nullptr,
const char *serial = nullptr) {
s = {}; std::snprintf(s.path, sizeof(s.path), "%s", path);
s.source_kind = std::strstr(path, ".arw") ? LARDON3D_ACQUISITION_SOURCE_RAW : LARDON3D_ACQUISITION_SOURCE_JPEG;
s.metadata_result = LARDON3D_ACQUISITION_OK;
s.metadata.policy_version = LARDON3D_ACQUISITION_PAIRING_POLICY_VERSION;
s.metadata.source_kind = s.source_kind;
if (id) { s.metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_IMAGE_UNIQUE_ID;
std::snprintf(s.metadata.image_unique_id, sizeof(s.metadata.image_unique_id), "%s", id); }
if (serial) { s.metadata.present_fields |= LARDON3D_ACQUISITION_FIELD_BODY_SERIAL;
std::snprintf(s.metadata.body_serial, sizeof(s.metadata.body_serial), "%s", serial); }
}
bool remove_tree(const char *path) {
struct stat st{}; if (lstat(path, &st) != 0) return errno == ENOENT;
if (!S_ISDIR(st.st_mode)) return unlink(path) == 0;
DIR *dir = opendir(path); if (!dir) return false; bool ok = true;
for (dirent *e = readdir(dir); e; e = readdir(dir)) {
if (!std::strcmp(e->d_name, ".") || !std::strcmp(e->d_name, "..")) continue;
char child[4096]{}; int n = std::snprintf(child, sizeof(child), "%s/%s", path, e->d_name);
if (n <= 0 || static_cast<size_t>(n) >= sizeof(child) || !remove_tree(child)) ok = false;
}
return closedir(dir) == 0 && rmdir(path) == 0 && ok;
}
void ascii(ExifData *d, ExifIfd ifd, ExifTag tag, const char *text) {
ExifEntry *e = exif_entry_new(); e->tag = tag; e->format = EXIF_FORMAT_ASCII;
e->components = std::strlen(text) + 1; e->size = static_cast<unsigned>(e->components);
e->data = static_cast<unsigned char *>(std::malloc(e->size)); std::memcpy(e->data, text, e->size);
exif_content_add_entry(d->ifd[ifd], e); exif_entry_unref(e);
}
bool jpeg(const char *path, const char *id) {
ExifData *d = exif_data_new(); if (!d) return false; exif_data_set_byte_order(d, EXIF_BYTE_ORDER_INTEL);
ascii(d, EXIF_IFD_0, EXIF_TAG_MODEL, "Campaign test camera");
ascii(d, EXIF_IFD_EXIF, EXIF_TAG_IMAGE_UNIQUE_ID, id);
unsigned char *encoded = nullptr; unsigned encoded_size = 0; exif_data_save_data(d, &encoded, &encoded_size);
FILE *f = std::fopen(path, "wb"); unsigned segment = encoded_size + 2;
const unsigned char head[] = {0xff, 0xd8, 0xff, 0xe1, static_cast<unsigned char>(segment >> 8),
static_cast<unsigned char>(segment)};
const unsigned char tail[] = {0xff, 0xd9};
bool ok = f && encoded && segment <= UINT16_MAX && std::fwrite(head, 1, sizeof(head), f) == sizeof(head) &&
std::fwrite(encoded, 1, encoded_size, f) == encoded_size && std::fwrite(tail, 1, 2, f) == 2 && std::fclose(f) == 0;
if (f && !ok) std::fclose(f);
std::free(encoded);
exif_data_unref(d);
return ok;
}
bool empty_file(const char *path) {
FILE *file = std::fopen(path, "wb");
return file != nullptr && std::fclose(file) == 0;
}
struct Fixture {
char root[4096] = "/tmp/lardon3d-campaign-XXXXXX", db_path[4096]{};
Lardon3DProjectDb *db{}; Lardon3DAppState state{}; Lardon3DProjectDbScanSet scanset{};
bool open() {
if (!mkdtemp(root) || std::snprintf(db_path, sizeof(db_path), "%s/project.db", root) <= 0) return false;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
if (lardon3d_project_db_open(db_path, &db, error) != LARDON3D_PROJECT_DB_OK) return false;
state.project_loaded = true; state.project_db = db;
std::snprintf(state.project_path, sizeof(state.project_path), "%s", root);
return lardon3d_project_db_create_scanset(db, "campaign", &scanset) == LARDON3D_PROJECT_DB_OK;
}
bool path(char out[4096], const char *name) { int n = std::snprintf(out, 4096, "%s/%s", root, name); return n > 0 && n < 4096; }
~Fixture() { if (db) lardon3d_project_db_close(db); (void)remove_tree(root); }
};
size_t captures(Fixture &f) { Lardon3DProjectDbCapture c[16]{}; size_t n = 0;
return lardon3d_project_db_list_captures(f.db, f.scanset.scanset_id, 0, c, 16, &n) == LARDON3D_PROJECT_DB_OK ? n : SIZE_MAX; }
Lardon3DAcquisitionIngestOptions options() { Lardon3DAcquisitionIngestOptions o{};
o.grouping = LARDON3D_ACQUISITION_GROUP_AUTOMATIC; o.representation = LARDON3D_ACQUISITION_SELECT_JPEG_SOURCE;
o.select_representation = 1; o.imported_at = 17; o.max_source_bytes = 1024 * 1024; return o; }
void planner_tests() {
Lardon3DAcquisitionCampaignSource v[4]{};
source(v[0], "/a/0.arw", "same", "camera-a"); source(v[1], "/a/1.jpg", "same", "camera-a");
source(v[2], "/a/2.arw", "same", "camera-b"); source(v[3], "/a/3.jpg", "other", "camera-a");
Lardon3DAcquisitionCampaignPlan p{};
CHECK(lardon3d_acquisition_campaign_plan(v, 4, nullptr, 0, &p) == LARDON3D_ACQUISITION_CAMPAIGN_OK);
CHECK(p.summary.strong_group_count == 1 && p.groups[0].source_indices[0] == 0 && p.groups[0].source_indices[1] == 1);
Lardon3DAcquisitionCampaignSource explicit_sources[4]{};
source(explicit_sources[0], "/e/0.jpg"); source(explicit_sources[1], "/e/1.jpg");
source(explicit_sources[2], "/e/2.jpg"); source(explicit_sources[3], "/e/3.jpg");
Lardon3DAcquisitionCampaignConfirmation x[2]{}; x[0].source_count = 3;
x[0].source_indices[0] = 2; x[0].source_indices[1] = 0; x[0].source_indices[2] = 1;
CHECK(lardon3d_acquisition_campaign_plan(explicit_sources, 4, x, 1, &p) == LARDON3D_ACQUISITION_CAMPAIGN_OK);
CHECK(p.groups[0].basis == LARDON3D_ACQUISITION_GROUP_EXPLICIT && p.groups[0].source_count == 3 &&
p.groups[0].source_indices[0] == 0 && p.groups[0].source_indices[2] == 2);
x[0].source_count = 2; x[0].source_indices[0] = 0; x[0].source_indices[1] = 0;
CHECK(lardon3d_acquisition_campaign_plan(explicit_sources, 4, x, 1, &p) == LARDON3D_ACQUISITION_CAMPAIGN_CONSTRAINT);
x[0].source_indices[1] = 4;
CHECK(lardon3d_acquisition_campaign_plan(explicit_sources, 4, x, 1, &p) == LARDON3D_ACQUISITION_CAMPAIGN_CONSTRAINT);
x[0].source_indices[1] = 1; x[1].source_count = 2; x[1].source_indices[0] = 1; x[1].source_indices[1] = 2;
CHECK(lardon3d_acquisition_campaign_plan(explicit_sources, 4, x, 2, &p) == LARDON3D_ACQUISITION_CAMPAIGN_CONSTRAINT);
Lardon3DAcquisitionCampaignSource a[4]{v[0], v[1], v[2], v[3]}, b[4]{v[3], v[1], v[0], v[2]};
auto less = [](const auto &l, const auto &r) { return std::strcmp(l.path, r.path) < 0; };
std::sort(a, a + 4, less); std::sort(b, b + 4, less);
Lardon3DAcquisitionCampaignPlan pa{}, pb{};
CHECK(lardon3d_acquisition_campaign_plan(a, 4, nullptr, 0, &pa) == LARDON3D_ACQUISITION_CAMPAIGN_OK);
CHECK(lardon3d_acquisition_campaign_plan(b, 4, nullptr, 0, &pb) == LARDON3D_ACQUISITION_CAMPAIGN_OK);
CHECK(std::memcmp(&pa, &pb, sizeof(pa)) == 0);
constexpr size_t weak_source_count = 100;
auto weak = std::make_unique<Lardon3DAcquisitionCampaignSource[]>(weak_source_count);
for (size_t i = 0; i < weak_source_count - 2u; ++i) {
char path[64]{};
std::snprintf(path, sizeof(path), "/weak/%03zu.jpg", i);
source(weak[i], path, nullptr, "shared-camera");
}
source(weak[weak_source_count - 2u], "/weak/match.arw", nullptr, "shared-camera");
source(weak[weak_source_count - 1u], "/weak/match.jpg", nullptr, "shared-camera");
std::sort(weak.get(), weak.get() + weak_source_count, less);
auto weak_plan = std::make_unique<Lardon3DAcquisitionCampaignPlan>();
CHECK(lardon3d_acquisition_campaign_plan(weak.get(), weak_source_count, nullptr, 0,
weak_plan.get()) ==
LARDON3D_ACQUISITION_CAMPAIGN_OK);
CHECK(weak_plan->proposal_count == 1u);
CHECK(weak_plan->summary.candidate_pair_count == 1u);
CHECK(weak_plan->summary.insufficient_pair_count == 4949u);
CHECK(weak_plan->proposals[0].kind == LARDON3D_ACQUISITION_CAMPAIGN_PROPOSAL_CANDIDATE);
CHECK(std::strstr(weak[weak_plan->proposals[0].left_source_index].path, "/match.") != nullptr);
CHECK(std::strstr(weak[weak_plan->proposals[0].right_source_index].path, "/match.") != nullptr);
}
void materialization_tests() {
Fixture f; CHECK(f.open()); if (!f.db) return; char paths[3][4096]{};
CHECK(f.path(paths[0], "a.JPG") && f.path(paths[1], "b.JPG") && f.path(paths[2], "c.JPG"));
CHECK(jpeg(paths[0], "a") && jpeg(paths[1], "b") && jpeg(paths[2], "c"));
Lardon3DAcquisitionCampaignSource s[3]{}; source(s[0], paths[0]); source(s[1], paths[1]); source(s[2], paths[2]);
Lardon3DAcquisitionCampaignConfirmation c[2]{}; c[0].source_count = 2; c[0].source_indices[0] = 0; c[0].source_indices[1] = 1;
c[1].source_count = 1; c[1].source_indices[0] = 2; Lardon3DAcquisitionCampaignPlan p{};
CHECK(lardon3d_acquisition_campaign_plan(s, 3, c, 2, &p) == LARDON3D_ACQUISITION_CAMPAIGN_OK);
auto o = options(); Lardon3DAcquisitionIngestOutput out{}; Lardon3DAcquisitionIngestResult ir{};
CHECK(lardon3d_acquisition_campaign_materialize_group(&f.state, f.scanset.scanset_id, s, 3, &p, 1, &o, &out, &ir) == LARDON3D_ACQUISITION_CAMPAIGN_OK);
CHECK(ir == LARDON3D_ACQUISITION_INGEST_OK && out.group_count == 1 && out.groups[0].source_count == 2 && captures(f) == 1);
uint64_t capture_id = out.groups[0].capture_id, image_count = 0, selected = 0; size_t asset_count = 0;
Lardon3DProjectDbCaptureAsset assets[4]{};
CHECK(lardon3d_project_db_list_capture_assets(f.db, capture_id, 0, assets, 4, &asset_count) == LARDON3D_PROJECT_DB_OK && asset_count == 2);
CHECK(assets[0].role == LARDON3D_DB_CAPTURE_ASSET_SOURCE && assets[1].role == LARDON3D_DB_CAPTURE_ASSET_SOURCE);
CHECK(lardon3d_project_db_count_images(f.db, f.scanset.scanset_id, &image_count) == LARDON3D_PROJECT_DB_OK && image_count == 1);
CHECK(lardon3d_project_db_get_selected_capture_image(f.db, capture_id, &selected) == LARDON3D_PROJECT_DB_OK && selected != 0);
o.resume_capture_id = capture_id;
CHECK(lardon3d_acquisition_campaign_materialize_group(&f.state, f.scanset.scanset_id, s, 3, &p, 1, &o, &out, &ir) == LARDON3D_ACQUISITION_CAMPAIGN_OK);
CHECK(out.groups[0].capture_id == capture_id && captures(f) == 1);
asset_count = 0; CHECK(lardon3d_project_db_list_capture_assets(f.db, capture_id, 0, assets, 4, &asset_count) == LARDON3D_PROJECT_DB_OK && asset_count == 2);
CHECK(lardon3d_project_db_count_images(f.db, f.scanset.scanset_id, &image_count) == LARDON3D_PROJECT_DB_OK && image_count == 1);
o.resume_capture_id = 0;
CHECK(lardon3d_acquisition_campaign_materialize_group(&f.state, f.scanset.scanset_id, s, 3, &p, 2, &o, &out, &ir) == LARDON3D_ACQUISITION_CAMPAIGN_OK);
CHECK(out.groups[0].capture_id != capture_id && captures(f) == 2);
}
bool same_discovery(const Lardon3DAcquisitionCampaignDiscovery &a, const Lardon3DAcquisitionCampaignDiscovery &b) {
return a.source_count == b.source_count && !std::memcmp(&a.summary, &b.summary, sizeof(a.summary)) &&
!std::memcmp(a.sources, b.sources, a.source_count * sizeof(a.sources[0]));
}
void discovery_tests() {
char base[] = "/tmp/lardon3d-campaign-discovery-XXXXXX";
CHECK(mkdtemp(base) != nullptr);
char left[4096]{}, right[4096]{}, nested[4096]{}, path[4096]{}, target[4096]{};
CHECK(std::snprintf(left, sizeof(left), "%s/left", base) > 0);
CHECK(std::snprintf(right, sizeof(right), "%s/right", base) > 0);
CHECK(std::snprintf(nested, sizeof(nested), "%s/nested", left) > 0);
CHECK(mkdir(left, 0700) == 0 && mkdir(right, 0700) == 0 && mkdir(nested, 0700) == 0);
CHECK(std::snprintf(path, sizeof(path), "%s/good.jpeg", left) > 0 && jpeg(path, "valid-id"));
CHECK(std::snprintf(path, sizeof(path), "%s/bad.JPG", left) > 0 && empty_file(path));
CHECK(std::snprintf(path, sizeof(path), "%s/raw.ARW", right) > 0 && empty_file(path));
CHECK(std::snprintf(path, sizeof(path), "%s/other.jpg", right) > 0 && empty_file(path));
CHECK(std::snprintf(path, sizeof(path), "%s/ignored.txt", left) > 0 && empty_file(path));
CHECK(std::snprintf(path, sizeof(path), "%s/hidden.jpg", nested) > 0 && jpeg(path, "nested"));
CHECK(std::snprintf(target, sizeof(target), "%s/good.jpeg", left) > 0);
CHECK(std::snprintf(path, sizeof(path), "%s/link.jpg", left) > 0 && symlink(target, path) == 0);
CHECK(std::snprintf(path, sizeof(path), "%s/pipe.jpeg", right) > 0 && mkfifo(path, 0600) == 0);
Lardon3DAcquisitionCampaignRoot roots[2]{};
std::snprintf(roots[0].path, sizeof(roots[0].path), "%s", right);
std::snprintf(roots[1].path, sizeof(roots[1].path), "%s", left);
auto first = std::make_unique<Lardon3DAcquisitionCampaignDiscovery>();
auto second = std::make_unique<Lardon3DAcquisitionCampaignDiscovery>();
CHECK(lardon3d_acquisition_campaign_discover(roots, 2, first.get()) ==
LARDON3D_ACQUISITION_CAMPAIGN_OK);
std::swap(roots[0], roots[1]);
CHECK(lardon3d_acquisition_campaign_discover(roots, 2, second.get()) ==
LARDON3D_ACQUISITION_CAMPAIGN_OK);
CHECK(same_discovery(*first, *second));
CHECK(first->source_count == 4 && first->summary.discovered_entry_count == 8 &&
first->summary.supported_source_count == 4 && first->summary.unsupported_entry_count == 4);
for (size_t i = 1; i < first->source_count; ++i)
CHECK(std::strcmp(first->sources[i - 1].path, first->sources[i].path) < 0);
bool valid_jpeg = false, corrupt_jpeg = false;
for (size_t i = 0; i < first->source_count; ++i) {
if (std::strstr(first->sources[i].path, "/good.jpeg"))
valid_jpeg = first->sources[i].metadata_result == LARDON3D_ACQUISITION_OK;
if (std::strstr(first->sources[i].path, "/bad.JPG"))
corrupt_jpeg = first->sources[i].metadata_result != LARDON3D_ACQUISITION_OK;
CHECK(std::strstr(first->sources[i].path, "/nested/") == nullptr);
CHECK(std::strstr(first->sources[i].path, "/link.jpg") == nullptr);
CHECK(std::strstr(first->sources[i].path, "/pipe.jpeg") == nullptr);
}
CHECK(valid_jpeg && corrupt_jpeg);
auto rejected = std::make_unique<Lardon3DAcquisitionCampaignDiscovery>();
auto reject = [&](const Lardon3DAcquisitionCampaignRoot *r, size_t count,
Lardon3DAcquisitionCampaignResult expected) {
std::memset(rejected.get(), 0xa5, sizeof(*rejected));
CHECK(lardon3d_acquisition_campaign_discover(r, count, rejected.get()) == expected);
auto zero = std::make_unique<Lardon3DAcquisitionCampaignDiscovery>();
CHECK(std::memcmp(rejected.get(), zero.get(), sizeof(*zero)) == 0);
};
Lardon3DAcquisitionCampaignRoot invalid{};
std::snprintf(invalid.path, sizeof(invalid.path), "relative");
reject(&invalid, 1, LARDON3D_ACQUISITION_CAMPAIGN_INVALID_ARGUMENT);
std::snprintf(invalid.path, sizeof(invalid.path), "%s/./left", base);
reject(&invalid, 1, LARDON3D_ACQUISITION_CAMPAIGN_INVALID_ARGUMENT);
std::snprintf(invalid.path, sizeof(invalid.path), "%s/../discovery", base);
reject(&invalid, 1, LARDON3D_ACQUISITION_CAMPAIGN_INVALID_ARGUMENT);
Lardon3DAcquisitionCampaignRoot duplicate[2]{};
std::snprintf(duplicate[0].path, sizeof(duplicate[0].path), "%s", left);
duplicate[1] = duplicate[0];
reject(duplicate, 2, LARDON3D_ACQUISITION_CAMPAIGN_DUPLICATE);
Lardon3DAcquisitionCampaignRoot too_many[LARDON3D_ACQUISITION_CAMPAIGN_MAX_ROOTS + 1]{};
reject(too_many, LARDON3D_ACQUISITION_CAMPAIGN_MAX_ROOTS + 1,
LARDON3D_ACQUISITION_CAMPAIGN_INVALID_ARGUMENT);
char limit_root[4096]{};
CHECK(std::snprintf(limit_root, sizeof(limit_root), "%s/limit", base) > 0 &&
mkdir(limit_root, 0700) == 0);
bool files_ok = true;
for (size_t i = 0; i <= LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES; ++i) {
int written = std::snprintf(path, sizeof(path), "%s/%04zu.jpg", limit_root, i);
if (written <= 0 || static_cast<size_t>(written) >= sizeof(path) || !empty_file(path)) {
files_ok = false;
break;
}
}
CHECK(files_ok);
Lardon3DAcquisitionCampaignRoot limit{};
std::snprintf(limit.path, sizeof(limit.path), "%s", limit_root);
reject(&limit, 1, LARDON3D_ACQUISITION_CAMPAIGN_LIMIT_EXCEEDED);
CHECK(remove_tree(base));
}
int dry_run(const char *a, const char *b) {
Lardon3DAcquisitionCampaignRoot roots[2]{}; std::snprintf(roots[0].path, sizeof(roots[0].path), "%s", a);
std::snprintf(roots[1].path, sizeof(roots[1].path), "%s", b);
auto d1 = std::make_unique<Lardon3DAcquisitionCampaignDiscovery>(); auto d2 = std::make_unique<Lardon3DAcquisitionCampaignDiscovery>();
auto p1 = std::make_unique<Lardon3DAcquisitionCampaignPlan>(); auto p2 = std::make_unique<Lardon3DAcquisitionCampaignPlan>();
auto result = lardon3d_acquisition_campaign_discover(roots, 2, d1.get());
if (result != LARDON3D_ACQUISITION_CAMPAIGN_OK) {
std::fprintf(stderr, "CAMPAIGN_DRY_RUN_STAGE=DISCOVER_1\nCAMPAIGN_RESULT=%d\n", result);
return 2;
}
std::swap(roots[0], roots[1]);
result = lardon3d_acquisition_campaign_discover(roots, 2, d2.get());
if (result != LARDON3D_ACQUISITION_CAMPAIGN_OK) {
std::fprintf(stderr, "CAMPAIGN_DRY_RUN_STAGE=DISCOVER_2\nCAMPAIGN_RESULT=%d\n", result);
return 3;
}
const auto plan_result_1 = lardon3d_acquisition_campaign_plan(
d1->sources, d1->source_count, nullptr, 0, p1.get());
const auto plan_result_2 = lardon3d_acquisition_campaign_plan(
d2->sources, d2->source_count, nullptr, 0, p2.get());
if (plan_result_1 != LARDON3D_ACQUISITION_CAMPAIGN_OK ||
plan_result_2 != LARDON3D_ACQUISITION_CAMPAIGN_OK) {
std::fprintf(stderr,
"CAMPAIGN_DRY_RUN_STAGE=PLAN\nCAMPAIGN_PLAN_RESULT_1=%d\n"
"CAMPAIGN_PLAN_RESULT_2=%d\n",
plan_result_1, plan_result_2);
return 4;
}
size_t raw = 0, jpeg_count = 0, unavailable = 0;
size_t raw_ok = 0, raw_unavailable = 0, raw_error = 0;
size_t jpeg_ok = 0, jpeg_unavailable = 0, jpeg_error = 0;
for (size_t i = 0; i < d1->source_count; ++i) {
const bool is_raw = d1->sources[i].source_kind == LARDON3D_ACQUISITION_SOURCE_RAW;
raw += is_raw;
jpeg_count += !is_raw;
const bool is_ok = d1->sources[i].metadata_result == LARDON3D_ACQUISITION_OK;
const bool is_unavailable =
d1->sources[i].metadata_result == LARDON3D_ACQUISITION_METADATA_UNAVAILABLE;
unavailable += is_unavailable;
(is_raw ? raw_ok : jpeg_ok) += is_ok;
(is_raw ? raw_unavailable : jpeg_unavailable) += is_unavailable;
(is_raw ? raw_error : jpeg_error) += !is_ok && !is_unavailable;
}
bool deterministic = same_discovery(*d1, *d2) && !std::memcmp(p1.get(), p2.get(), sizeof(*p1));
std::printf("CAMPAIGN_RAW_COUNT=%zu\nCAMPAIGN_JPEG_COUNT=%zu\nCAMPAIGN_TOTAL_SOURCE_COUNT=%zu\n", raw, jpeg_count, d1->source_count);
std::printf("METADATA_OK=%zu\nMETADATA_UNAVAILABLE=%zu\nMETADATA_ERROR=%zu\n", d1->summary.metadata_ok_count, unavailable, d1->summary.metadata_error_count - unavailable);
std::printf("RAW_METADATA_OK=%zu\nRAW_METADATA_UNAVAILABLE=%zu\nRAW_METADATA_ERROR=%zu\n",
raw_ok, raw_unavailable, raw_error);
std::printf("JPEG_METADATA_OK=%zu\nJPEG_METADATA_UNAVAILABLE=%zu\nJPEG_METADATA_ERROR=%zu\n",
jpeg_ok, jpeg_unavailable, jpeg_error);
std::printf("STRONG_GROUPS=%zu\nCANDIDATE_PAIRS=%zu\nAMBIGUOUS_PAIRS=%zu\nCONTRADICTORY_PAIRS=%zu\nINSUFFICIENT_PAIRS=%zu\nUNPAIRED_SOURCES=%zu\n",
p1->summary.strong_group_count, p1->summary.candidate_pair_count, p1->summary.ambiguous_pair_count,
p1->summary.contradictory_pair_count, p1->summary.insufficient_pair_count, p1->summary.unresolved_source_count);
/* With no confirmations this is the automatic plan's strong groups plus singletons. */
std::printf("PROPOSED_PHYSICAL_CAPTURE_COUNT=%zu\nCAMPAIGN_PLAN_DETERMINISTIC=%s\n", p1->group_count, deterministic ? "PASS" : "FAIL");
if (!deterministic) std::fprintf(stderr, "CAMPAIGN_DRY_RUN_STAGE=DETERMINISM\nCAMPAIGN_RESULT=FAIL\n");
return deterministic ? 0 : 5;
}
} // namespace
int main(int argc, char **argv) {
if (argc == 4 && !std::strcmp(argv[1], "--dry-run")) return dry_run(argv[2], argv[3]);
if (argc != 1) { std::fprintf(stderr, "usage: %s [--dry-run ROOT1 ROOT2]\n", argv[0]); return 64; }
discovery_tests(); planner_tests(); materialization_tests(); return failures ? 1 : 0;
}

View file

@ -7,6 +7,7 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <string> #include <string>
#include <vector>
#include <unistd.h> #include <unistd.h>
namespace { namespace {
@ -213,6 +214,40 @@ void write_exif_jpeg(const std::string &path) {
exif_data_unref(data); exif_data_unref(data);
} }
void append_bytes(const std::string &path, const unsigned char *bytes, size_t size) {
FILE *file = std::fopen(path.c_str(), "ab");
assert(file != nullptr);
assert(std::fwrite(bytes, 1u, size, file) == size);
assert(std::fclose(file) == 0);
}
void write_bytes(const std::string &path, const unsigned char *bytes, size_t size) {
FILE *file = std::fopen(path.c_str(), "wb");
assert(file != nullptr);
assert(std::fwrite(bytes, 1u, size, file) == size);
assert(std::fclose(file) == 0);
}
void assert_jpeg_corrupt(const unsigned char *bytes, size_t size) {
const std::string path = temporary_path(".jpg");
write_bytes(path, bytes, size);
Lardon3DAcquisitionMetadata value = {};
assert(lardon3d_acquisition_extract_metadata(path.c_str(), &value) ==
LARDON3D_ACQUISITION_CORRUPT_SOURCE);
assert(unlink(path.c_str()) == 0);
}
void assert_jpeg_structurally_accepted(const unsigned char *bytes, size_t size) {
const std::string path = temporary_path(".jpg");
write_bytes(path, bytes, size);
Lardon3DAcquisitionMetadata value = {};
const Lardon3DAcquisitionResult result =
lardon3d_acquisition_extract_metadata(path.c_str(), &value);
assert(result == LARDON3D_ACQUISITION_OK ||
result == LARDON3D_ACQUISITION_METADATA_UNAVAILABLE);
assert(unlink(path.c_str()) == 0);
}
void test_extraction() { void test_extraction() {
const std::string jpeg = temporary_path(".jpg"); const std::string jpeg = temporary_path(".jpg");
write_exif_jpeg(jpeg); write_exif_jpeg(jpeg);
@ -229,6 +264,159 @@ void test_extraction() {
assert(std::strcmp(value.image_unique_id, "IMAGE-99") == 0); assert(std::strcmp(value.image_unique_id, "IMAGE-99") == 0);
assert(unlink(jpeg.c_str()) == 0); assert(unlink(jpeg.c_str()) == 0);
const std::string padded = temporary_path(".jpg");
write_exif_jpeg(padded);
const unsigned char zero_padding[37] = {};
append_bytes(padded, zero_padding, sizeof(zero_padding));
assert(lardon3d_acquisition_extract_metadata(padded.c_str(), &value) ==
LARDON3D_ACQUISITION_OK);
assert(value.source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG);
assert(std::strcmp(value.make, "Sony") == 0);
assert(std::strcmp(value.model, "ILCE-7M4") == 0);
assert(std::strcmp(value.datetime_original, "2026:08:27 12:34:56") == 0);
assert(std::strcmp(value.subsec_original, "123") == 0);
assert(std::strcmp(value.offset_original, "+02:00") == 0);
assert(std::strcmp(value.body_serial, "BODY-42") == 0);
assert(std::strcmp(value.image_unique_id, "IMAGE-99") == 0);
assert(unlink(padded.c_str()) == 0);
const unsigned char missing_eoi[] = {0xffu, 0xd8u, 0xffu, 0xe0u,
0x00u, 0x02u};
assert_jpeg_corrupt(missing_eoi, sizeof(missing_eoi));
const unsigned char truncated_segment[] = {0xffu, 0xd8u, 0xffu, 0xe1u,
0x00u, 0x08u, 0x11u};
assert_jpeg_corrupt(truncated_segment, sizeof(truncated_segment));
const unsigned char payload_eoi[] = {0xffu, 0xd8u, 0xffu, 0xe2u, 0x00u,
0x06u, 0xffu, 0xd9u, 0x11u, 0x22u};
assert_jpeg_corrupt(payload_eoi, sizeof(payload_eoi));
const std::string trailing_garbage = temporary_path(".jpg");
write_exif_jpeg(trailing_garbage);
const unsigned char nonzero_padding[] = {0x00u, 0x01u};
append_bytes(trailing_garbage, nonzero_padding, sizeof(nonzero_padding));
assert(lardon3d_acquisition_extract_metadata(trailing_garbage.c_str(), &value) ==
LARDON3D_ACQUISITION_CORRUPT_SOURCE);
assert(unlink(trailing_garbage.c_str()) == 0);
const unsigned char entropy_markers[] = {
0xffu, 0xd8u, 0xffu, 0xdau, 0x00u, 0x08u, 0x01u, 0x01u, 0x00u, 0x00u,
0x3fu, 0x00u, 0x12u, 0xffu, 0x00u, 0x34u, 0xffu, 0xd0u, 0x56u, 0xffu,
0xffu, 0xd9u};
const std::string entropy = temporary_path(".jpg");
write_bytes(entropy, entropy_markers, sizeof(entropy_markers));
const Lardon3DAcquisitionResult entropy_result =
lardon3d_acquisition_extract_metadata(entropy.c_str(), &value);
assert(entropy_result == LARDON3D_ACQUISITION_OK ||
entropy_result == LARDON3D_ACQUISITION_METADATA_UNAVAILABLE);
assert(unlink(entropy.c_str()) == 0);
const unsigned char mismatched_sos_length[] = {
0xffu, 0xd8u, 0xffu, 0xdau, 0x00u, 0x06u, 0x01u, 0x01u,
0x00u, 0x00u, 0xffu, 0xd9u};
assert_jpeg_corrupt(mismatched_sos_length, sizeof(mismatched_sos_length));
const unsigned char zero_sos_components[] = {
0xffu, 0xd8u, 0xffu, 0xdau, 0x00u, 0x06u, 0x00u, 0x00u,
0x00u, 0x00u, 0xffu, 0xd9u};
assert_jpeg_corrupt(zero_sos_components, sizeof(zero_sos_components));
const unsigned char too_many_sos_components[] = {
0xffu, 0xd8u, 0xffu, 0xdau, 0x00u, 0x10u, 0x05u, 0x01u, 0x00u,
0x02u, 0x00u, 0x03u, 0x00u, 0x04u, 0x00u, 0x05u, 0x00u, 0x00u,
0x3fu, 0x00u, 0xffu, 0xd9u};
assert_jpeg_corrupt(too_many_sos_components, sizeof(too_many_sos_components));
const unsigned char entropy_dnl[] = {
0xffu, 0xd8u, 0xffu, 0xdau, 0x00u, 0x08u, 0x01u, 0x01u, 0x00u,
0x00u, 0x3fu, 0x00u, 0x12u, 0xffu, 0xdcu, 0x00u, 0x04u, 0x00u,
0x2au, 0x34u, 0xffu, 0x00u, 0x56u, 0xffu, 0xd1u, 0x78u, 0xffu,
0xd9u};
const std::string entropy_dnl_path = temporary_path(".jpg");
write_bytes(entropy_dnl_path, entropy_dnl, sizeof(entropy_dnl));
const Lardon3DAcquisitionResult entropy_dnl_result =
lardon3d_acquisition_extract_metadata(entropy_dnl_path.c_str(), &value);
assert(entropy_dnl_result == LARDON3D_ACQUISITION_OK ||
entropy_dnl_result == LARDON3D_ACQUISITION_METADATA_UNAVAILABLE);
assert(unlink(entropy_dnl_path.c_str()) == 0);
const unsigned char malformed_dnl_length[] = {
0xffu, 0xd8u, 0xffu, 0xdcu, 0x00u, 0x02u, 0xffu, 0xd9u};
assert_jpeg_corrupt(malformed_dnl_length, sizeof(malformed_dnl_length));
const unsigned char zero_dnl_line_count[] = {
0xffu, 0xd8u, 0xffu, 0xdcu, 0x00u, 0x04u, 0x00u, 0x00u, 0xffu, 0xd9u};
assert_jpeg_corrupt(zero_dnl_line_count, sizeof(zero_dnl_line_count));
const unsigned char marker_level_dnl_does_not_start_entropy[] = {
0xffu, 0xd8u, 0xffu, 0xdcu, 0x00u, 0x04u, 0x00u, 0x2au,
0x12u, 0xffu, 0xd9u};
assert_jpeg_corrupt(marker_level_dnl_does_not_start_entropy,
sizeof(marker_level_dnl_does_not_start_entropy));
const unsigned char mpf_primary[] = {
0xffu, 0xd8u, 0xffu, 0xe2u, 0x00u, 0x06u, 'M', 'P', 'F', 0x00u,
0xffu, 0xd9u};
const unsigned char ordinary_image[] = {0xffu, 0xd8u, 0xffu, 0xd9u};
std::vector<unsigned char> mpf_two_images(std::begin(mpf_primary), std::end(mpf_primary));
mpf_two_images.insert(mpf_two_images.end(), 9u, 0u);
mpf_two_images.insert(mpf_two_images.end(), std::begin(ordinary_image),
std::end(ordinary_image));
mpf_two_images.insert(mpf_two_images.end(), 11u, 0u);
assert_jpeg_structurally_accepted(mpf_two_images.data(), mpf_two_images.size());
std::vector<unsigned char> no_mpf_second(std::begin(ordinary_image),
std::end(ordinary_image));
no_mpf_second.insert(no_mpf_second.end(), 3u, 0u);
no_mpf_second.insert(no_mpf_second.end(), std::begin(ordinary_image),
std::end(ordinary_image));
assert_jpeg_corrupt(no_mpf_second.data(), no_mpf_second.size());
std::vector<unsigned char> mpf_garbage(std::begin(mpf_primary), std::end(mpf_primary));
mpf_garbage.insert(mpf_garbage.end(), 5u, 0u);
mpf_garbage.push_back(0x42u);
assert_jpeg_corrupt(mpf_garbage.data(), mpf_garbage.size());
std::vector<unsigned char> mpf_truncated(std::begin(mpf_primary), std::end(mpf_primary));
const unsigned char truncated_second[] = {0xffu, 0xd8u, 0xffu, 0xe0u, 0x00u};
mpf_truncated.insert(mpf_truncated.end(), std::begin(truncated_second),
std::end(truncated_second));
assert_jpeg_corrupt(mpf_truncated.data(), mpf_truncated.size());
std::vector<unsigned char> mpf_missing_second_eoi(std::begin(mpf_primary),
std::end(mpf_primary));
const unsigned char second_without_eoi[] = {0xffu, 0xd8u, 0xffu, 0xe0u, 0x00u, 0x02u};
mpf_missing_second_eoi.insert(mpf_missing_second_eoi.end(),
std::begin(second_without_eoi),
std::end(second_without_eoi));
assert_jpeg_corrupt(mpf_missing_second_eoi.data(), mpf_missing_second_eoi.size());
std::vector<unsigned char> too_many_images(std::begin(mpf_primary),
std::end(mpf_primary));
for (size_t index = 1u; index < 9u; ++index) {
too_many_images.insert(too_many_images.end(), std::begin(ordinary_image),
std::end(ordinary_image));
}
assert_jpeg_corrupt(too_many_images.data(), too_many_images.size());
const unsigned char embedded_boundaries_in_payload[] = {
0xffu, 0xd8u, 0xffu, 0xe1u, 0x00u, 0x06u, 0xffu, 0xd8u, 0xffu, 0xd9u,
0xffu, 0xd9u};
assert_jpeg_structurally_accepted(embedded_boundaries_in_payload,
sizeof(embedded_boundaries_in_payload));
const unsigned char stuffed_boundaries_in_entropy[] = {
0xffu, 0xd8u, 0xffu, 0xdau, 0x00u, 0x08u, 0x01u, 0x01u, 0x00u,
0x00u, 0x3fu, 0x00u, 0xffu, 0x00u, 0xd8u, 0xffu, 0x00u, 0xd9u,
0xffu, 0xd9u};
assert_jpeg_structurally_accepted(stuffed_boundaries_in_entropy,
sizeof(stuffed_boundaries_in_entropy));
std::vector<unsigned char> mpf_nonzero_final = mpf_two_images;
mpf_nonzero_final.push_back(0x01u);
assert_jpeg_corrupt(mpf_nonzero_final.data(), mpf_nonzero_final.size());
const std::string malformed = temporary_path(".jpg"); const std::string malformed = temporary_path(".jpg");
FILE *file = std::fopen(malformed.c_str(), "wb"); FILE *file = std::fopen(malformed.c_str(), "wb");
assert(file != nullptr); assert(file != nullptr);
@ -279,7 +467,27 @@ void test_non_terminated_raw_fixed_text_mapping() {
} // namespace } // namespace
int main() { int main(int argc, char **argv) {
if (argc == 3 && std::strcmp(argv[1], "--extract") == 0) {
Lardon3DAcquisitionMetadata value = {};
const Lardon3DAcquisitionResult result =
lardon3d_acquisition_extract_metadata(argv[2], &value);
std::printf("METADATA_RESULT=%d\n", static_cast<int>(result));
if (result == LARDON3D_ACQUISITION_OK) {
std::printf("MAKE=%s\nMODEL=%s\nDATETIME_ORIGINAL=%s\nSUBSEC_ORIGINAL=%s\n"
"BODY_SERIAL=%s\nIMAGE_UNIQUE_ID=%s\nWIDTH=%u\nHEIGHT=%u\n"
"ORIENTATION=%u\nPRESENT_FIELDS=%u\n",
value.make, value.model, value.datetime_original,
value.subsec_original, value.body_serial, value.image_unique_id,
value.width, value.height, static_cast<unsigned int>(value.orientation),
value.present_fields);
}
return result == LARDON3D_ACQUISITION_OK ? 0 : 1;
}
if (argc != 1) {
std::fprintf(stderr, "usage: %s [--extract PATH]\n", argv[0]);
return 64;
}
test_pairing_policy(); test_pairing_policy();
test_selector(); test_selector();
test_non_terminated_public_text_is_unavailable(); test_non_terminated_public_text_is_unavailable();