diff --git a/include/lardon3d/image_catalog.h b/include/lardon3d/image_catalog.h index be95416..42883eb 100644 --- a/include/lardon3d/image_catalog.h +++ b/include/lardon3d/image_catalog.h @@ -16,6 +16,16 @@ typedef enum { LARDON3D_IMAGE_CATALOG_DB_ERROR } Lardon3DImageCatalogImportResult; +typedef enum { + LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED = 0, + LARDON3D_IMAGE_CATALOG_ASSET_INVALID_ARGUMENT, + LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_ERROR, + LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_CHANGED, + LARDON3D_IMAGE_CATALOG_ASSET_TOO_LARGE, + LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR, + LARDON3D_IMAGE_CATALOG_ASSET_DB_ERROR +} Lardon3DImageCatalogAssetPublishResult; + bool lardon3d_image_catalog_create_scanset( Lardon3DAppState *state, const char *name, Lardon3DProjectDbScanSet *scanset @@ -25,6 +35,12 @@ Lardon3DImageCatalogImportResult lardon3d_image_catalog_import_file( uint64_t producer_task_id, Lardon3DProjectDbImage *image, Lardon3DProjectDbImageAsset *asset ); +/* Publishes immutable bytes to the managed asset store without creating an image. */ +Lardon3DImageCatalogAssetPublishResult lardon3d_image_catalog_publish_asset_file( + Lardon3DAppState *state, const char *source_path, int64_t created_at, + uint64_t max_source_bytes, + Lardon3DProjectDbImageAsset *asset +); Lardon3DProjectDbResult lardon3d_image_catalog_list( Lardon3DAppState *state, uint64_t scanset_id, uint64_t after_image_id, Lardon3DProjectDbImage *images, Lardon3DProjectDbImageAsset *assets, diff --git a/include/lardon3d/raw_development.h b/include/lardon3d/raw_development.h new file mode 100644 index 0000000..d52351b --- /dev/null +++ b/include/lardon3d/raw_development.h @@ -0,0 +1,57 @@ +#ifndef LARDON3D_RAW_DEVELOPMENT_H +#define LARDON3D_RAW_DEVELOPMENT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +typedef enum { + LARDON3D_RAW_DEVELOPMENT_OK = 0, + LARDON3D_RAW_DEVELOPMENT_INVALID_ARGUMENT, + LARDON3D_RAW_DEVELOPMENT_SOURCE_NOT_FOUND, + LARDON3D_RAW_DEVELOPMENT_SOURCE_CHANGED, + LARDON3D_RAW_DEVELOPMENT_UNSUPPORTED_RAW, + LARDON3D_RAW_DEVELOPMENT_CORRUPT_RAW, + LARDON3D_RAW_DEVELOPMENT_DECODER_FAILURE, + LARDON3D_RAW_DEVELOPMENT_OUT_OF_MEMORY, + LARDON3D_RAW_DEVELOPMENT_OUTPUT_LIMIT_EXCEEDED, + LARDON3D_RAW_DEVELOPMENT_IO_ERROR, + LARDON3D_RAW_DEVELOPMENT_DB_ERROR, + LARDON3D_RAW_DEVELOPMENT_CONSTRAINT, + LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR +} Lardon3DRawDevelopmentResult; + +typedef struct { + Lardon3DProjectDbImageAsset source_asset; + Lardon3DProjectDbImageAsset derived_asset; + Lardon3DProjectDbImage image; + unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE]; + float resolved_camera_wb[4]; + uint32_t width; + uint32_t height; + char libraw_version[64]; + char png_encoder_version[64]; +} Lardon3DRawDevelopmentOutput; + +/* Computes the fixed RAW policy v1 fingerprint for explicitly resolved camera WB. */ +Lardon3DRawDevelopmentResult lardon3d_raw_development_policy_fingerprint( + const float resolved_camera_wb[4], + unsigned char fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE] +); + +/* Publishes a managed ARW source and a derived PNG into an existing Capture. */ +Lardon3DRawDevelopmentResult lardon3d_raw_develop_to_capture( + Lardon3DAppState *state, uint64_t capture_id, const char *source_arw_path, + uint64_t producer_task_id, int64_t created_at, Lardon3DRawDevelopmentOutput *output +); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/meson.build b/meson.build index 8196a9c..0a0aec5 100644 --- a/meson.build +++ b/meson.build @@ -36,6 +36,9 @@ opencv = dependency( include_type: 'system', modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_features2d', 'opencv_geometry'], ) +libraw = dependency('libraw', required: true) +libpng = dependency('libpng', required: true) +libdeflate = dependency('libdeflate', required: true) opencv_benchmark = dependency( 'opencv5', required: true, @@ -144,6 +147,7 @@ executable( 'src/import_task.c', 'src/image_catalog.c', 'src/image_catalog_persistent.c', + 'src/raw_development.cpp', 'src/feature_extractor_opencv.cpp', 'src/feature_store.c', 'src/feature_extractor_opencv.cpp', @@ -193,6 +197,9 @@ executable( threads, sqlite3, openssl, + libraw, + libpng, + libdeflate, opencv, opencv_geometry, opencv_dense_mvs, @@ -294,6 +301,25 @@ persistent_image_catalog_test = executable( test('persistent-image-catalog', persistent_image_catalog_test, timeout: 60) +raw_development_test = executable( + 'test-raw-development', + sources: [ + 'tests/test_raw_development.cpp', + 'src/app_state.c', + 'src/image_catalog_persistent.c', + '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'), + cpp_args: ['-DLARDON3D_RAW_DEVELOPMENT_TESTING'], + dependencies: [threads, sqlite3, openssl, opencv, libraw, libpng, libdeflate], +) + +test('raw-development', raw_development_test, timeout: 30) + feature_store_test = executable( 'test-feature-store', sources: [ diff --git a/src/image_catalog_persistent.c b/src/image_catalog_persistent.c index a1c1b42..ce82d03 100644 --- a/src/image_catalog_persistent.c +++ b/src/image_catalog_persistent.c @@ -14,6 +14,12 @@ enum { COPY_BUFFER_SIZE = 64 * 1024 }; +typedef enum { + COPY_OK = 0, + COPY_SOURCE_ERROR, + COPY_DESTINATION_ERROR +} CopyResult; + static bool join_path(char output[PATH_MAX], const char *parent, const char *child) { @@ -46,34 +52,44 @@ write_all(int descriptor, const unsigned char *data, size_t size) return true; } -static bool +static CopyResult hash_stream(int input, int output, unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE], uint64_t *size) { EVP_MD_CTX *context = EVP_MD_CTX_new(); - if (!context) return false; + if (!context) return COPY_SOURCE_ERROR; bool success = EVP_DigestInit_ex(context, EVP_sha256(), NULL) == 1; + CopyResult result = COPY_OK; unsigned char buffer[COPY_BUFFER_SIZE]; uint64_t total = 0; while (success) { ssize_t count = read(input, buffer, sizeof(buffer)); if (count < 0 && errno == EINTR) continue; - if (count < 0) { success = false; break; } + if (count < 0) { success = false; result = COPY_SOURCE_ERROR; break; } if (count == 0) break; size_t bytes = (size_t)count; if (total > (uint64_t)INT64_MAX - bytes - || EVP_DigestUpdate(context, buffer, bytes) != 1 - || (output >= 0 && !write_all(output, buffer, bytes))) { - success = false; break; + || EVP_DigestUpdate(context, buffer, bytes) != 1) { + success = false; + result = COPY_SOURCE_ERROR; + break; + } + if (output >= 0 && !write_all(output, buffer, bytes)) { + success = false; + result = COPY_DESTINATION_ERROR; + break; } total += bytes; } unsigned int hash_size = 0; if (!success || EVP_DigestFinal_ex(context, hash, &hash_size) != 1 - || hash_size != LARDON3D_PROJECT_DB_SHA256_SIZE) success = false; + || hash_size != LARDON3D_PROJECT_DB_SHA256_SIZE) { + success = false; + if (result == COPY_OK) result = COPY_SOURCE_ERROR; + } EVP_MD_CTX_free(context); if (success) *size = total; - return success; + return success ? COPY_OK : result; } static void @@ -100,7 +116,7 @@ same_published_asset(const char *path, uint64_t size = 0; bool success = fstat(descriptor, &information) == 0 && S_ISREG(information.st_mode) - && hash_stream(descriptor, -1, actual, &size) + && hash_stream(descriptor, -1, actual, &size) == COPY_OK && size == expected_size && memcmp(actual, expected, sizeof(actual)) == 0; (void)close(descriptor); @@ -117,6 +133,114 @@ sync_directory(const char *path) return success; } +static bool +same_source_snapshot(const struct stat *before, const struct stat *after) +{ + return before->st_dev == after->st_dev && before->st_ino == after->st_ino + && before->st_size == after->st_size + && before->st_mtim.tv_sec == after->st_mtim.tv_sec + && before->st_mtim.tv_nsec == after->st_mtim.tv_nsec + && before->st_ctim.tv_sec == after->st_ctim.tv_sec + && before->st_ctim.tv_nsec == after->st_ctim.tv_nsec; +} + +Lardon3DImageCatalogAssetPublishResult +lardon3d_image_catalog_publish_asset_file(Lardon3DAppState *state, + const char *source_path, int64_t created_at, uint64_t max_source_bytes, + Lardon3DProjectDbImageAsset *asset) +{ + if (!state || !state->project_loaded || !state->project_db || !source_path + || !source_path[0] || created_at < 0 || max_source_bytes == 0 || !asset) + return LARDON3D_IMAGE_CATALOG_ASSET_INVALID_ARGUMENT; + memset(asset, 0, sizeof(*asset)); + int input = open(source_path, O_RDONLY | O_NOFOLLOW); + if (input < 0) return LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_ERROR; + struct stat source_before; + if (fstat(input, &source_before) != 0 || !S_ISREG(source_before.st_mode) + || source_before.st_size < 0) { + (void)close(input); + return LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_ERROR; + } + if ((uint64_t)source_before.st_size > max_source_bytes) { + (void)close(input); + return LARDON3D_IMAGE_CATALOG_ASSET_TOO_LARGE; + } + char assets[PATH_MAX], images[PATH_MAX], temporary[PATH_MAX]; + if (!join_path(assets, state->project_path, "assets") + || !ensure_directory(assets) || !sync_directory(state->project_path) + || !join_path(images, assets, "images") || !ensure_directory(images) + || !sync_directory(assets) + || snprintf(temporary, sizeof(temporary), "%s/.asset.tmp.XXXXXX", images) <= 0) { + (void)close(input); + return LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR; + } + int output = mkstemp(temporary); + if (output < 0) { + (void)close(input); + return LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR; + } + unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE]; + uint64_t size = 0; + struct stat source_after; + CopyResult copy = hash_stream(input, output, hash, &size); + bool copied = copy == COPY_OK; + bool source_changed = copied && (fstat(input, &source_after) != 0 + || !same_source_snapshot(&source_before, &source_after)); + bool source_close_ok = close(input) == 0; + bool destination_sync_ok = copied && !source_changed && source_close_ok + && fsync(output) == 0; + int destination_close_result = close(output); + if (copy == COPY_DESTINATION_ERROR || (copied && !source_changed && source_close_ok + && (!destination_sync_ok || destination_close_result != 0))) { + (void)unlink(temporary); + return LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR; + } + if (source_changed) { + (void)unlink(temporary); + return LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_CHANGED; + } + if (copy != COPY_OK || !source_close_ok) { + (void)unlink(temporary); + return LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_ERROR; + } + char hex[65], prefix[3], prefix_path[PATH_MAX], final[PATH_MAX]; + hash_hex(hash, hex); + prefix[0] = hex[0]; prefix[1] = hex[1]; prefix[2] = '\0'; + if (!join_path(prefix_path, images, prefix) || !ensure_directory(prefix_path) + || !sync_directory(images) || !join_path(final, prefix_path, hex)) { + (void)unlink(temporary); + return LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR; + } + bool published = false; + if (link(temporary, final) == 0) { + published = unlink(temporary) == 0 && sync_directory(prefix_path); + } else if (errno == EEXIST && same_published_asset(final, hash, size)) { + published = unlink(temporary) == 0; + } + if (!published) { + (void)unlink(temporary); + return LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR; + } + char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + int relative_size = snprintf(relative, sizeof(relative), "assets/images/%s/%s", prefix, hex); + if (relative_size <= 0 || (size_t)relative_size >= sizeof(relative) + || lardon3d_project_db_register_image_asset(state->project_db, hash, relative, + size, created_at, asset) != LARDON3D_PROJECT_DB_OK) + return LARDON3D_IMAGE_CATALOG_ASSET_DB_ERROR; + return LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED; +} + +Lardon3DImageCatalogAssetPublishResult +lardon3d_image_catalog_test_copy_hash(int input, int output) +{ + unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE]; + uint64_t size = 0; + CopyResult result = hash_stream(input, output, hash, &size); + return result == COPY_OK ? LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED + : result == COPY_DESTINATION_ERROR ? LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR + : LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_ERROR; +} + bool lardon3d_image_catalog_create_scanset(Lardon3DAppState *state, const char *name, Lardon3DProjectDbScanSet *scanset) @@ -144,59 +268,22 @@ lardon3d_image_catalog_import_file(Lardon3DAppState *state, Lardon3DProjectDbScanSet scanset; if (lardon3d_project_db_load_scanset(state->project_db, scanset_id, &scanset) != LARDON3D_PROJECT_DB_OK) return LARDON3D_IMAGE_CATALOG_INVALID_ARGUMENT; - int input = open(source_path, O_RDONLY | O_NOFOLLOW); - if (input < 0) return LARDON3D_IMAGE_CATALOG_SOURCE_ERROR; - struct stat source_information; - if (fstat(input, &source_information) != 0 - || !S_ISREG(source_information.st_mode) || source_information.st_size < 0) { - (void)close(input); return LARDON3D_IMAGE_CATALOG_SOURCE_ERROR; - } - char assets[PATH_MAX], images[PATH_MAX], temporary[PATH_MAX]; - if (!join_path(assets, state->project_path, "assets") - || !ensure_directory(assets) || !sync_directory(state->project_path) - || !join_path(images, assets, "images") || !ensure_directory(images) - || !sync_directory(assets) - || snprintf(temporary, sizeof(temporary), "%s/.asset.tmp.XXXXXX", images) <= 0) { - (void)close(input); return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR; - } - int output = mkstemp(temporary); - if (output < 0) { (void)close(input); return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR; } - unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE]; - uint64_t size = 0; - bool copied = hash_stream(input, output, hash, &size); - if (close(input) != 0) copied = false; - if (copied && fsync(output) != 0) copied = false; - if (close(output) != 0) copied = false; - if (!copied) { - (void)unlink(temporary); return LARDON3D_IMAGE_CATALOG_SOURCE_ERROR; - } - char hex[65], prefix[3], prefix_path[PATH_MAX], final[PATH_MAX]; - hash_hex(hash, hex); prefix[0] = hex[0]; prefix[1] = hex[1]; prefix[2] = '\0'; - if (!join_path(prefix_path, images, prefix) || !ensure_directory(prefix_path) - || !sync_directory(images) - || !join_path(final, prefix_path, hex)) { - (void)unlink(temporary); return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR; - } - bool published = false; - if (link(temporary, final) == 0) { - bool temporary_removed = unlink(temporary) == 0; - bool directory_synced = sync_directory(prefix_path); - published = temporary_removed && directory_synced; - } else if (errno == EEXIST && same_published_asset(final, hash, size)) { - published = unlink(temporary) == 0; - } - if (!published) { - (void)unlink(temporary); return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR; - } - char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY]; - int relative_size = snprintf(relative, sizeof(relative), - "assets/images/%s/%s", prefix, hex); - if (relative_size <= 0 || (size_t)relative_size >= sizeof(relative)) - return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR; - Lardon3DProjectDbImageRegisterStatus status; int64_t imported_at = (int64_t)time(NULL); + if (imported_at < 0) return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR; + Lardon3DImageCatalogAssetPublishResult published = + lardon3d_image_catalog_publish_asset_file(state, source_path, imported_at, + UINT64_MAX, asset); + if (published == LARDON3D_IMAGE_CATALOG_ASSET_INVALID_ARGUMENT) + return LARDON3D_IMAGE_CATALOG_INVALID_ARGUMENT; + if (published == LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_ERROR) + return LARDON3D_IMAGE_CATALOG_SOURCE_ERROR; + if (published == LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR) + return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR; + if (published != LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED) + return LARDON3D_IMAGE_CATALOG_DB_ERROR; + Lardon3DProjectDbImageRegisterStatus status; if (imported_at < 0 || lardon3d_project_db_register_image(state->project_db, - scanset_id, hash, relative, size, basename, source_path, + scanset_id, asset->sha256, asset->path, asset->size_bytes, basename, source_path, producer_task_id, imported_at, &status, image) != LARDON3D_PROJECT_DB_OK) return LARDON3D_IMAGE_CATALOG_DB_ERROR; if (lardon3d_project_db_load_image(state->project_db, image->image_id, diff --git a/src/raw_development.cpp b/src/raw_development.cpp new file mode 100644 index 0000000..09893d8 --- /dev/null +++ b/src/raw_development.cpp @@ -0,0 +1,331 @@ +#include +extern "C" { +#include +} + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { +constexpr uint32_t kMaxDimension = 16384; +constexpr uint64_t kMaxPixels = 40000000; +constexpr uint64_t kMaxSourceBytes = UINT64_C(1024) * 1024U * 1024U; +constexpr int kPngCompression = 6; +constexpr int kPngStrategy = cv::IMWRITE_PNG_STRATEGY_DEFAULT; +constexpr uint32_t kBackendKindLibRaw = 1; +constexpr uint32_t kEncoderKindOpenCvPng = 1; +constexpr size_t kRawFingerprintPayloadSize = 200; + +bool valid_wb(const float wb[4]) { + for (size_t i = 0; i < 4; ++i) if (!std::isfinite(wb[i]) || wb[i] <= 0.0F) return false; + return true; +} +void put_u32(std::vector *bytes, uint32_t value) { + for (unsigned i = 0; i < 4; ++i) bytes->push_back((unsigned char)(value >> (8U * i))); +} +void put_float(std::vector *bytes, float value) { + uint32_t bits = 0; static_assert(sizeof(bits) == sizeof(value), "float width"); + std::memcpy(&bits, &value, sizeof(bits)); put_u32(bytes, bits); +} +bool sha256(const std::vector &bytes, unsigned char out[32]) { + unsigned int size = 0; + return EVP_Digest(bytes.data(), bytes.size(), out, &size, EVP_sha256(), nullptr) == 1 && size == 32; +} +bool checked_rgb_size(uint32_t width, uint32_t height, size_t *bytes) { + if (width == 0 || height == 0 || width > kMaxDimension || height > kMaxDimension) return false; + uint64_t pixels = (uint64_t)width * height; + if (pixels > kMaxPixels || pixels > SIZE_MAX / 3U) return false; + *bytes = (size_t)pixels * 3U; return true; +} +Lardon3DRawDevelopmentResult catalog_result(Lardon3DImageCatalogAssetPublishResult result) { + switch (result) { + case LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED: return LARDON3D_RAW_DEVELOPMENT_OK; + case LARDON3D_IMAGE_CATALOG_ASSET_INVALID_ARGUMENT: return LARDON3D_RAW_DEVELOPMENT_INVALID_ARGUMENT; + case LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_ERROR: return LARDON3D_RAW_DEVELOPMENT_SOURCE_NOT_FOUND; + case LARDON3D_IMAGE_CATALOG_ASSET_SOURCE_CHANGED: return LARDON3D_RAW_DEVELOPMENT_SOURCE_CHANGED; + case LARDON3D_IMAGE_CATALOG_ASSET_TOO_LARGE: return LARDON3D_RAW_DEVELOPMENT_OUTPUT_LIMIT_EXCEEDED; + case LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR: return LARDON3D_RAW_DEVELOPMENT_IO_ERROR; + case LARDON3D_IMAGE_CATALOG_ASSET_DB_ERROR: return LARDON3D_RAW_DEVELOPMENT_DB_ERROR; + } + return LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR; +} +Lardon3DRawDevelopmentResult db_result(Lardon3DProjectDbResult result) { + if (result == LARDON3D_PROJECT_DB_OK) return LARDON3D_RAW_DEVELOPMENT_OK; + if (result == LARDON3D_PROJECT_DB_CONSTRAINT) return LARDON3D_RAW_DEVELOPMENT_CONSTRAINT; + if (result == LARDON3D_PROJECT_DB_NOT_FOUND) return LARDON3D_RAW_DEVELOPMENT_SOURCE_NOT_FOUND; + return LARDON3D_RAW_DEVELOPMENT_DB_ERROR; +} +bool same_scientific_derivation(const Lardon3DProjectDbAssetDerivation &stored, + const Lardon3DProjectDbAssetDerivation &expected) { + return stored.parent_asset_id == expected.parent_asset_id + && stored.child_asset_id == expected.child_asset_id && stored.kind == expected.kind + && stored.version == expected.version + && std::memcmp(stored.parameter_fingerprint, expected.parameter_fingerprint, + sizeof(expected.parameter_fingerprint)) == 0; +} +Lardon3DRawDevelopmentResult ensure_derivation(Lardon3DProjectDb *database, + const Lardon3DProjectDbAssetDerivation &expected) { + Lardon3DProjectDbAssetDerivation stored = {}; + Lardon3DProjectDbResult load = lardon3d_project_db_load_asset_derivation( + database, expected.child_asset_id, &stored); + if (load == LARDON3D_PROJECT_DB_NOT_FOUND) + return db_result(lardon3d_project_db_record_asset_derivation(database, &expected)); + if (load != LARDON3D_PROJECT_DB_OK) return LARDON3D_RAW_DEVELOPMENT_DB_ERROR; + return same_scientific_derivation(stored, expected) ? LARDON3D_RAW_DEVELOPMENT_OK + : LARDON3D_RAW_DEVELOPMENT_CONSTRAINT; +} +Lardon3DRawDevelopmentResult ensure_capture_asset_role( + Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id, + Lardon3DProjectDbCaptureAssetRole expected_role) { + Lardon3DProjectDbResult attach = lardon3d_project_db_attach_capture_asset( + database, capture_id, asset_id, expected_role); + if (attach == LARDON3D_PROJECT_DB_OK) return LARDON3D_RAW_DEVELOPMENT_OK; + if (attach != LARDON3D_PROJECT_DB_CONSTRAINT) return db_result(attach); + + Lardon3DProjectDbCaptureAsset page[LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX]; + uint64_t after_asset_id = 0; + for (;;) { + size_t count = 0; + Lardon3DProjectDbResult listed = lardon3d_project_db_list_capture_assets( + database, capture_id, after_asset_id, page, + LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX, &count); + if (listed != LARDON3D_PROJECT_DB_OK) return LARDON3D_RAW_DEVELOPMENT_DB_ERROR; + for (size_t index = 0; index < count; ++index) { + if (page[index].asset_id == asset_id) + return page[index].role == expected_role ? LARDON3D_RAW_DEVELOPMENT_OK + : LARDON3D_RAW_DEVELOPMENT_CONSTRAINT; + if (page[index].asset_id > asset_id) return LARDON3D_RAW_DEVELOPMENT_CONSTRAINT; + } + if (count < LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX) return LARDON3D_RAW_DEVELOPMENT_CONSTRAINT; + after_asset_id = page[count - 1].asset_id; + } +} +Lardon3DRawDevelopmentResult libraw_result(int code) { + if (code == LIBRAW_FILE_UNSUPPORTED) return LARDON3D_RAW_DEVELOPMENT_UNSUPPORTED_RAW; + if (code == LIBRAW_UNSUFFICIENT_MEMORY) return LARDON3D_RAW_DEVELOPMENT_OUT_OF_MEMORY; + if (code == LIBRAW_DATA_ERROR || code == LIBRAW_IO_ERROR) return LARDON3D_RAW_DEVELOPMENT_CORRUPT_RAW; + return LARDON3D_RAW_DEVELOPMENT_DECODER_FAILURE; +} +bool write_all(int fd, const unsigned char *data, size_t size) { + size_t offset = 0; + while (offset < size) { + ssize_t written = write(fd, data + offset, size - offset); + if (written < 0 && errno == EINTR) continue; + if (written <= 0) return false; + offset += (size_t)written; + } + return true; +} +bool asset_file_path(const Lardon3DAppState *state, const Lardon3DProjectDbImageAsset &asset, + char path[LARDON3D_APP_STATE_PATH_CAPACITY]) { + int count = std::snprintf(path, LARDON3D_APP_STATE_PATH_CAPACITY, "%s/%s", + state->project_path, asset.path); + return count > 0 && count < LARDON3D_APP_STATE_PATH_CAPACITY; +} +bool verify_managed_asset(const char *path, const unsigned char expected[32]) { + int fd = open(path, O_RDONLY | O_NOFOLLOW); + if (fd < 0) return false; + struct stat info; EVP_MD_CTX *context = EVP_MD_CTX_new(); + bool ok = fstat(fd, &info) == 0 && S_ISREG(info.st_mode) && context + && EVP_DigestInit_ex(context, EVP_sha256(), nullptr) == 1; + unsigned char buffer[65536]; + while (ok) { + ssize_t count = read(fd, buffer, sizeof(buffer)); + if (count < 0 && errno == EINTR) continue; + if (count < 0) { ok = false; break; } + if (count == 0) break; + ok = EVP_DigestUpdate(context, buffer, (size_t)count) == 1; + } + unsigned char actual[32]; unsigned int length = 0; + if (ok) ok = EVP_DigestFinal_ex(context, actual, &length) == 1 && length == sizeof(actual) + && std::memcmp(actual, expected, sizeof(actual)) == 0; + EVP_MD_CTX_free(context); (void)close(fd); return ok; +} +bool valid_name(const char *path, char name[LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY]) { + const char *base = std::strrchr(path, '/'); base = base ? base + 1 : path; + size_t size = strnlen(base, LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY); + if (size == 0 || size >= LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY || std::strchr(base, '\\') + || std::strchr(base, '\t') || std::strchr(base, '\r') || std::strchr(base, '\n')) return false; + std::memcpy(name, base, size + 1); return true; +} +} + +extern "C" Lardon3DRawDevelopmentResult lardon3d_raw_development_policy_fingerprint( + const float wb[4], unsigned char fingerprint[32]) { + if (!wb || !fingerprint || !valid_wb(wb)) return LARDON3D_RAW_DEVELOPMENT_INVALID_ARGUMENT; + try { + if (std::strcmp(png_get_libpng_ver(nullptr), PNG_LIBPNG_VER_STRING) != 0) + return LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR; + std::vector bytes; + bytes.reserve(kRawFingerprintPayloadSize); + const char magic[] = "L3DRAWD1"; + bytes.insert(bytes.end(), magic, magic + sizeof(magic) - 1); + put_u32(&bytes, 1); put_u32(&bytes, kBackendKindLibRaw); + put_u32(&bytes, LIBRAW_VERSION); put_u32(&bytes, libraw_versionNumber()); + put_u32(&bytes, 3); put_u32(&bytes, 0); put_u32(&bytes, 0); + for (size_t i = 0; i < 4; ++i) put_float(&bytes, wb[i]); + put_u32(&bytes, 0); put_u32(&bytes, 0); put_u32(&bytes, 0); put_u32(&bytes, 1); + put_float(&bytes, 1.0F / 2.4F); put_float(&bytes, 12.92F); + put_u32(&bytes, 1); put_float(&bytes, 1.0F); put_float(&bytes, 0.0F); + put_u32(&bytes, 0); put_float(&bytes, 1.0F); put_float(&bytes, 0.0F); + put_u32(&bytes, 0); put_float(&bytes, 0.0F); put_u32(&bytes, 0); put_u32(&bytes, 0); + put_u32(&bytes, 0); put_u32(&bytes, 0); put_u32(&bytes, 0); put_u32(&bytes, 0); + put_u32(&bytes, 8); put_u32(&bytes, 3); put_u32(&bytes, kEncoderKindOpenCvPng); + put_u32(&bytes, kPngCompression); + put_u32(&bytes, kPngStrategy); put_u32(&bytes, 0); put_u32(&bytes, kMaxDimension); + put_u32(&bytes, (uint32_t)kMaxPixels); + put_u32(&bytes, (uint32_t)(kMaxSourceBytes >> 20)); put_u32(&bytes, CV_VERSION_MAJOR); + put_u32(&bytes, CV_VERSION_MINOR); put_u32(&bytes, CV_VERSION_REVISION); + put_u32(&bytes, PNG_LIBPNG_VER_MAJOR); put_u32(&bytes, PNG_LIBPNG_VER_MINOR); + put_u32(&bytes, PNG_LIBPNG_VER_RELEASE); + put_u32(&bytes, LIBDEFLATE_VERSION_MAJOR); put_u32(&bytes, LIBDEFLATE_VERSION_MINOR); + if (bytes.size() != kRawFingerprintPayloadSize) + return LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR; + return sha256(bytes, fingerprint) ? LARDON3D_RAW_DEVELOPMENT_OK + : LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR; + } catch (const std::bad_alloc &) { + return LARDON3D_RAW_DEVELOPMENT_OUT_OF_MEMORY; + } catch (...) { + return LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR; + } +} + +#ifdef LARDON3D_RAW_DEVELOPMENT_TESTING +extern "C" size_t lardon3d_raw_development_test_policy_payload_size(void) { + return kRawFingerprintPayloadSize; +} +extern "C" Lardon3DRawDevelopmentResult +lardon3d_raw_development_test_ensure_derivation( + Lardon3DProjectDb *database, const Lardon3DProjectDbAssetDerivation *expected) { + return !database || !expected ? LARDON3D_RAW_DEVELOPMENT_INVALID_ARGUMENT + : ensure_derivation(database, *expected); +} +extern "C" Lardon3DRawDevelopmentResult +lardon3d_raw_development_test_ensure_capture_asset_role( + Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id, + Lardon3DProjectDbCaptureAssetRole expected_role) { + return ensure_capture_asset_role(database, capture_id, asset_id, expected_role); +} +#endif + +extern "C" Lardon3DRawDevelopmentResult lardon3d_raw_develop_to_capture( + Lardon3DAppState *state, uint64_t capture_id, const char *source_arw_path, + uint64_t producer_task_id, int64_t created_at, Lardon3DRawDevelopmentOutput *output) { + if (!state || !state->project_loaded || !state->project_db || capture_id == 0 || !source_arw_path + || !source_arw_path[0] || created_at < 0 || !output) return LARDON3D_RAW_DEVELOPMENT_INVALID_ARGUMENT; + std::memset(output, 0, sizeof(*output)); + try { + Lardon3DProjectDbCapture capture; + Lardon3DRawDevelopmentResult result = db_result(lardon3d_project_db_load_capture( + state->project_db, capture_id, &capture)); + if (result != LARDON3D_RAW_DEVELOPMENT_OK) return result; + result = catalog_result(lardon3d_image_catalog_publish_asset_file( + state, source_arw_path, created_at, kMaxSourceBytes, &output->source_asset)); + if (result != LARDON3D_RAW_DEVELOPMENT_OK) return result; + result = ensure_capture_asset_role(state->project_db, capture_id, + output->source_asset.asset_id, LARDON3D_DB_CAPTURE_ASSET_SOURCE); + if (result != LARDON3D_RAW_DEVELOPMENT_OK) return result; + char managed_source[LARDON3D_APP_STATE_PATH_CAPACITY]; + if (!asset_file_path(state, output->source_asset, managed_source)) return LARDON3D_RAW_DEVELOPMENT_IO_ERROR; + if (!verify_managed_asset(managed_source, output->source_asset.sha256)) return LARDON3D_RAW_DEVELOPMENT_SOURCE_CHANGED; + LibRaw raw; + raw.imgdata.params.user_qual = 3; raw.imgdata.params.half_size = 0; + raw.imgdata.params.user_flip = 0; raw.imgdata.params.use_auto_wb = 0; + raw.imgdata.params.use_camera_wb = 0; raw.imgdata.params.use_camera_matrix = 0; + raw.imgdata.params.output_profile = nullptr; raw.imgdata.params.camera_profile = nullptr; + raw.imgdata.params.output_color = 1; raw.imgdata.params.gamm[0] = 1.0 / 2.4; + raw.imgdata.params.gamm[1] = 12.92; raw.imgdata.params.no_auto_bright = 1; + raw.imgdata.params.bright = 1.0F; raw.imgdata.params.adjust_maximum_thr = 0.0F; + raw.imgdata.params.exp_correc = 0; raw.imgdata.params.exp_shift = 1.0F; + raw.imgdata.params.exp_preser = 0.0F; raw.imgdata.params.highlight = 0; + raw.imgdata.params.threshold = 0.0F; raw.imgdata.params.fbdd_noiserd = 0; + raw.imgdata.params.med_passes = 0; raw.imgdata.params.output_bps = 8; + raw.imgdata.params.output_tiff = 0; raw.imgdata.params.user_black = 0; + raw.imgdata.params.user_sat = 0; raw.imgdata.params.no_auto_scale = 0; + raw.imgdata.params.no_interpolation = 0; + for (size_t i = 0; i < 4; ++i) raw.imgdata.params.aber[i] = 1.0; + int code = raw.open_file(managed_source); + if (code != LIBRAW_SUCCESS) return libraw_result(code); + size_t decoded_size = 0; + if (!checked_rgb_size(raw.imgdata.sizes.iwidth, raw.imgdata.sizes.iheight, &decoded_size)) + return LARDON3D_RAW_DEVELOPMENT_OUTPUT_LIMIT_EXCEEDED; + code = raw.unpack(); if (code != LIBRAW_SUCCESS) return libraw_result(code); + for (size_t i = 0; i < 4; ++i) output->resolved_camera_wb[i] = raw.imgdata.color.cam_mul[i]; + if (!valid_wb(output->resolved_camera_wb)) return LARDON3D_RAW_DEVELOPMENT_UNSUPPORTED_RAW; + for (size_t i = 0; i < 4; ++i) raw.imgdata.params.user_mul[i] = output->resolved_camera_wb[i]; + result = lardon3d_raw_development_policy_fingerprint(output->resolved_camera_wb, + output->parameter_fingerprint); + if (result != LARDON3D_RAW_DEVELOPMENT_OK) return result; + code = raw.dcraw_process(); if (code != LIBRAW_SUCCESS) return libraw_result(code); + int image_error = LIBRAW_SUCCESS; + libraw_processed_image_t *processed = raw.dcraw_make_mem_image(&image_error); + if (!processed) return libraw_result(image_error); + output->width = processed->width; output->height = processed->height; + size_t rgb_size = 0; + if (processed->type != LIBRAW_IMAGE_BITMAP || processed->colors != 3 + || !checked_rgb_size(output->width, output->height, &rgb_size) + || processed->data_size != rgb_size) { LibRaw::dcraw_clear_mem(processed); return LARDON3D_RAW_DEVELOPMENT_OUTPUT_LIMIT_EXCEEDED; } + std::vector bgr(rgb_size); + for (size_t i = 0; i < rgb_size; i += 3) { bgr[i] = processed->data[i + 2]; bgr[i + 1] = processed->data[i + 1]; bgr[i + 2] = processed->data[i]; } + LibRaw::dcraw_clear_mem(processed); + cv::Mat image((int)output->height, (int)output->width, CV_8UC3, bgr.data()); + std::vector png; + std::vector parameters = {cv::IMWRITE_PNG_COMPRESSION, kPngCompression, + cv::IMWRITE_PNG_STRATEGY, kPngStrategy}; + if (!cv::imencode(".png", image, png, parameters) || png.empty()) return LARDON3D_RAW_DEVELOPMENT_IO_ERROR; + cv::Mat validated = cv::imdecode(png, cv::IMREAD_UNCHANGED); + if (validated.empty() || validated.cols != (int)output->width + || validated.rows != (int)output->height || validated.type() != CV_8UC3) + return LARDON3D_RAW_DEVELOPMENT_IO_ERROR; + char staging[LARDON3D_APP_STATE_PATH_CAPACITY]; + int written = std::snprintf(staging, sizeof(staging), "%s/assets/images/.raw-png.tmp.XXXXXX", state->project_path); + if (written <= 0 || written >= (int)sizeof(staging)) return LARDON3D_RAW_DEVELOPMENT_IO_ERROR; + int fd = mkstemp(staging); if (fd < 0) return LARDON3D_RAW_DEVELOPMENT_IO_ERROR; + bool write_ok = write_all(fd, png.data(), png.size()); + bool sync_ok = write_ok && fsync(fd) == 0; + int close_result = close(fd); + if (!write_ok || !sync_ok || close_result != 0) { + (void)unlink(staging); + return LARDON3D_RAW_DEVELOPMENT_IO_ERROR; + } + result = catalog_result(lardon3d_image_catalog_publish_asset_file(state, staging, created_at, + kMaxSourceBytes, &output->derived_asset)); + (void)unlink(staging); + if (result != LARDON3D_RAW_DEVELOPMENT_OK) return result; + Lardon3DProjectDbAssetDerivation derivation = {}; + derivation.parent_asset_id = output->source_asset.asset_id; derivation.child_asset_id = output->derived_asset.asset_id; + derivation.kind = LARDON3D_DB_ASSET_DERIVATION_GENERIC_VERSIONED; derivation.version = 1; + std::memcpy(derivation.parameter_fingerprint, output->parameter_fingerprint, sizeof(derivation.parameter_fingerprint)); + derivation.has_producer_task = producer_task_id != 0; derivation.producer_task_id = producer_task_id; + derivation.created_at = created_at; + result = ensure_derivation(state->project_db, derivation); + if (result != LARDON3D_RAW_DEVELOPMENT_OK) return result; + char name[LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY]; + if (!valid_name(source_arw_path, name)) return LARDON3D_RAW_DEVELOPMENT_INVALID_ARGUMENT; + Lardon3DProjectDbImageRegisterStatus status; + result = db_result(lardon3d_project_db_publish_derived_capture_image(state->project_db, capture_id, + output->derived_asset.asset_id, name, source_arw_path, producer_task_id, created_at, &status, &output->image)); + if (result != LARDON3D_RAW_DEVELOPMENT_OK) return result; + std::snprintf(output->libraw_version, sizeof(output->libraw_version), "%s", libraw_version()); + std::snprintf(output->png_encoder_version, sizeof(output->png_encoder_version), "%s", CV_VERSION); + return LARDON3D_RAW_DEVELOPMENT_OK; + } catch (const std::bad_alloc &) { return LARDON3D_RAW_DEVELOPMENT_OUT_OF_MEMORY; } + catch (...) { return LARDON3D_RAW_DEVELOPMENT_INTERNAL_ERROR; } +} diff --git a/tests/test_persistent_image_catalog.c b/tests/test_persistent_image_catalog.c index d2bc871..4bc5d60 100644 --- a/tests/test_persistent_image_catalog.c +++ b/tests/test_persistent_image_catalog.c @@ -12,6 +12,9 @@ #include +Lardon3DImageCatalogAssetPublishResult lardon3d_image_catalog_test_copy_hash( + int input, int output); + #define CHECK(condition) do { if (!(condition)) { \ (void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); return false; \ } } while (0) @@ -139,12 +142,27 @@ run_test(void) { char root[] = "/tmp/lardon3d-catalog-v1-XXXXXX"; CHECK(mkdtemp(root)); - char database_path[PATH_MAX], source_a[PATH_MAX], source_b[PATH_MAX]; + char database_path[PATH_MAX], source_a[PATH_MAX], source_b[PATH_MAX], copied_path[PATH_MAX]; CHECK(join_path(database_path, root, "project.db")); CHECK(join_path(source_a, root, "photo001.jpg")); CHECK(join_path(source_b, root, "other-name.jpg")); + CHECK(join_path(copied_path, root, "copy.bin")); CHECK(write_file(source_a, "same-image-content")); CHECK(write_file(source_b, "same-image-content")); + int copy_source = open(source_a, O_RDONLY); + int copy_destination = open(copied_path, O_WRONLY | O_CREAT | O_EXCL, 0600); + CHECK(copy_source >= 0 && copy_destination >= 0); + CHECK(lardon3d_image_catalog_test_copy_hash(copy_source, copy_destination) + == LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED); + CHECK(close(copy_source) == 0 && close(copy_destination) == 0); + struct stat full_information; + CHECK(stat("/dev/full", &full_information) == 0 && S_ISCHR(full_information.st_mode)); + copy_source = open(source_a, O_RDONLY); + copy_destination = open("/dev/full", O_WRONLY); + CHECK(copy_source >= 0 && copy_destination >= 0); + CHECK(lardon3d_image_catalog_test_copy_hash(copy_source, copy_destination) + == LARDON3D_IMAGE_CATALOG_ASSET_PUBLICATION_ERROR); + CHECK(close(copy_source) == 0 && close(copy_destination) == 0); Lardon3DProjectDb *database = NULL; char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; CHECK(lardon3d_project_db_open(database_path, &database, error) diff --git a/tests/test_raw_development.cpp b/tests/test_raw_development.cpp new file mode 100644 index 0000000..2a56d95 --- /dev/null +++ b/tests/test_raw_development.cpp @@ -0,0 +1,300 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +extern "C" { +#include +} +#include + +extern "C" size_t lardon3d_raw_development_test_policy_payload_size(void); +extern "C" Lardon3DRawDevelopmentResult lardon3d_raw_development_test_ensure_derivation( + Lardon3DProjectDb *database, const Lardon3DProjectDbAssetDerivation *expected); +extern "C" Lardon3DRawDevelopmentResult +lardon3d_raw_development_test_ensure_capture_asset_role( + Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id, + Lardon3DProjectDbCaptureAssetRole expected_role); + +#define CHECK(condition) do { if (!(condition)) { \ + std::fprintf(stderr, "Failure line %d: %s\\n", __LINE__, #condition); return false; \ +} } while (0) + +static bool write_file(const char *path, const char *text) { + int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (fd < 0) return false; + size_t size = std::strlen(text); + bool ok = write(fd, text, size) == (ssize_t)size; + return close(fd) == 0 && ok; +} + +static bool remove_tree(const char *path) { + struct stat info; + if (lstat(path, &info) != 0) return errno == ENOENT; + if (!S_ISDIR(info.st_mode)) return unlink(path) == 0; + DIR *directory = opendir(path); + if (!directory) return false; + bool ok = true; + for (dirent *entry = readdir(directory); entry; entry = readdir(directory)) { + if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) continue; + char child[4096]; + int count = std::snprintf(child, sizeof(child), "%s/%s", path, entry->d_name); + if (count <= 0 || count >= (int)sizeof(child) || !remove_tree(child)) ok = false; + } + return closedir(directory) == 0 && rmdir(path) == 0 && ok; +} + +static void print_hex(const char *key, const unsigned char value[32]) { + static const char digits[] = "0123456789abcdef"; + char text[65]; + for (size_t index = 0; index < 32; ++index) { + text[index * 2] = digits[value[index] >> 4]; + text[index * 2 + 1] = digits[value[index] & 15U]; + } + text[64] = '\0'; + std::printf("%s=%s\n", key, text); +} + +static bool capture_has_asset(Lardon3DProjectDb *db, uint64_t capture_id, + uint64_t asset_id, Lardon3DProjectDbCaptureAssetRole role) { + Lardon3DProjectDbCaptureAsset assets[4]; + size_t count = 0; + if (lardon3d_project_db_list_capture_assets(db, capture_id, 0, assets, 4, &count) + != LARDON3D_PROJECT_DB_OK) return false; + for (size_t index = 0; index < count; ++index) { + if (assets[index].asset_id == asset_id && assets[index].role == role) return true; + } + return false; +} + +static bool run_golden(Lardon3DAppState *state, Lardon3DProjectDb *db, + const char *golden_path, bool keep, const char *root) { + Lardon3DProjectDbScanSet scanset; + CHECK(lardon3d_project_db_create_scanset(db, "A6000 golden", &scanset) + == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbCapture capture; + CHECK(lardon3d_project_db_create_capture(db, scanset.scanset_id, 2, &capture) + == LARDON3D_PROJECT_DB_OK); + + Lardon3DRawDevelopmentOutput first; + Lardon3DRawDevelopmentResult first_result = lardon3d_raw_develop_to_capture( + state, capture.capture_id, golden_path, 0, 2, &first); + std::fprintf(stderr, "GOLDEN_FIRST_RESULT=%d\n", (int)first_result); + CHECK(first_result == LARDON3D_RAW_DEVELOPMENT_OK); + CHECK(first.source_asset.asset_id != 0 && first.derived_asset.asset_id != 0 + && first.image.image_id != 0); + CHECK(first.source_asset.asset_id != first.derived_asset.asset_id); + Lardon3DProjectDbCapture image_capture; + CHECK(lardon3d_project_db_find_capture_for_image(db, first.image.image_id, &image_capture) + == LARDON3D_PROJECT_DB_OK && image_capture.capture_id == capture.capture_id); + CHECK(capture_has_asset(db, capture.capture_id, first.source_asset.asset_id, + LARDON3D_DB_CAPTURE_ASSET_SOURCE)); + CHECK(capture_has_asset(db, capture.capture_id, first.derived_asset.asset_id, + LARDON3D_DB_CAPTURE_ASSET_DERIVED)); + Lardon3DProjectDbAssetDerivation derivation; + CHECK(lardon3d_project_db_load_asset_derivation(db, first.derived_asset.asset_id, &derivation) + == LARDON3D_PROJECT_DB_OK); + CHECK(derivation.parent_asset_id == first.source_asset.asset_id + && derivation.child_asset_id == first.derived_asset.asset_id); + CHECK(std::memcmp(derivation.parameter_fingerprint, first.parameter_fingerprint, + sizeof(first.parameter_fingerprint)) == 0); + uint64_t selected = 0; + CHECK(lardon3d_project_db_get_selected_capture_image(db, capture.capture_id, &selected) + == LARDON3D_PROJECT_DB_NOT_FOUND); + CHECK(first.width > 0 && first.height > 0 && first.width <= 16384 && first.height <= 16384 + && (uint64_t)first.width * first.height <= 40000000U); + for (size_t index = 0; index < 4; ++index) + CHECK(std::isfinite(first.resolved_camera_wb[index]) && first.resolved_camera_wb[index] > 0.0F); + + char png_path[4096]; + CHECK(std::snprintf(png_path, sizeof(png_path), "%s/%s", state->project_path, + first.derived_asset.path) > 0); + struct stat png_info; + CHECK(stat(png_path, &png_info) == 0 && S_ISREG(png_info.st_mode) && png_info.st_size > 0); + + std::printf("GOLDEN_WIDTH=%u\n", first.width); + std::printf("GOLDEN_HEIGHT=%u\n", first.height); + for (size_t index = 0; index < 4; ++index) + std::printf("GOLDEN_WB_%zu=%.9g\n", index, first.resolved_camera_wb[index]); + print_hex("GOLDEN_SOURCE_SHA256", first.source_asset.sha256); + print_hex("GOLDEN_DERIVED_SHA256", first.derived_asset.sha256); + print_hex("GOLDEN_PARAMETER_FINGERPRINT", first.parameter_fingerprint); + std::printf("GOLDEN_SOURCE_ASSET_ID=%llu\n", + (unsigned long long)first.source_asset.asset_id); + std::printf("GOLDEN_DERIVED_ASSET_ID=%llu\n", + (unsigned long long)first.derived_asset.asset_id); + std::printf("GOLDEN_IMAGE_ID=%llu\n", (unsigned long long)first.image.image_id); + std::printf("GOLDEN_LIBRAW=%s\n", first.libraw_version); + std::printf("GOLDEN_PNG_ENCODER=%s\n", first.png_encoder_version); + std::printf("GOLDEN_PNG_PATH=%s\n", png_path); + std::printf("GOLDEN_PNG_BYTES=%llu\n", (unsigned long long)png_info.st_size); + + Lardon3DRawDevelopmentOutput second; + Lardon3DRawDevelopmentResult second_result = lardon3d_raw_develop_to_capture( + state, capture.capture_id, golden_path, 0, 2, &second); + std::fprintf(stderr, "GOLDEN_SECOND_RESULT=%d\n", (int)second_result); + CHECK(second_result == LARDON3D_RAW_DEVELOPMENT_OK); + CHECK(second.source_asset.asset_id == first.source_asset.asset_id + && second.derived_asset.asset_id == first.derived_asset.asset_id + && second.image.image_id == first.image.image_id + && std::memcmp(second.source_asset.sha256, first.source_asset.sha256, 32) == 0 + && std::memcmp(second.derived_asset.sha256, first.derived_asset.sha256, 32) == 0 + && std::memcmp(second.parameter_fingerprint, first.parameter_fingerprint, 32) == 0 + && second.width == first.width && second.height == first.height + && std::memcmp(second.resolved_camera_wb, first.resolved_camera_wb, + sizeof(first.resolved_camera_wb)) == 0); + uint64_t image_count = 0; + CHECK(lardon3d_project_db_count_images(db, scanset.scanset_id, &image_count) + == LARDON3D_PROJECT_DB_OK && image_count == 1); + CHECK(lardon3d_project_db_get_selected_capture_image(db, capture.capture_id, &selected) + == LARDON3D_PROJECT_DB_NOT_FOUND); + if (keep) std::printf("GOLDEN_PROJECT_PATH=%s\n", root); + std::printf("GOLDEN_A6000_RESULT=PASS\n"); + return true; +} + +static bool run_test() { + float wb_a[4] = {2.0F, 1.0F, 1.0F, 1.5F}; + float wb_b[4] = {2.1F, 1.0F, 1.0F, 1.5F}; + unsigned char first[32], second[32], changed[32]; + CHECK(lardon3d_raw_development_policy_fingerprint(wb_a, first) == LARDON3D_RAW_DEVELOPMENT_OK); + CHECK(lardon3d_raw_development_policy_fingerprint(wb_a, second) == LARDON3D_RAW_DEVELOPMENT_OK); + CHECK(lardon3d_raw_development_policy_fingerprint(wb_b, changed) == LARDON3D_RAW_DEVELOPMENT_OK); + CHECK(std::memcmp(first, second, sizeof(first)) == 0); + CHECK(std::memcmp(first, changed, sizeof(first)) != 0); + CHECK(lardon3d_raw_development_test_policy_payload_size() == 200); + CHECK(lardon3d_raw_development_policy_fingerprint(nullptr, first) == LARDON3D_RAW_DEVELOPMENT_INVALID_ARGUMENT); + + char root[] = "/tmp/lardon3d-raw-XXXXXX"; + CHECK(mkdtemp(root) != nullptr); + char database_path[4096], corrupt_path[4096], link_path[4096], parent_path[4096], child_path[4096], + other_parent_path[4096]; + CHECK(std::snprintf(database_path, sizeof(database_path), "%s/project.db", root) > 0); + CHECK(std::snprintf(corrupt_path, sizeof(corrupt_path), "%s/not-arw.bin", root) > 0); + CHECK(std::snprintf(link_path, sizeof(link_path), "%s/link.arw", root) > 0); + CHECK(std::snprintf(parent_path, sizeof(parent_path), "%s/parent.bin", root) > 0); + CHECK(std::snprintf(child_path, sizeof(child_path), "%s/child.bin", root) > 0); + CHECK(std::snprintf(other_parent_path, sizeof(other_parent_path), "%s/other.bin", root) > 0); + CHECK(write_file(corrupt_path, "not a raw image")); + CHECK(write_file(parent_path, "parent")); + CHECK(write_file(child_path, "child")); + CHECK(write_file(other_parent_path, "other")); + Lardon3DProjectDb *db = nullptr; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + CHECK(lardon3d_project_db_open(database_path, &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DAppState state; lardon3d_app_state_init(&state); + state.project_loaded = true; state.project_db = db; + CHECK(std::snprintf(state.project_path, sizeof(state.project_path), "%s", root) > 0); + Lardon3DProjectDbImageAsset parent_asset, child_asset, other_parent_asset; + CHECK(lardon3d_image_catalog_publish_asset_file(&state, parent_path, 1, UINT64_MAX, + &parent_asset) == LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED); + CHECK(lardon3d_image_catalog_publish_asset_file(&state, child_path, 1, UINT64_MAX, + &child_asset) == LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED); + CHECK(lardon3d_image_catalog_publish_asset_file(&state, other_parent_path, 1, UINT64_MAX, + &other_parent_asset) == LARDON3D_IMAGE_CATALOG_ASSET_PUBLISHED); + Lardon3DProjectDbAssetDerivation expected = {}; + expected.parent_asset_id = parent_asset.asset_id; + expected.child_asset_id = child_asset.asset_id; + expected.kind = LARDON3D_DB_ASSET_DERIVATION_GENERIC_VERSIONED; + expected.version = 1; + std::memset(expected.parameter_fingerprint, 0x5a, sizeof(expected.parameter_fingerprint)); + expected.created_at = 5; + Lardon3DProjectDbAssetDerivation stored; + CHECK(lardon3d_project_db_load_asset_derivation(db, child_asset.asset_id, &stored) + == LARDON3D_PROJECT_DB_NOT_FOUND); + CHECK(lardon3d_raw_development_test_ensure_derivation(db, &expected) + == LARDON3D_RAW_DEVELOPMENT_OK); + CHECK(lardon3d_project_db_load_asset_derivation(db, child_asset.asset_id, &stored) + == LARDON3D_PROJECT_DB_OK && stored.parent_asset_id == parent_asset.asset_id + && stored.child_asset_id == child_asset.asset_id && stored.kind == expected.kind + && stored.version == expected.version && !stored.has_producer_task + && stored.created_at == expected.created_at + && std::memcmp(stored.parameter_fingerprint, expected.parameter_fingerprint, 32) == 0); + Lardon3DProjectDbAssetDerivation retry = expected; + retry.has_producer_task = true; + retry.producer_task_id = 1; + retry.created_at = 9; + CHECK(lardon3d_raw_development_test_ensure_derivation(db, &retry) + == LARDON3D_RAW_DEVELOPMENT_OK); + CHECK(lardon3d_project_db_load_asset_derivation(db, child_asset.asset_id, &stored) + == LARDON3D_PROJECT_DB_OK && !stored.has_producer_task && stored.created_at == 5); + retry.parent_asset_id = other_parent_asset.asset_id; + CHECK(lardon3d_raw_development_test_ensure_derivation(db, &retry) + == LARDON3D_RAW_DEVELOPMENT_CONSTRAINT); + retry.parent_asset_id = parent_asset.asset_id; + retry.parameter_fingerprint[0] ^= 1U; + CHECK(lardon3d_raw_development_test_ensure_derivation(db, &retry) + == LARDON3D_RAW_DEVELOPMENT_CONSTRAINT); + CHECK(lardon3d_project_db_load_asset_derivation(db, child_asset.asset_id, &stored) + == LARDON3D_PROJECT_DB_OK && stored.parent_asset_id == parent_asset.asset_id + && std::memcmp(stored.parameter_fingerprint, expected.parameter_fingerprint, 32) == 0); + Lardon3DProjectDbScanSet attachment_scanset; + CHECK(lardon3d_project_db_create_scanset(db, "Attachment", &attachment_scanset) + == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbCapture attachment_capture; + CHECK(lardon3d_project_db_create_capture(db, attachment_scanset.scanset_id, 1, + &attachment_capture) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_raw_development_test_ensure_capture_asset_role(db, + attachment_capture.capture_id, parent_asset.asset_id, LARDON3D_DB_CAPTURE_ASSET_SOURCE) + == LARDON3D_RAW_DEVELOPMENT_OK); + CHECK(capture_has_asset(db, attachment_capture.capture_id, parent_asset.asset_id, + LARDON3D_DB_CAPTURE_ASSET_SOURCE)); + CHECK(lardon3d_raw_development_test_ensure_capture_asset_role(db, + attachment_capture.capture_id, parent_asset.asset_id, LARDON3D_DB_CAPTURE_ASSET_SOURCE) + == LARDON3D_RAW_DEVELOPMENT_OK); + CHECK(lardon3d_project_db_attach_capture_asset(db, attachment_capture.capture_id, + other_parent_asset.asset_id, LARDON3D_DB_CAPTURE_ASSET_SOURCE) == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbCaptureAsset attachment_page[4]; + size_t attachment_count = 0; + CHECK(lardon3d_project_db_list_capture_assets(db, attachment_capture.capture_id, 0, + attachment_page, 4, &attachment_count) == LARDON3D_PROJECT_DB_OK + && attachment_count == 2); + Lardon3DProjectDbCapture conflict_capture; + CHECK(lardon3d_project_db_create_capture(db, attachment_scanset.scanset_id, 2, + &conflict_capture) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_attach_capture_asset(db, conflict_capture.capture_id, + child_asset.asset_id, LARDON3D_DB_CAPTURE_ASSET_DERIVED) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_raw_development_test_ensure_capture_asset_role(db, + conflict_capture.capture_id, child_asset.asset_id, LARDON3D_DB_CAPTURE_ASSET_SOURCE) + == LARDON3D_RAW_DEVELOPMENT_CONSTRAINT); + CHECK(capture_has_asset(db, conflict_capture.capture_id, child_asset.asset_id, + LARDON3D_DB_CAPTURE_ASSET_DERIVED)); + Lardon3DProjectDbImageAsset rejected_asset; + CHECK(lardon3d_image_catalog_publish_asset_file(&state, corrupt_path, 1, 1, + &rejected_asset) == LARDON3D_IMAGE_CATALOG_ASSET_TOO_LARGE); + CHECK(rejected_asset.asset_id == 0); + Lardon3DProjectDbScanSet scanset; + CHECK(lardon3d_project_db_create_scanset(db, "RAW", &scanset) == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbCapture capture; + CHECK(lardon3d_project_db_create_capture(db, scanset.scanset_id, 1, &capture) == LARDON3D_PROJECT_DB_OK); + Lardon3DRawDevelopmentOutput output; + CHECK(lardon3d_raw_develop_to_capture(&state, capture.capture_id, "/missing/file.arw", 0, 1, &output) + == LARDON3D_RAW_DEVELOPMENT_SOURCE_NOT_FOUND); + CHECK(symlink(corrupt_path, link_path) == 0); + CHECK(lardon3d_raw_develop_to_capture(&state, capture.capture_id, link_path, 0, 1, &output) + == LARDON3D_RAW_DEVELOPMENT_SOURCE_NOT_FOUND); + Lardon3DRawDevelopmentResult corrupt = lardon3d_raw_develop_to_capture( + &state, capture.capture_id, corrupt_path, 0, 1, &output); + CHECK(corrupt == LARDON3D_RAW_DEVELOPMENT_UNSUPPORTED_RAW + || corrupt == LARDON3D_RAW_DEVELOPMENT_CORRUPT_RAW); + uint64_t selected = 0; + CHECK(lardon3d_project_db_get_selected_capture_image(db, capture.capture_id, &selected) + == LARDON3D_PROJECT_DB_NOT_FOUND); + uint64_t count = 1; + CHECK(lardon3d_project_db_count_images(db, scanset.scanset_id, &count) == LARDON3D_PROJECT_DB_OK && count == 0); + const char *golden_path = std::getenv("LARDON3D_RAW_GOLDEN"); + bool golden = golden_path && golden_path[0]; + const char *keep_value = std::getenv("LARDON3D_RAW_GOLDEN_KEEP"); + bool keep = golden && keep_value && std::strcmp(keep_value, "1") == 0; + if (golden) CHECK(run_golden(&state, db, golden_path, keep, root)); + lardon3d_project_db_close(db); + return keep || remove_tree(root); +} + +int main() { return run_test() ? 0 : 1; }