feat: add in-memory image catalog

This commit is contained in:
fy59 2026-08-06 19:55:14 +02:00
parent 8bb90ed05a
commit 118172ad0e
10 changed files with 996 additions and 9 deletions

View file

@ -35,3 +35,7 @@ le dossier choisi, sans parcourir ses sous-dossiers. Elles sont copiées vers
`images/originals` et répertoriées dans `images/manifest.tsv`.
L'import s'exécute en arrière-plan afin que la TUI reste réactive. Pendant une
opération, la touche `C` demande son annulation.
L'écran Import présente un catalogue en mémoire vérifié par rapport au manifeste
et aux fichiers de `images/originals`. Les flèches, `j`/`k`, PageUp, PageDown,
Home et End naviguent dans la liste ; `R` recharge le catalogue.

View file

@ -3,6 +3,9 @@
#include <stdbool.h>
#include <limits.h>
#include <stddef.h>
typedef struct Lardon3DImageCatalog Lardon3DImageCatalog;
typedef enum {
LARDON3D_SCREEN_HOME = 0,
@ -19,6 +22,9 @@ typedef struct {
char project_name[128];
char project_path[PATH_MAX];
char status_message[256];
Lardon3DImageCatalog *image_catalog;
size_t image_selection;
size_t image_offset;
} Lardon3DAppState;
void lardon3d_app_state_init(Lardon3DAppState *state);

View file

@ -0,0 +1,36 @@
#ifndef LARDON3D_IMAGE_CATALOG_H
#define LARDON3D_IMAGE_CATALOG_H
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/app_state.h>
typedef struct {
char *filename;
char *source_path;
uint64_t size_bytes;
} Lardon3DImageEntry;
Lardon3DImageCatalog *lardon3d_image_catalog_load(
const Lardon3DAppState *state,
char *error_message,
size_t error_message_size
);
void lardon3d_image_catalog_destroy(Lardon3DImageCatalog *catalog);
size_t lardon3d_image_catalog_count(const Lardon3DImageCatalog *catalog);
uint64_t lardon3d_image_catalog_total_size(
const Lardon3DImageCatalog *catalog
);
const Lardon3DImageEntry *lardon3d_image_catalog_get(
const Lardon3DImageCatalog *catalog,
size_t index
);
void lardon3d_image_catalog_format_size(
uint64_t size_bytes,
char *text,
size_t text_size
);
#endif

View file

@ -31,6 +31,7 @@ executable(
'src/layout.c',
'src/import.c',
'src/import_task.c',
'src/image_catalog.c',
'src/project.c',
],
include_directories: include_directories('include'),
@ -62,3 +63,15 @@ import_task_test = executable(
)
test('import-task', import_task_test, timeout: 30)
image_catalog_test = executable(
'test-image-catalog',
sources: [
'tests/test_image_catalog.c',
'src/app_state.c',
'src/image_catalog.c',
],
include_directories: include_directories('include'),
)
test('image-catalog', image_catalog_test, timeout: 30)

View file

@ -3,6 +3,7 @@
#include <lardon3d/app.h>
#include <lardon3d/app_state.h>
#include <lardon3d/image_catalog.h>
#include <lardon3d/tui.h>
int
@ -20,6 +21,7 @@ lardon3d_app_run(void)
}
bool success = lardon3d_tui_run(&state);
lardon3d_image_catalog_destroy(state.image_catalog);
lardon3d_tui_shutdown();
return success ? EXIT_SUCCESS : EXIT_FAILURE;

393
src/image_catalog.c Normal file
View file

