feat: add in-memory image catalog
This commit is contained in:
parent
8bb90ed05a
commit
118172ad0e
10 changed files with 996 additions and 9 deletions
|
|
@ -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`.
|
`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
|
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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
typedef struct Lardon3DImageCatalog Lardon3DImageCatalog;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
LARDON3D_SCREEN_HOME = 0,
|
LARDON3D_SCREEN_HOME = 0,
|
||||||
|
|
@ -19,6 +22,9 @@ typedef struct {
|
||||||
char project_name[128];
|
char project_name[128];
|
||||||
char project_path[PATH_MAX];
|
char project_path[PATH_MAX];
|
||||||
char status_message[256];
|
char status_message[256];
|
||||||
|
Lardon3DImageCatalog *image_catalog;
|
||||||
|
size_t image_selection;
|
||||||
|
size_t image_offset;
|
||||||
} Lardon3DAppState;
|
} Lardon3DAppState;
|
||||||
|
|
||||||
void lardon3d_app_state_init(Lardon3DAppState *state);
|
void lardon3d_app_state_init(Lardon3DAppState *state);
|
||||||
|
|
|
||||||
36
include/lardon3d/image_catalog.h
Normal file
36
include/lardon3d/image_catalog.h
Normal 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
|
||||||
13
meson.build
13
meson.build
|
|
@ -31,6 +31,7 @@ executable(
|
||||||
'src/layout.c',
|
'src/layout.c',
|
||||||
'src/import.c',
|
'src/import.c',
|
||||||
'src/import_task.c',
|
'src/import_task.c',
|
||||||
|
'src/image_catalog.c',
|
||||||
'src/project.c',
|
'src/project.c',
|
||||||
],
|
],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
|
|
@ -62,3 +63,15 @@ import_task_test = executable(
|
||||||
)
|
)
|
||||||
|
|
||||||
test('import-task', import_task_test, timeout: 30)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include <lardon3d/app.h>
|
#include <lardon3d/app.h>
|
||||||
#include <lardon3d/app_state.h>
|
#include <lardon3d/app_state.h>
|
||||||
|
#include <lardon3d/image_catalog.h>
|
||||||
#include <lardon3d/tui.h>
|
#include <lardon3d/tui.h>
|
||||||
|
|
||||||
int
|
int
|
||||||
|
|
@ -20,6 +21,7 @@ lardon3d_app_run(void)
|
||||||
}
|
}
|
||||||
|
|
||||||
bool success = lardon3d_tui_run(&state);
|
bool success = lardon3d_tui_run(&state);
|
||||||
|
lardon3d_image_catalog_destroy(state.image_catalog);
|
||||||
lardon3d_tui_shutdown();
|
lardon3d_tui_shutdown();
|
||||||
|
|
||||||
return success ? EXIT_SUCCESS : EXIT_FAILURE;
|
return success ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
|
|
|
||||||
393
src/image_catalog.c
Normal file
393
src/image_catalog.c
Normal 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 = ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/layout.c
77
src/layout.c
|
|
@ -3,6 +3,7 @@
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include <lardon3d/layout.h>
|
#include <lardon3d/layout.h>
|
||||||
|
#include <lardon3d/image_catalog.h>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
MINIMUM_ROWS = 20,
|
MINIMUM_ROWS = 20,
|
||||||
|
|
@ -70,7 +71,7 @@ screen_texts(
|
||||||
case LARDON3D_SCREEN_IMPORT:
|
case LARDON3D_SCREEN_IMPORT:
|
||||||
*title = "Import";
|
*title = "Import";
|
||||||
*content = "Import des images";
|
*content = "Import des images";
|
||||||
*footer = "I Importer des images ESC Accueil Q Quit";
|
*footer = "I Importer R Recharger ↑/↓ Naviguer ESC Accueil Q Quit";
|
||||||
break;
|
break;
|
||||||
case LARDON3D_SCREEN_VIEWER:
|
case LARDON3D_SCREEN_VIEWER:
|
||||||
*title = "Viewer";
|
*title = "Viewer";
|
||||||
|
|
@ -131,11 +132,74 @@ draw_project_screen(
|
||||||
draw_input_field(input_text, input_label, 11, columns);
|
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
|
static void
|
||||||
draw_import_screen(
|
draw_import_screen(
|
||||||
|
const Lardon3DAppState *state,
|
||||||
const char *input_text,
|
const char *input_text,
|
||||||
const char *input_label,
|
const char *input_label,
|
||||||
const Lardon3DImportTaskSnapshot *snapshot,
|
const Lardon3DImportTaskSnapshot *snapshot,
|
||||||
|
int rows,
|
||||||
int columns
|
int columns
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
@ -191,10 +255,11 @@ draw_import_screen(
|
||||||
draw_text(12, 4, columns - 6, "C : Annuler l'import");
|
draw_text(12, 4, columns - 6, "C : Annuler l'import");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
draw_text(7, 4, columns - 6, "I : Importer des images");
|
if (input_text) {
|
||||||
draw_text(8, 4, columns - 6, "ESC : Accueil");
|
draw_input_field(input_text, input_label, 7, columns);
|
||||||
draw_text(9, 4, columns - 6, "Q : Quitter");
|
return;
|
||||||
draw_input_field(input_text, input_label, 10, columns);
|
}
|
||||||
|
draw_catalog(state, rows, columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
@ -237,9 +302,11 @@ draw_content(
|
||||||
);
|
);
|
||||||
} else if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
} else if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
||||||
draw_import_screen(
|
draw_import_screen(
|
||||||
|
state,
|
||||||
input_text,
|
input_text,
|
||||||
input_label,
|
input_label,
|
||||||
import_snapshot,
|
import_snapshot,
|
||||||
|
rows,
|
||||||
columns
|
columns
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <lardon3d/project.h>
|
#include <lardon3d/project.h>
|
||||||
|
#include <lardon3d/image_catalog.h>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
MAX_CREATED_DIRECTORIES = 16,
|
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
|
static bool
|
||||||
normalize_name(
|
normalize_name(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
|
|
@ -346,6 +356,7 @@ lardon3d_project_create(Lardon3DAppState *state, const char *name)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clear_catalog(state);
|
||||||
state->project_loaded = true;
|
state->project_loaded = true;
|
||||||
(void)copy_path(
|
(void)copy_path(
|
||||||
state->project_name,
|
state->project_name,
|
||||||
|
|
@ -495,6 +506,7 @@ lardon3d_project_open(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clear_catalog(state);
|
||||||
state->project_loaded = true;
|
state->project_loaded = true;
|
||||||
(void)copy_path(
|
(void)copy_path(
|
||||||
state->project_name,
|
state->project_name,
|
||||||
|
|
@ -527,6 +539,7 @@ lardon3d_project_close(Lardon3DAppState *state)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clear_catalog(state);
|
||||||
state->project_loaded = false;
|
state->project_loaded = false;
|
||||||
state->project_name[0] = '\0';
|
state->project_name[0] = '\0';
|
||||||
state->project_path[0] = '\0';
|
state->project_path[0] = '\0';
|
||||||
|
|
|
||||||
159
src/tui.c
159
src/tui.c
|
|
@ -5,6 +5,7 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
#include <lardon3d/import_task.h>
|
#include <lardon3d/import_task.h>
|
||||||
|
#include <lardon3d/image_catalog.h>
|
||||||
#include <lardon3d/layout.h>
|
#include <lardon3d/layout.h>
|
||||||
#include <lardon3d/project.h>
|
#include <lardon3d/project.h>
|
||||||
#include <lardon3d/tui.h>
|
#include <lardon3d/tui.h>
|
||||||
|
|
@ -27,6 +28,70 @@ typedef struct {
|
||||||
size_t length;
|
size_t length;
|
||||||
} TuiInput;
|
} 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
|
static void
|
||||||
redraw(
|
redraw(
|
||||||
const Lardon3DAppState *state,
|
const Lardon3DAppState *state,
|
||||||
|
|
@ -135,11 +200,15 @@ handle_active_input(
|
||||||
|
|
||||||
if (key == '\n' || key == '\r' || key == KEY_ENTER) {
|
if (key == '\n' || key == '\r' || key == KEY_ENTER) {
|
||||||
if (input->mode == INPUT_PROJECT_OPEN) {
|
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) {
|
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
||||||
(void)start_import_task(state, input, task);
|
(void)start_import_task(state, input, task);
|
||||||
} else {
|
} else {
|
||||||
(void)lardon3d_project_create(state, input->text);
|
if (lardon3d_project_create(state, input->text)) {
|
||||||
|
(void)reload_catalog(state, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
input->mode = INPUT_NONE;
|
input->mode = INPUT_NONE;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -156,7 +225,9 @@ handle_active_input(
|
||||||
if (key >= 0 && key <= UCHAR_MAX && isprint((unsigned char)key)) {
|
if (key >= 0 && key <= UCHAR_MAX && isprint((unsigned char)key)) {
|
||||||
if (input->length + 1 >= sizeof(input->text)) {
|
if (input->length + 1 >= sizeof(input->text)) {
|
||||||
if (input->mode == INPUT_PROJECT_OPEN) {
|
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) {
|
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
||||||
(void)snprintf(
|
(void)snprintf(
|
||||||
state->status_message,
|
state->status_message,
|
||||||
|
|
@ -164,7 +235,9 @@ handle_active_input(
|
||||||
"Erreur : chemin source trop long."
|
"Erreur : chemin source trop long."
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
(void)lardon3d_project_create(state, input->text);
|
if (lardon3d_project_create(state, input->text)) {
|
||||||
|
(void)reload_catalog(state, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
input->mode = INPUT_NONE;
|
input->mode = INPUT_NONE;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -228,7 +301,67 @@ handle_normal_input(
|
||||||
state->screen = LARDON3D_SCREEN_HOME;
|
state->screen = LARDON3D_SCREEN_HOME;
|
||||||
return true;
|
return true;
|
||||||
case KEY_RESIZE:
|
case KEY_RESIZE:
|
||||||
|
normalize_catalog_navigation(state);
|
||||||
return true;
|
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':
|
||||||
case 'N':
|
case 'N':
|
||||||
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
||||||
|
|
@ -270,6 +403,21 @@ handle_normal_input(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
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':
|
||||||
case 'C':
|
case 'C':
|
||||||
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
||||||
|
|
@ -327,6 +475,9 @@ lardon3d_tui_run(Lardon3DAppState *state)
|
||||||
"%s",
|
"%s",
|
||||||
snapshot.message
|
snapshot.message
|
||||||
);
|
);
|
||||||
|
if (snapshot.status == LARDON3D_IMPORT_TASK_SUCCEEDED) {
|
||||||
|
(void)reload_catalog(state, false);
|
||||||
|
}
|
||||||
lardon3d_import_task_destroy(task);
|
lardon3d_import_task_destroy(task);
|
||||||
task = NULL;
|
task = NULL;
|
||||||
should_redraw = true;
|
should_redraw = true;
|
||||||
|
|
|
||||||
302
tests/test_image_catalog.c
Normal file
302
tests/test_image_catalog.c
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue