feat: complete acquisition campaign ingestion
This commit is contained in:
parent
7886b38553
commit
6f2db41010
8 changed files with 1234 additions and 8 deletions
|
|
@ -27,6 +27,9 @@ persistante, enrichissable et versionnable.
|
|||
- **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é
|
||||
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
|
||||
- **Image View** : vues triées et filtrées pour la TUI
|
||||
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
|
||||
**IMPLEMENTED / VALIDATION PENDING — non FROZEN.** S3-D extrait, depuis des
|
||||
|
|
|
|||
130
include/lardon3d/acquisition_campaign.h
Normal file
130
include/lardon3d/acquisition_campaign.h
Normal 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
|
||||
22
meson.build
22
meson.build
|
|
@ -151,6 +151,7 @@ executable(
|
|||
'src/raw_development.cpp',
|
||||
'src/acquisition_pairing.cpp',
|
||||
'src/acquisition_ingest.cpp',
|
||||
'src/acquisition_campaign.cpp',
|
||||
'src/feature_extractor_opencv.cpp',
|
||||
'src/feature_store.c',
|
||||
'src/feature_extractor_opencv.cpp',
|
||||
|
|
@ -355,6 +356,27 @@ acquisition_ingest_test = executable(
|
|||
|
||||
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(
|
||||
'test-feature-store',
|
||||
sources: [
|
||||
|
|
|
|||
299
src/acquisition_campaign.cpp
Normal file
299
src/acquisition_campaign.cpp
Normal 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;
|
||||
}
|
||||
|
|
@ -133,6 +133,184 @@ bool extract_unsigned(ExifData *data, ExifTag tag, uint32_t &value) {
|
|||
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,
|
||||
Lardon3DAcquisitionMetadata &metadata) {
|
||||
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
|
||||
: LARDON3D_ACQUISITION_IO_ERROR;
|
||||
}
|
||||
bool has_end_marker = false;
|
||||
if (std::fseek(file, -2L, SEEK_END) == 0) {
|
||||
unsigned char tail[2] = {};
|
||||
has_end_marker = std::fread(tail, 1u, sizeof(tail), file) == sizeof(tail) &&
|
||||
tail[0] == 0xffu && tail[1] == 0xd9u;
|
||||
const JpegValidationResult validation = validate_jpeg_structure(file);
|
||||
const bool close_failed = std::fclose(file) != 0;
|
||||
if (validation == JpegValidationResult::io_error || close_failed) {
|
||||
return LARDON3D_ACQUISITION_IO_ERROR;
|
||||
}
|
||||
std::fclose(file);
|
||||
if (!has_end_marker) {
|
||||
if (validation != JpegValidationResult::valid) {
|
||||
return LARDON3D_ACQUISITION_CORRUPT_SOURCE;
|
||||
}
|
||||
|
||||
|
|
|
|||
330
tests/test_acquisition_campaign.cpp
Normal file
330
tests/test_acquisition_campaign.cpp
Normal 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;
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
|
@ -213,6 +214,40 @@ void write_exif_jpeg(const std::string &path) {
|
|||
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() {
|
||||
const std::string jpeg = temporary_path(".jpg");
|
||||
write_exif_jpeg(jpeg);
|
||||
|
|
@ -229,6 +264,159 @@ void test_extraction() {
|
|||
assert(std::strcmp(value.image_unique_id, "IMAGE-99") == 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");
|
||||
FILE *file = std::fopen(malformed.c_str(), "wb");
|
||||
assert(file != nullptr);
|
||||
|
|
@ -279,7 +467,27 @@ void test_non_terminated_raw_fixed_text_mapping() {
|
|||
|
||||
} // 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_selector();
|
||||
test_non_terminated_public_text_is_unavailable();
|
||||
|
|
|
|||
Loading…
Reference in a new issue