@ -0,0 +1,393 @@
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <lardon3d/image_catalog.h>
struct Lardon3DImageCatalog {
Lardon3DImageEntry *entries;
size_t count;
size_t capacity;
uint64_t total_size;
};
static void
set_error(char *message, size_t size, const char *text)
{
if (message && size > 0) {
(void)snprintf(message, size, "%s", text);
}
}
static char *
join_path(const char *parent, const char *child)
{
size_t parent_length = strlen(parent);
size_t child_length = strlen(child);
if (parent_length > SIZE_MAX - child_length - 2) {
return NULL;
}
size_t size = parent_length + child_length + 2;
char *path = malloc(size);
if (!path) {
return NULL;
}
int written = snprintf(path, size, "%s/%s", parent, child);
if (written < 0 || (size_t)written >= size) {
free(path);
return NULL;
}
return path;
}
static bool
parse_size(const char *text, uint64_t *value)
{
if (!text[0]) {
return false;
}
uint64_t parsed = 0;
for (const unsigned char *digit = (const unsigned char *)text;
*digit;
++digit) {
if (*digit < '0' || *digit > '9') {
return false;
}
uint64_t number = (uint64_t)(*digit - '0');
if (parsed > (UINT64_MAX - number) / 10) {
return false;
}
parsed = parsed * 10 + number;
}
*value = parsed;
return true;
}
static bool
valid_filename(const char *filename)
{
return filename[0]
&& !strchr(filename, '/')
&& !strchr(filename, '\\')
&& !strchr(filename, '\t')
&& !strchr(filename, '\r')
&& !strchr(filename, '\n');
}
static bool
filename_exists(
const Lardon3DImageCatalog *catalog,
const char *filename
)
{
for (size_t index = 0; index < catalog->count; ++index) {
if (strcmp(catalog->entries[index].filename, filename) == 0) {
return true;
}
}
return false;
}
static bool
append_entry(
Lardon3DImageCatalog *catalog,
const char *filename,
const char *source_path,
uint64_t size_bytes
)
{
if (catalog->count == catalog->capacity) {
size_t capacity = catalog->capacity == 0 ? 16 : catalog->capacity * 2;
if (capacity < catalog->capacity
|| capacity > SIZE_MAX / sizeof(*catalog->entries)) {
return false;
}
void *entries = realloc(
catalog->entries,
capacity * sizeof(*catalog->entries)
);
if (!entries) {
return false;
}
catalog->entries = entries;
catalog->capacity = capacity;
}
char *filename_copy = strdup(filename);
char *source_copy = strdup(source_path);
if (!filename_copy || !source_copy) {
free(filename_copy);
free(source_copy);
return false;
}
catalog->entries[catalog->count] = (Lardon3DImageEntry) {
.filename = filename_copy,
.source_path = source_copy,
.size_bytes = size_bytes,
};
++catalog->count;
catalog->total_size += size_bytes;
return true;
}
void
lardon3d_image_catalog_destroy(Lardon3DImageCatalog *catalog)
{
if (!catalog) {
return;
}
for (size_t index = 0; index < catalog->count; ++index) {
free(catalog->entries[index].filename);
free(catalog->entries[index].source_path);
}
free(catalog->entries);
free(catalog);
}
static bool
validate_file(
const char *originals_path,
const char *filename,
uint64_t expected_size,
char *error_message,
size_t error_message_size
)
{
char *path = join_path(originals_path, filename);
if (!path) {
set_error(error_message, error_message_size, "Erreur : chemin d'image trop long.");
return false;
}
struct stat info;
if (lstat(path, &info) != 0) {
free(path);
set_error(error_message, error_message_size, "Erreur : image du manifeste absente.");
return false;
}
free(path);
if (S_ISLNK(info.st_mode) || !S_ISREG(info.st_mode)) {
set_error(error_message, error_message_size, "Erreur : image du manifeste non régulière.");
return false;
}
if (info.st_size < 0 || (uintmax_t)info.st_size != (uintmax_t)expected_size) {
set_error(error_message, error_message_size, "Erreur : taille d'image incohérente.");
return false;
}
return true;
}
static bool
parse_line(
Lardon3DImageCatalog *catalog,
char *line,
const char *originals_path,
char *error_message,
size_t error_message_size
)
{
char *first_tab = strchr(line, '\t');
char *second_tab = first_tab ? strchr(first_tab + 1, '\t') : NULL;
if (!first_tab || !second_tab || strchr(second_tab + 1, '\t')) {
set_error(error_message, error_message_size, "Erreur : ligne de manifeste invalide.");
return false;
}
*first_tab = '\0';
*second_tab = '\0';
const char *filename = line;
const char *size_text = first_tab + 1;
const char *source_path = second_tab + 1;
uint64_t size_bytes;
if (!valid_filename(filename)) {
set_error(error_message, error_message_size, "Erreur : nom d'image invalide dans le manifeste.");
return false;
}
if (!source_path[0]) {
set_error(error_message, error_message_size, "Erreur : chemin source vide dans le manifeste.");
return false;
}
if (!parse_size(size_text, &size_bytes)) {
set_error(error_message, error_message_size, "Erreur : taille invalide dans le manifeste.");
return false;
}
if (filename_exists(catalog, filename)) {
set_error(error_message, error_message_size, "Erreur : image dupliquée dans le manifeste.");
return false;
}
if (catalog->total_size > UINT64_MAX - size_bytes) {
set_error(error_message, error_message_size, "Erreur : taille totale du catalogue trop grande.");
return false;
}
if (!validate_file(
originals_path,
filename,
size_bytes,
error_message,
error_message_size
)) {
return false;
}
if (!append_entry(catalog, filename, source_path, size_bytes)) {
set_error(error_message, error_message_size, "Erreur : mémoire insuffisante pour le catalogue.");
return false;
}
return true;
}
Lardon3DImageCatalog *
lardon3d_image_catalog_load(
const Lardon3DAppState *state,
char *error_message,
size_t error_message_size
)
{
set_error(error_message, error_message_size, "");
if (!state || !state->project_loaded || !state->project_path[0]) {
set_error(error_message, error_message_size, "Aucun projet chargé.");
return NULL;
}
Lardon3DImageCatalog *catalog = calloc(1, sizeof(*catalog));
if (!catalog) {
set_error(error_message, error_message_size, "Erreur : mémoire insuffisante pour le catalogue.");
return NULL;
}
char *images_path = join_path(state->project_path, "images");
char *originals_path = images_path ? join_path(images_path, "originals") : NULL;
char *manifest_path = images_path ? join_path(images_path, "manifest.tsv") : NULL;
if (!images_path || !originals_path || !manifest_path) {
set_error(error_message, error_message_size, "Erreur : chemin du catalogue trop long.");
goto failure;
}
int descriptor = open(manifest_path, O_RDONLY | O_NOFOLLOW);
if (descriptor < 0) {
if (errno == ENOENT) {
set_error(error_message, error_message_size, "Aucune image importée.");
free(images_path);
free(originals_path);
free(manifest_path);
return catalog;
}
set_error(error_message, error_message_size, "Erreur : impossible d'ouvrir manifest.tsv.");
goto failure;
}
struct stat manifest_info;
if (fstat(descriptor, &manifest_info) != 0 || !S_ISREG(manifest_info.st_mode)) {
(void)close(descriptor);
set_error(error_message, error_message_size, "Erreur : manifest.tsv n'est pas régulier.");
goto failure;
}
FILE *file = fdopen(descriptor, "r");
if (!file) {
(void)close(descriptor);
set_error(error_message, error_message_size, "Erreur : impossible de lire manifest.tsv.");
goto failure;
}
char *line = NULL;
size_t capacity = 0;
ssize_t length = getline(&line, &capacity, file);
bool valid = length >= 0
&& strcmp(line, "filename\tsize_bytes\tsource_path\n") == 0;
if (!valid) {
set_error(error_message, error_message_size, "Erreur : en-tête de manifest.tsv invalide.");
}
while (valid && (length = getline(&line, &capacity, file)) >= 0) {
if (length == 0 || line[(size_t)length - 1] != '\n') {
set_error(error_message, error_message_size, "Erreur : ligne tronquée dans manifest.tsv.");
valid = false;
break;
}
line[(size_t)length - 1] = '\0';
if (strchr(line, '\r') || strchr(line, '\n')
|| !parse_line(
catalog,
line,
originals_path,
error_message,
error_message_size
)) {
if (!error_message || !error_message_size || !error_message[0]) {
set_error(error_message, error_message_size, "Erreur : ligne de manifeste invalide.");
}
valid = false;
}
}
if (ferror(file) || fclose(file) != 0) {
set_error(error_message, error_message_size, "Erreur : lecture de manifest.tsv impossible.");
valid = false;
}
free(line);
free(images_path);
free(originals_path);
free(manifest_path);
if (!valid) {
lardon3d_image_catalog_destroy(catalog);
return NULL;
}
return catalog;
failure:
free(images_path);
free(originals_path);
free(manifest_path);
lardon3d_image_catalog_destroy(catalog);
return NULL;
}
size_t
lardon3d_image_catalog_count(const Lardon3DImageCatalog *catalog)
{
return catalog ? catalog->count : 0;
}
uint64_t
lardon3d_image_catalog_total_size(const Lardon3DImageCatalog *catalog)
{
return catalog ? catalog->total_size : 0;
}
const Lardon3DImageEntry *
lardon3d_image_catalog_get(
const Lardon3DImageCatalog *catalog,
size_t index
)
{
return catalog && index < catalog->count ? &catalog->entries[index] : NULL;
}
void
lardon3d_image_catalog_format_size(
uint64_t size_bytes,
char *text,
size_t text_size
)
{
if (!text || text_size == 0) {
return;
}
static const char *units[] = {"octets", "Kio", "Mio", "Gio"};
long double value = (long double)size_bytes;
size_t unit = 0;
while (value >= 1024.0L && unit + 1 < sizeof(units) / sizeof(units[0])) {
value /= 1024.0L;
++unit;
}
if (unit == 0) {
(void)snprintf(text, text_size, "%" PRIu64 " %s", size_bytes, units[unit]);
} else {
(void)snprintf(text, text_size, "%.1Lf %s", value, units[unit]);
char *decimal = strchr(text, '.');
if (decimal) {
*decimal = ',';
}
}
}

