lardon3d/src/acquisition_campaign.cpp

297 lines
16 KiB
C++

#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;
}
void add_proposal(Lardon3DAcquisitionCampaignPlan &plan, size_t left, size_t right,
Lardon3DAcquisitionCampaignProposalKind kind,
const Lardon3DAcquisitionPairResult &pair) {
// Proposals are a bounded review aid, not acquisition identity or a scientific
// dataset-size limit. Retain the deterministic pair-order prefix while the
// complete summary and strong-partner analysis continue across every pair.
if (plan.proposal_count >= LARDON3D_ACQUISITION_CAMPAIGN_MAX_PROPOSALS) return;
auto &proposal = plan.proposals[plan.proposal_count++];
proposal = {left, right, kind, pair};
}
} // 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;
add_proposal(result, i, j,
LARDON3D_ACQUISITION_CAMPAIGN_PROPOSAL_CANDIDATE, pair);
} else {
++result.summary.insufficient_pair_count;
}
} else if (pair.decision == LARDON3D_ACQUISITION_AMBIGUOUS) {
++result.summary.ambiguous_pair_count;
add_proposal(result, i, j, LARDON3D_ACQUISITION_CAMPAIGN_PROPOSAL_AMBIGUOUS, pair);
} else if (pair.decision == LARDON3D_ACQUISITION_INSUFFICIENT && stem) {
++result.summary.candidate_pair_count;
add_proposal(result, i, j, LARDON3D_ACQUISITION_CAMPAIGN_PROPOSAL_CANDIDATE, pair);
} 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;
add_proposal(result, i, j, LARDON3D_ACQUISITION_CAMPAIGN_PROPOSAL_AMBIGUOUS, pair);
}
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;
}