#include #include #include #include #include #include #include #include #include #include #include #include enum { COPY_BUFFER_SIZE = 64 * 1024 }; static bool join_path(char output[PATH_MAX], const char *parent, const char *child) { int written = snprintf(output, PATH_MAX, "%s/%s", parent, child); return written > 0 && (size_t)written < PATH_MAX; } static bool ensure_directory(const char *path) { struct stat information; if (lstat(path, &information) == 0) return S_ISDIR(information.st_mode) && !S_ISLNK(information.st_mode); if (errno != ENOENT) return false; if (mkdir(path, 0755) == 0) return true; return errno == EEXIST && lstat(path, &information) == 0 && S_ISDIR(information.st_mode) && !S_ISLNK(information.st_mode); } static bool write_all(int descriptor, const unsigned char *data, size_t size) { size_t offset = 0; while (offset < size) { ssize_t written = write(descriptor, data + offset, size - offset); if (written < 0 && errno == EINTR) continue; if (written <= 0) return false; offset += (size_t)written; } return true; } static bool 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; bool success = EVP_DigestInit_ex(context, EVP_sha256(), NULL) == 1; 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) 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; } 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; EVP_MD_CTX_free(context); if (success) *size = total; return success; } static void hash_hex(const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE], char text[65]) { static const char digits[] = "0123456789abcdef"; for (size_t index = 0; index < LARDON3D_PROJECT_DB_SHA256_SIZE; ++index) { text[index * 2] = digits[hash[index] >> 4]; text[index * 2 + 1] = digits[hash[index] & 15U]; } text[64] = '\0'; } static bool same_published_asset(const char *path, const unsigned char expected[LARDON3D_PROJECT_DB_SHA256_SIZE], uint64_t expected_size) { int descriptor = open(path, O_RDONLY | O_NOFOLLOW); if (descriptor < 0) return false; struct stat information; unsigned char actual[LARDON3D_PROJECT_DB_SHA256_SIZE]; uint64_t size = 0; bool success = fstat(descriptor, &information) == 0 && S_ISREG(information.st_mode) && hash_stream(descriptor, -1, actual, &size) && size == expected_size && memcmp(actual, expected, sizeof(actual)) == 0; (void)close(descriptor); return success; } static bool sync_directory(const char *path) { int descriptor = open(path, O_RDONLY | O_DIRECTORY); if (descriptor < 0) return false; bool success = fsync(descriptor) == 0; if (close(descriptor) != 0) success = false; return success; } bool lardon3d_image_catalog_create_scanset(Lardon3DAppState *state, const char *name, Lardon3DProjectDbScanSet *scanset) { return state && state->project_loaded && state->project_db && lardon3d_project_db_create_scanset(state->project_db, name, scanset) == LARDON3D_PROJECT_DB_OK; } Lardon3DImageCatalogImportResult lardon3d_image_catalog_import_file(Lardon3DAppState *state, uint64_t scanset_id, const char *source_path, uint64_t producer_task_id, Lardon3DProjectDbImage *image, Lardon3DProjectDbImageAsset *asset) { if (!state || !state->project_loaded || !state->project_db || scanset_id == 0 || !source_path || !source_path[0] || !image || !asset) return LARDON3D_IMAGE_CATALOG_INVALID_ARGUMENT; const char *basename = strrchr(source_path, '/'); basename = basename ? basename + 1 : source_path; if (!basename[0] || strchr(basename, '\\') || strchr(basename, '\t') || strchr(basename, '\r') || strchr(basename, '\n') || strnlen(basename, LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY) >= LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY) return LARDON3D_IMAGE_CATALOG_INVALID_ARGUMENT; 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 || lardon3d_project_db_register_image(state->project_db, scanset_id, hash, relative, size, 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, image, asset) != LARDON3D_PROJECT_DB_OK) return LARDON3D_IMAGE_CATALOG_DB_ERROR; return status == LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT ? LARDON3D_IMAGE_CATALOG_ALREADY_PRESENT : LARDON3D_IMAGE_CATALOG_IMPORTED; } Lardon3DProjectDbResult lardon3d_image_catalog_list(Lardon3DAppState *state, uint64_t scanset_id, uint64_t after_image_id, Lardon3DProjectDbImage *images, Lardon3DProjectDbImageAsset *assets, size_t capacity, size_t *count) { if (!state || !state->project_loaded || !state->project_db) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return lardon3d_project_db_list_images(state->project_db, scanset_id, after_image_id, images, assets, capacity, count); }