View file

@ -3,6 +3,7 @@
#include <string.h>
#include <lardon3d/layout.h>
#include <lardon3d/image_catalog.h>
enum {
MINIMUM_ROWS = 20,
@ -70,7 +71,7 @@ screen_texts(
case LARDON3D_SCREEN_IMPORT:
*title = "Import";
*content = "Import des images";
*footer = "I Importer des images ESC Accueil Q Quit";
*footer = "I Importer R Recharger ↑/↓ Naviguer ESC Accueil Q Quit";
break;
case LARDON3D_SCREEN_VIEWER:
*title = "Viewer";
@ -131,11 +132,74 @@ draw_project_screen(
draw_input_field(input_text, input_label, 11, columns);
}
static void
draw_catalog(
const Lardon3DAppState *state,
int rows,
int columns
)
{
if (!state->project_loaded) {
draw_text(7, 4, columns - 6, "Aucun projet chargé.");
return;
}
size_t count = lardon3d_image_catalog_count(state->image_catalog);
char line[512];
(void)snprintf(
line,
sizeof(line),
"Images importées : %zu",
count
);
draw_text(6, 4, columns - 6, line);
char size_text[64];
lardon3d_image_catalog_format_size(
lardon3d_image_catalog_total_size(state->image_catalog),
size_text,
sizeof(size_text)
);
(void)snprintf(line, sizeof(line), "Taille totale : %s", size_text);
draw_text(7, 4, columns - 6, line);
if (count == 0) {
draw_text(9, 4, columns - 6, "Aucune image importée.");
return;
}
int journal_row = rows - 7;
size_t visible = journal_row > 9 ? (size_t)(journal_row - 9) : 0;
for (size_t row = 0; row < visible; ++row) {
size_t index = state->image_offset + row;
const Lardon3DImageEntry *entry = lardon3d_image_catalog_get(
state->image_catalog,
index
);
if (!entry) {
break;
}
lardon3d_image_catalog_format_size(
entry->size_bytes,
size_text,
sizeof(size_text)
);
(void)snprintf(
line,
sizeof(line),
"%c %s %s",
index == state->image_selection ? '>' : ' ',
entry->filename,
size_text
);
draw_text(9 + (int)row, 4, columns - 6, line);
}
}
static void
draw_import_screen(
const Lardon3DAppState *state,
const char *input_text,
const char *input_label,
const Lardon3DImportTaskSnapshot *snapshot,
int rows,
int columns
)
{
@ -191,10 +255,11 @@ draw_import_screen(
draw_text(12, 4, columns - 6, "C : Annuler l'import");
return;
}
draw_text(7, 4, columns - 6, "I : Importer des images");
draw_text(8, 4, columns - 6, "ESC : Accueil");
draw_text(9, 4, columns - 6, "Q : Quitter");
draw_input_field(input_text, input_label, 10, columns);
if (input_text) {
draw_input_field(input_text, input_label, 7, columns);
return;
}
draw_catalog(state, rows, columns);
}
static void
@ -237,9 +302,11 @@ draw_content(
);
} else if (state->screen == LARDON3D_SCREEN_IMPORT) {
draw_import_screen(
state,
input_text,
input_label,
import_snapshot,
rows,
columns
);
} else {

View file

@ -10,6 +10,7 @@
#include <unistd.h>
#include <lardon3d/project.h>
#include <lardon3d/image_catalog.h>
enum {
MAX_CREATED_DIRECTORIES = 16,
@ -32,6 +33,15 @@ set_status(Lardon3DAppState *state, const char *message)
);
}
static void
clear_catalog(Lardon3DAppState *state)
{
lardon3d_image_catalog_destroy(state->image_catalog);
state->image_catalog = NULL;
state->image_selection = 0;
state->image_offset = 0;
}
static bool
normalize_name(
Lardon3DAppState *state,
@ -346,6 +356,7 @@ lardon3d_project_create(Lardon3DAppState *state, const char *name)
return false;
}
clear_catalog(state);
state->project_loaded = true;
(void)copy_path(
state->project_name,
@ -495,6 +506,7 @@ lardon3d_project_open(
return false;
}
clear_catalog(state);
state->project_loaded = true;
(void)copy_path(
state->project_name,
@ -527,6 +539,7 @@ lardon3d_project_close(Lardon3DAppState *state)
return;
}
clear_catalog(state);
state->project_loaded = false;
state->project_name[0] = '\0';
state->project_path[0] = '\0';

159
src/tui.c
View file

@ -5,6 +5,7 @@
#include <stdio.h>
#include <lardon3d/import_task.h>
#include <lardon3d/image_catalog.h>
#include <lardon3d/layout.h>
#include <lardon3d/project.h>
#include <lardon3d/tui.h>
@ -27,6 +28,70 @@ typedef struct {
size_t length;
} TuiInput;
static size_t
catalog_page_size(void)
{
int rows;
int columns;
getmaxyx(stdscr, rows, columns);
(void)columns;
return rows > 16 ? (size_t)(rows - 16) : 1;
}
static void
normalize_catalog_navigation(Lardon3DAppState *state)
{
size_t count = lardon3d_image_catalog_count(state->image_catalog);
if (count == 0) {
state->image_selection = 0;
state->image_offset = 0;
return;
}
if (state->image_selection >= count) {
state->image_selection = count - 1;
}
if (state->image_offset > state->image_selection) {
state->image_offset = state->image_selection;
}
size_t page_size = catalog_page_size();
if (state->image_selection - state->image_offset >= page_size) {
state->image_offset = state->image_selection - page_size + 1;
}
}
static bool
reload_catalog(Lardon3DAppState *state, bool announce_success)
{
lardon3d_image_catalog_destroy(state->image_catalog);
state->image_catalog = NULL;
char message[sizeof(state->status_message)];
state->image_catalog = lardon3d_image_catalog_load(
state,
message,
sizeof(message)
);
normalize_catalog_navigation(state);
if (!state->image_catalog || message[0]) {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"%s",
message
);
return state->image_catalog != NULL;
}
if (announce_success) {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Catalogue rechargé : %zu image%s.",
lardon3d_image_catalog_count(state->image_catalog),
lardon3d_image_catalog_count(state->image_catalog) == 1 ? "" : "s"
);
}
return true;
}
static void
redraw(
const Lardon3DAppState *state,
@ -135,11 +200,15 @@ handle_active_input(
if (key == '\n' || key == '\r' || key == KEY_ENTER) {
if (input->mode == INPUT_PROJECT_OPEN) {
(void)lardon3d_project_open(state, input->text);
if (lardon3d_project_open(state, input->text)) {
(void)reload_catalog(state, false);
}
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
(void)start_import_task(state, input, task);
} else {
(void)lardon3d_project_create(state, input->text);
if (lardon3d_project_create(state, input->text)) {
(void)reload_catalog(state, false);
}
}
input->mode = INPUT_NONE;
return true;
@ -156,7 +225,9 @@ handle_active_input(
if (key >= 0 && key <= UCHAR_MAX && isprint((unsigned char)key)) {
if (input->length + 1 >= sizeof(input->text)) {
if (input->mode == INPUT_PROJECT_OPEN) {
(void)lardon3d_project_open(state, input->text);
if (lardon3d_project_open(state, input->text)) {
(void)reload_catalog(state, false);
}
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
(void)snprintf(
state->status_message,
@ -164,7 +235,9 @@ handle_active_input(
"Erreur : chemin source trop long."
);
} else {
(void)lardon3d_project_create(state, input->text);
if (lardon3d_project_create(state, input->text)) {
(void)reload_catalog(state, false);
}
}
input->mode = INPUT_NONE;
return true;
@ -228,7 +301,67 @@ handle_normal_input(
state->screen = LARDON3D_SCREEN_HOME;
return true;
case KEY_RESIZE:
normalize_catalog_navigation(state);
return true;
case KEY_UP:
case 'k':
if (state->screen == LARDON3D_SCREEN_IMPORT
&& state->image_selection > 0) {
--state->image_selection;
normalize_catalog_navigation(state);
return true;
}
return false;
case KEY_DOWN:
case 'j': {
size_t count = lardon3d_image_catalog_count(state->image_catalog);
if (state->screen == LARDON3D_SCREEN_IMPORT
&& state->image_selection + 1 < count) {
++state->image_selection;
normalize_catalog_navigation(state);
return true;
}
return false;
}
case KEY_PPAGE:
if (state->screen == LARDON3D_SCREEN_IMPORT) {
size_t page_size = catalog_page_size();
state->image_selection = state->image_selection > page_size
? state->image_selection - page_size
: 0;
normalize_catalog_navigation(state);
return true;
}
return false;
case KEY_NPAGE:
if (state->screen == LARDON3D_SCREEN_IMPORT) {
size_t count = lardon3d_image_catalog_count(state->image_catalog);
if (count > 0) {
size_t remaining = count - 1 - state->image_selection;
size_t page_size = catalog_page_size();
state->image_selection += remaining < page_size
? remaining
: page_size;
normalize_catalog_navigation(state);
}
return true;
}
return false;
case KEY_HOME:
if (state->screen == LARDON3D_SCREEN_IMPORT) {
state->image_selection = 0;
normalize_catalog_navigation(state);
return true;
}
return false;
case KEY_END:
if (state->screen == LARDON3D_SCREEN_IMPORT) {
size_t count = lardon3d_image_catalog_count(state->image_catalog);
state->image_selection = count > 0 ? count - 1 : 0;
normalize_catalog_navigation(state);
return true;
}
return false;
case 'n':
case 'N':
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
@ -270,6 +403,21 @@ handle_normal_input(
return true;
}
return false;
case 'r':
case 'R':
if (state->screen == LARDON3D_SCREEN_IMPORT) {
if (!state->project_loaded) {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Aucun projet chargé."
);
} else {
(void)reload_catalog(state, true);
}
return true;
}
return false;
case 'c':
case 'C':
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
@ -327,6 +475,9 @@ lardon3d_tui_run(Lardon3DAppState *state)
"%s",
snapshot.message
);
if (snapshot.status == LARDON3D_IMPORT_TASK_SUCCEEDED) {
(void)reload_catalog(state, false);
}
lardon3d_import_task_destroy(task);
task = NULL;
should_redraw = true;

302
tests/test_image_catalog.c Normal file
View file

@ -0,0 +1,302 @@
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <lardon3d/app_state.h>
#include <lardon3d/image_catalog.h>
#define CHECK(condition) \
do { \
if (!(condition)) { \
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \
return false; \
} \
} while (0)
static bool
join_path(char destination[PATH_MAX], const char *parent, const char *child)
{
int written = snprintf(destination, PATH_MAX, "%s/%s", parent, child);
return written >= 0 && (size_t)written < PATH_MAX;
}
static bool
write_all(int descriptor, const char *content, size_t length)
{
size_t total = 0;
while (total < length) {
ssize_t written = write(descriptor, content + total, length - total);
if (written < 0 && errno == EINTR) {
continue;
}
if (written <= 0) {
return false;
}
total += (size_t)written;
}
return true;
}
static bool
create_file(const char *path, const char *content)
{
int descriptor = open(path, O_WRONLY | O_CREAT | O_EXCL, 0644);
if (descriptor < 0) {
return false;
}
bool success = write_all(descriptor, content, strlen(content));
if (close(descriptor) != 0) {
success = false;
}
return success;
}
static bool
replace_manifest(const char *path, const char *content)
{
if (unlink(path) != 0 && errno != ENOENT) {
return false;
}
return create_file(path, content);
}
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 success = true;
for (struct dirent *entry = readdir(directory);
entry;
entry = readdir(directory)) {
if (strcmp(entry->d_name, ".") == 0
|| strcmp(entry->d_name, "..") == 0) {
continue;
}
char child[PATH_MAX];
if (!join_path(child, path, entry->d_name) || !remove_tree(child)) {
success = false;
}
}
if (closedir(directory) != 0 || rmdir(path) != 0) {
success = false;
}
return success;
}
static bool
expect_invalid(
const Lardon3DAppState *state,
const char *manifest,
const char *content
)
{
CHECK(replace_manifest(manifest, content));
char error[256];
Lardon3DImageCatalog *catalog = lardon3d_image_catalog_load(
state,
error,
sizeof(error)
);
CHECK(!catalog);
CHECK(error[0]);
lardon3d_image_catalog_destroy(catalog);
return true;
}
static bool
test_sizes(void)
{
char text[64];
lardon3d_image_catalog_format_size(512, text, sizeof(text));
CHECK(strcmp(text, "512 octets") == 0);
lardon3d_image_catalog_format_size(1536, text, sizeof(text));
CHECK(strcmp(text, "1,5 Kio") == 0);
lardon3d_image_catalog_format_size(2 * 1024 * 1024, text, sizeof(text));
CHECK(strcmp(text, "2,0 Mio") == 0);
lardon3d_image_catalog_format_size(
UINT64_C(3) * 1024 * 1024 * 1024,
text,
sizeof(text)
);
CHECK(strcmp(text, "3,0 Gio") == 0);
lardon3d_image_catalog_format_size(0, NULL, 0);
return true;
}
static bool
run_test(void)
{
char base[] = "/tmp/lardon3d-catalog-test.XXXXXX";
CHECK(mkdtemp(base));
char project[PATH_MAX];
char images[PATH_MAX];
char originals[PATH_MAX];
char manifest[PATH_MAX];
CHECK(join_path(project, base, "project"));
CHECK(join_path(images, project, "images"));
CHECK(join_path(originals, images, "originals"));
CHECK(join_path(manifest, images, "manifest.tsv"));
CHECK(mkdir(project, 0755) == 0);
CHECK(mkdir(images, 0755) == 0);
CHECK(mkdir(originals, 0755) == 0);
Lardon3DAppState state;
lardon3d_app_state_init(&state);
state.project_loaded = true;
CHECK(snprintf(
state.project_path,
sizeof(state.project_path),
"%s",
project
) > 0);
char error[256];
Lardon3DImageCatalog *catalog = lardon3d_image_catalog_load(
&state,
error,
sizeof(error)
);
CHECK(catalog);
CHECK(lardon3d_image_catalog_count(catalog) == 0);
CHECK(strcmp(error, "Aucune image importée.") == 0);
CHECK(lardon3d_image_catalog_get(catalog, 0) == NULL);
lardon3d_image_catalog_destroy(catalog);
lardon3d_image_catalog_destroy(NULL);
char first[PATH_MAX];
char utf8[PATH_MAX];
char link_path[PATH_MAX];
CHECK(join_path(first, originals, "première image.jpg"));
CHECK(join_path(utf8, originals, "été.png"));
CHECK(join_path(link_path, originals, "lien.jpg"));
CHECK(create_file(first, "abc"));
CHECK(create_file(utf8, "12345"));
const char valid_manifest[] =
"filename\tsize_bytes\tsource_path\n"
"première image.jpg\t3\t/tmp/source avec espaces/première image.jpg\n"
"été.png\t5\t/tmp/source/été.png\n";
CHECK(replace_manifest(manifest, valid_manifest));
catalog = lardon3d_image_catalog_load(&state, error, sizeof(error));
CHECK(catalog);
CHECK(error[0] == '\0');
CHECK(lardon3d_image_catalog_count(catalog) == 2);
CHECK(lardon3d_image_catalog_total_size(catalog) == 8);
const Lardon3DImageEntry *entry = lardon3d_image_catalog_get(catalog, 0);
CHECK(entry);
CHECK(strcmp(entry->filename, "première image.jpg") == 0);
CHECK(strcmp(
entry->source_path,
"/tmp/source avec espaces/première image.jpg"
) == 0);
CHECK(entry->size_bytes == 3);
entry = lardon3d_image_catalog_get(catalog, 1);
CHECK(entry && strcmp(entry->filename, "été.png") == 0);
CHECK(lardon3d_image_catalog_get(catalog, 2) == NULL);
lardon3d_image_catalog_destroy(catalog);
CHECK(expect_invalid(
&state,
manifest,
"bad\theader\npremière image.jpg\t3\t/tmp/source\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\npremière image.jpg\t3\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\npremière image.jpg\t3\t/a\textra\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\npremière image.jpg\t12x\t/a\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\n"
"première image.jpg\t18446744073709551616\t/a\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\n\t3\t/a\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\ndir/image.jpg\t3\t/a\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\ndir\\image.jpg\t3\t/a\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\npremière image.jpg\t3\t\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\n"
"première image.jpg\t3\t/a\npremière image.jpg\t3\t/b\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\nabsente.jpg\t1\t/a\n"
));
CHECK(symlink(first, link_path) == 0);
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\nlien.jpg\t3\t/a\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\npremière image.jpg\t4\t/a\n"
));
CHECK(expect_invalid(
&state,
manifest,
"filename\tsize_bytes\tsource_path\npremière image.jpg\t3\t/a"
));
CHECK(replace_manifest(manifest, valid_manifest));
catalog = lardon3d_image_catalog_load(&state, error, sizeof(error));
CHECK(catalog && lardon3d_image_catalog_count(catalog) == 2);
lardon3d_image_catalog_destroy(catalog);
CHECK(test_sizes());
CHECK(remove_tree(base));
return true;
}
int
main(void)
{
return run_test() ? EXIT_SUCCESS : EXIT_FAILURE;
}