From a934063f03e525e78d6a82b64fbcb4d3c182c7a4 Mon Sep 17 00:00:00 2001 From: guilhhotina Date: Thu, 7 May 2026 16:20:00 -0300 Subject: [PATCH 01/11] fix: report missing pipeline paths --- src/Backend_Workers/pipeline.c | 48 ++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/src/Backend_Workers/pipeline.c b/src/Backend_Workers/pipeline.c index 7531e16..9a31a14 100644 --- a/src/Backend_Workers/pipeline.c +++ b/src/Backend_Workers/pipeline.c @@ -23,30 +23,68 @@ void PipelineBegin(haio_pipeline_t* pipe, haio_type_t first_step) { * @details add tasks to streaming pipeline * @param [in,out] pipe */ -void PipelineStepAdd(haio_pipeline_t* pipe, haio_type_t next_step) { +int PipelineStepAdd(haio_pipeline_t* pipe, haio_type_t next_step) { + static char path_not_found[] = "pipeline path not found"; + static char worker_limit_exceeded[] = "pipeline worker limit exceeded"; haio_worker_func_t workers[HAIO_MAX_STEPS_BY_WORKER]; haio_type_t from = pipe->current_format; haio_type_t to = next_step; haio_type_t result = HAIO_TYPE_NULL; - uint8_t nworkers = GetCodecWorkersFromFormats(from, to, workers, HAIO_MAX_STEPS_BY_WORKER, &result); + uint8_t nworkers; + + if (PipelineHasError(pipe)) { + return 1; + } + + nworkers = GetCodecWorkersFromFormats( + from, + to, + workers, + HAIO_MAX_STEPS_BY_WORKER, + &result + ); + + if (!nworkers || result == HAIO_TYPE_NULL) { + pipe->error = path_not_found; + return 1; + } + + if (nworkers > HAIO_MAX_WORKERS_BY_PIPE - pipe->worker_count) { + pipe->error = worker_limit_exceeded; + return 1; + } + pipe->current_format = result; pipe->state = HAIO_FSM_PIPE_PREPARE; - memcpy(&pipe->workers[pipe->worker_count], workers, nworkers * sizeof(haio_worker_func_t)); + memcpy( + &pipe->workers[pipe->worker_count], + workers, + nworkers * sizeof(haio_worker_func_t) + ); + pipe->worker_count += nworkers; + + return 0; } /** * @details finish pipeline tasks * @param [in,out] pipe */ -void PipelineEnd(haio_pipeline_t* pipe, haio_type_t last_step) { - PipelineStepAdd(pipe, last_step); +int PipelineEnd(haio_pipeline_t* pipe, haio_type_t last_step) { + if (PipelineStepAdd(pipe, last_step)) { + return 1; + } + pipe->state = HAIO_FSM_PIPE_RUNNING; + for (uint8_t i = 1; i < pipe->worker_count; i++) { pipe->handlers[i].canvas.parent = &pipe->handlers[i - 1].canvas; } + + return 0; } /** From f53f7361fb8d697dfa0442f6d9b590df87fb4327 Mon Sep 17 00:00:00 2001 From: guilhhotina Date: Thu, 7 May 2026 17:05:00 -0300 Subject: [PATCH 02/11] feat: parse convert arguments into pipeline steps --- src/Frontend_Convert/cli.c | 185 +++++++++++++++++++++++++++++++++---- 1 file changed, 167 insertions(+), 18 deletions(-) diff --git a/src/Frontend_Convert/cli.c b/src/Frontend_Convert/cli.c index b6a45e4..d302aba 100644 --- a/src/Frontend_Convert/cli.c +++ b/src/Frontend_Convert/cli.c @@ -5,37 +5,186 @@ #include "haio.h" #include "haio/functions.h" -#define BUFFER_SIZE 1024 +#define BUFFER_SIZE 4096 +#define FORMAT_PROBE_SIZE 16 -int FrontendConvertCli(int argc, char* argv[]) { - if (argc <= 2) { - printf("usage:\n./img convert input.png output.ppm\n"); +static void PrintConvertUsage(void) +{ + printf("usage:\n"); + printf("./img convert input.png [options] output.ppm\n"); + printf("\n"); + printf("options:\n"); + printf(" -crop crop image using the default crop filter\n"); +} + +static haio_type_t GetFormatFromPath(char *path) +{ + char *ext = GetExtensionFromString(path); + + if (!ext) { + return HAIO_TYPE_NULL; + } + + return GetFormatFromExtension(ext); +} + +static haio_type_t GetInputFormatFromBytes(const unsigned char *buffer, size_t len) +{ + static const unsigned char png_signature[] = { + 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' + }; + + if (len >= sizeof(png_signature) && + memcmp(buffer, png_signature, sizeof(png_signature)) == 0) { + return HAIO_TYPE_IMG_PNG; + } + + return HAIO_TYPE_NULL; +} + +static haio_type_t GetInputFormat(FILE *f_in, char *path) +{ + unsigned char buffer[FORMAT_PROBE_SIZE]; + haio_type_t format; + size_t nread; + + nread = fread(buffer, sizeof(unsigned char), sizeof(buffer), f_in); + format = GetInputFormatFromBytes(buffer, nread); + + if (fseek(f_in, 0, SEEK_SET)) { + return HAIO_TYPE_NULL; + } + + if (format != HAIO_TYPE_NULL) { + return format; + } + + return GetFormatFromPath(path); +} + +static int PipelineAddConvertToken(haio_pipeline_t *pipe, char *token) +{ + if (strcmp(token, "-crop") == 0) { + return PipelineStepAdd(pipe, HAIO_TYPE_FILTER_CROP); + } + + printf("[error] unknown token: %s\n", token); + return 1; +} + +static int PipelineParseConvertArgs( + haio_pipeline_t *pipe, + int argc, + char* argv[], + haio_type_t input_format, + haio_type_t output_format +) +{ + PipelineBegin(pipe, HAIO_TYPE_BUFFER); + + if (PipelineStepAdd(pipe, input_format)) { + printf("[error] %s\n", GetPipelineError(pipe)); + return 1; + } + + for (int i = 2; i < argc - 1; i++) { + if (PipelineAddConvertToken(pipe, argv[i])) { + if (PipelineHasError(pipe)) { + printf("[error] %s\n", GetPipelineError(pipe)); + } + return 1; + } + } + + if (PipelineEnd(pipe, output_format)) { + printf("[error] %s\n", GetPipelineError(pipe)); return 1; } - size_t nbytes; - char buffer[4096]; - FILE* f_in = fopen(argv[1], "rb"); - FILE* f_out = fopen(argv[2], "wb"); + return 0; +} + +int FrontendConvertCli(int argc, char* argv[]) +{ + size_t nread; + size_t nwrite; + char buffer[BUFFER_SIZE]; + FILE* f_in; + FILE* f_out; + + haio_type_t input_format; + haio_type_t output_format; haio_pipeline_t pipe; - PipelineBegin(&pipe, HAIO_TYPE_BUFFER); - PipelineStepAdd(&pipe, HAIO_TYPE_IMG_PNG); - //PipelineStepAdd(&pipe, HAIO_TYPE_FILTER_CROP); - PipelineEnd(&pipe, HAIO_TYPE_IMG_PPM); - while(PipelineIsRunning(&pipe)) { - nbytes = fread(buffer, sizeof(char), sizeof(buffer), f_in); - nbytes = PipelineProcess(&pipe, buffer, nbytes, buffer, sizeof(buffer)); - fwrite(buffer, sizeof(char), nbytes, f_out); + if (argc <= 2) { + PrintConvertUsage(); + return 1; + } + + output_format = GetFormatFromPath(argv[argc - 1]); + if (output_format == HAIO_TYPE_NULL) { + printf("[error] unknown output format: %s\n", argv[argc - 1]); + return 1; + } + + f_in = fopen(argv[1], "rb"); + if (!f_in) { + printf("[error] could not open input: %s\n", argv[1]); + return 1; + } + + input_format = GetInputFormat(f_in, argv[1]); + if (input_format == HAIO_TYPE_NULL) { + printf("[error] unknown input format: %s\n", argv[1]); + fclose(f_in); + return 1; + } + + if (PipelineParseConvertArgs(&pipe, argc, argv, input_format, output_format)) { + fclose(f_in); + return 1; } if (PipelineHasError(&pipe)) { - printf("[error] %s", GetPipelineError(&pipe)); + printf("[error] %s\n", GetPipelineError(&pipe)); + fclose(f_in); + return 1; + } + + f_out = fopen(argv[argc - 1], "wb"); + if (!f_out) { + fclose(f_in); + printf("[error] could not open output: %s\n", argv[argc - 1]); + return 1; + } + + while(PipelineIsRunning(&pipe)) { + nread = fread(buffer, sizeof(unsigned char), sizeof(buffer), f_in); + + nwrite = PipelineProcess(&pipe, buffer, nread, buffer, sizeof(buffer)); + + if (nwrite && fwrite(buffer, sizeof(char), nwrite, f_out) != nwrite) { + printf("[error] could not write output: %s\n", argv[argc - 1]); + fclose(f_in); + fclose(f_out); + return 1; + } + } + + if (ferror(f_in)) { + printf("[error] could not read input: %s\n", argv[1]); + fclose(f_in); + fclose(f_out); return 1; } - //printf("feito!"); + if (PipelineHasError(&pipe)) { + printf("[error] %s\n", GetPipelineError(&pipe)); + fclose(f_in); + fclose(f_out); + return 1; + } fclose(f_in); fclose(f_out); From 334351f77879f2081be6947aaed50b9698c197a5 Mon Sep 17 00:00:00 2001 From: guilhhotina Date: Fri, 8 May 2026 10:30:00 -0300 Subject: [PATCH 03/11] fix: align codec path test with decode flow --- tests/test_get_codec_workers_from_formats.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_get_codec_workers_from_formats.c b/tests/test_get_codec_workers_from_formats.c index 9b8c8f2..c30ae52 100644 --- a/tests/test_get_codec_workers_from_formats.c +++ b/tests/test_get_codec_workers_from_formats.c @@ -11,18 +11,22 @@ int main() { - haio_worker_func_t *workers; + haio_worker_func_t workers[10]; haio_type_t output; { + output = HAIO_TYPE_NULL; assert(GetCodecWorkersFromFormats(HAIO_TYPE_BUFFER, HAIO_TYPE_IMG_PNG, NULL, 0, &output)); assert(output == HAIO_TYPE_IMG_RGBA8); } { - assert(GetCodecWorkersFromFormats(HAIO_TYPE_IMG_PNG, HAIO_TYPE_IMG_PPM, workers, 10, &output)); + output = HAIO_TYPE_NULL; + assert(GetCodecWorkersFromFormats(HAIO_TYPE_IMG_RGBA8, HAIO_TYPE_IMG_PPM, workers, 10, &output)); assert(output == HAIO_TYPE_IMG_PPM); } { - assert(GetCodecWorkersFromFormats(HAIO_TYPE_IMG_PNG, HAIO_TYPE_IMG_Y4M420, NULL, 0, NULL)); + output = HAIO_TYPE_NULL; + assert(GetCodecWorkersFromFormats(HAIO_TYPE_BUFFER, HAIO_TYPE_IMG_Y4M420, NULL, 0, &output)); + assert(output == HAIO_TYPE_IMG_Y4M420); } return 0; } From 3790f184a5faafcad6953fe189517119198f0e9e Mon Sep 17 00:00:00 2001 From: guilhhotina Date: Fri, 8 May 2026 20:08:16 -0300 Subject: [PATCH 04/11] feat: tokenize convert commands --- src/Frontend_Convert/cli.c | 176 ++++++-------- src/Frontend_Convert/convert.h | 60 +++++ src/Frontend_Convert/format.c | 139 +++++++++++ src/Frontend_Convert/pipeline_builder.c | 81 +++++++ src/Frontend_Convert/tokenize.c | 307 ++++++++++++++++++++++++ 5 files changed, 665 insertions(+), 98 deletions(-) create mode 100644 src/Frontend_Convert/convert.h create mode 100644 src/Frontend_Convert/format.c create mode 100644 src/Frontend_Convert/pipeline_builder.c create mode 100644 src/Frontend_Convert/tokenize.c diff --git a/src/Frontend_Convert/cli.c b/src/Frontend_Convert/cli.c index d302aba..13427b7 100644 --- a/src/Frontend_Convert/cli.c +++ b/src/Frontend_Convert/cli.c @@ -4,104 +4,97 @@ #include "haio.h" #include "haio/functions.h" +#include "convert.h" #define BUFFER_SIZE 4096 -#define FORMAT_PROBE_SIZE 16 static void PrintConvertUsage(void) { printf("usage:\n"); - printf("./img convert input.png [options] output.ppm\n"); + printf("haio convert input.png [options] output.ppm\n"); + printf("haio convert png:- output.ppm\n"); printf("\n"); printf("options:\n"); printf(" -crop crop image using the default crop filter\n"); } -static haio_type_t GetFormatFromPath(char *path) +static void PrintConvertBuildError(convert_command_t *cmd, haio_pipeline_t *pipe) { - char *ext = GetExtensionFromString(path); - - if (!ext) { - return HAIO_TYPE_NULL; + if (cmd->error.message) { + convert_print_error(cmd->error); + return; } - return GetFormatFromExtension(ext); -} - -static haio_type_t GetInputFormatFromBytes(const unsigned char *buffer, size_t len) -{ - static const unsigned char png_signature[] = { - 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' - }; - - if (len >= sizeof(png_signature) && - memcmp(buffer, png_signature, sizeof(png_signature)) == 0) { - return HAIO_TYPE_IMG_PNG; + if (PipelineHasError(pipe)) { + printf("[error] %s\n", GetPipelineError(pipe)); } - - return HAIO_TYPE_NULL; } -static haio_type_t GetInputFormat(FILE *f_in, char *path) +static int OpenConvertInput(convert_command_t *cmd, FILE **f_in) { - unsigned char buffer[FORMAT_PROBE_SIZE]; - haio_type_t format; - size_t nread; + if (cmd->has_generator) { + *f_in = NULL; + return 0; + } - nread = fread(buffer, sizeof(unsigned char), sizeof(buffer), f_in); - format = GetInputFormatFromBytes(buffer, nread); + if (cmd->input_format_name && cmd->input_format == HAIO_TYPE_NULL) { + convert_print_unsupported_format("input", cmd->input_format_name, cmd->input_path); + return 1; + } - if (fseek(f_in, 0, SEEK_SET)) { - return HAIO_TYPE_NULL; + if (strcmp(cmd->input_path, "-") == 0) { + *f_in = stdin; + return 0; } - if (format != HAIO_TYPE_NULL) { - return format; + *f_in = fopen(cmd->input_path, "rb"); + if (!*f_in) { + printf("[error] could not open input: %s\n", cmd->input_path); + return 1; } - return GetFormatFromPath(path); -} + if (!cmd->input_format_name) { + cmd->input_format = convert_input_format_from_file(*f_in, cmd->input_path); + } -static int PipelineAddConvertToken(haio_pipeline_t *pipe, char *token) -{ - if (strcmp(token, "-crop") == 0) { - return PipelineStepAdd(pipe, HAIO_TYPE_FILTER_CROP); + if (cmd->input_format == HAIO_TYPE_NULL) { + convert_print_unsupported_format("input", cmd->input_format_name, cmd->input_path); + fclose(*f_in); + *f_in = NULL; + return 1; } - printf("[error] unknown token: %s\n", token); - return 1; + return 0; } -static int PipelineParseConvertArgs( - haio_pipeline_t *pipe, - int argc, - char* argv[], - haio_type_t input_format, - haio_type_t output_format -) +static int OpenConvertOutput(convert_command_t *cmd, FILE **f_out) { - PipelineBegin(pipe, HAIO_TYPE_BUFFER); + if (cmd->output_is_stdout) { + *f_out = stdout; + return 0; + } - if (PipelineStepAdd(pipe, input_format)) { - printf("[error] %s\n", GetPipelineError(pipe)); + *f_out = fopen(cmd->output_path, "wb"); + if (!*f_out) { + printf("[error] could not open output: %s\n", cmd->output_path); return 1; } - for (int i = 2; i < argc - 1; i++) { - if (PipelineAddConvertToken(pipe, argv[i])) { - if (PipelineHasError(pipe)) { - printf("[error] %s\n", GetPipelineError(pipe)); - } - return 1; - } - } + return 0; +} - if (PipelineEnd(pipe, output_format)) { - printf("[error] %s\n", GetPipelineError(pipe)); - return 1; +static void CloseConvertInput(FILE *f_in) +{ + if (f_in && f_in != stdin) { + fclose(f_in); } +} - return 0; +static void CloseConvertOutput(FILE *f_out) +{ + if (f_out && f_out != stdout) { + fclose(f_out); + } } int FrontendConvertCli(int argc, char* argv[]) @@ -110,11 +103,10 @@ int FrontendConvertCli(int argc, char* argv[]) size_t nwrite; char buffer[BUFFER_SIZE]; - FILE* f_in; - FILE* f_out; + FILE* f_in = NULL; + FILE* f_out = NULL; - haio_type_t input_format; - haio_type_t output_format; + convert_command_t cmd; haio_pipeline_t pipe; if (argc <= 2) { @@ -122,72 +114,60 @@ int FrontendConvertCli(int argc, char* argv[]) return 1; } - output_format = GetFormatFromPath(argv[argc - 1]); - if (output_format == HAIO_TYPE_NULL) { - printf("[error] unknown output format: %s\n", argv[argc - 1]); + if (convert_tokenize_args(&cmd, argc, argv)) { + convert_print_error(cmd.error); return 1; } - f_in = fopen(argv[1], "rb"); - if (!f_in) { - printf("[error] could not open input: %s\n", argv[1]); + if (cmd.output_format == HAIO_TYPE_NULL) { + convert_print_unsupported_format("output", cmd.output_format_name, argv[argc - 1]); return 1; } - input_format = GetInputFormat(f_in, argv[1]); - if (input_format == HAIO_TYPE_NULL) { - printf("[error] unknown input format: %s\n", argv[1]); - fclose(f_in); + if (OpenConvertInput(&cmd, &f_in)) { return 1; } - if (PipelineParseConvertArgs(&pipe, argc, argv, input_format, output_format)) { - fclose(f_in); + if (convert_build_pipeline(&cmd, &pipe)) { + PrintConvertBuildError(&cmd, &pipe); + CloseConvertInput(f_in); return 1; } - if (PipelineHasError(&pipe)) { - printf("[error] %s\n", GetPipelineError(&pipe)); - fclose(f_in); - return 1; - } - - f_out = fopen(argv[argc - 1], "wb"); - if (!f_out) { - fclose(f_in); - printf("[error] could not open output: %s\n", argv[argc - 1]); + if (OpenConvertOutput(&cmd, &f_out)) { + CloseConvertInput(f_in); return 1; } while(PipelineIsRunning(&pipe)) { - nread = fread(buffer, sizeof(unsigned char), sizeof(buffer), f_in); + nread = fread(buffer, sizeof(char), sizeof(buffer), f_in); nwrite = PipelineProcess(&pipe, buffer, nread, buffer, sizeof(buffer)); if (nwrite && fwrite(buffer, sizeof(char), nwrite, f_out) != nwrite) { - printf("[error] could not write output: %s\n", argv[argc - 1]); - fclose(f_in); - fclose(f_out); + printf("[error] could not write output: %s\n", cmd.output_path); + CloseConvertInput(f_in); + CloseConvertOutput(f_out); return 1; } } if (ferror(f_in)) { - printf("[error] could not read input: %s\n", argv[1]); - fclose(f_in); - fclose(f_out); + printf("[error] could not read input: %s\n", cmd.input_path); + CloseConvertInput(f_in); + CloseConvertOutput(f_out); return 1; } if (PipelineHasError(&pipe)) { printf("[error] %s\n", GetPipelineError(&pipe)); - fclose(f_in); - fclose(f_out); + CloseConvertInput(f_in); + CloseConvertOutput(f_out); return 1; } - fclose(f_in); - fclose(f_out); + CloseConvertInput(f_in); + CloseConvertOutput(f_out); return 0; } diff --git a/src/Frontend_Convert/convert.h b/src/Frontend_Convert/convert.h new file mode 100644 index 0000000..886f9ca --- /dev/null +++ b/src/Frontend_Convert/convert.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include + +#include "haio.h" + +#define CONVERT_MAX_TOKENS 32 +#define CONVERT_FORMAT_PROBE_SIZE 16 + +typedef enum { + CONVERT_TOKEN_INPUT_FILE, + CONVERT_TOKEN_OUTPUT_FILE, + CONVERT_TOKEN_GENERATOR_XC, + CONVERT_TOKEN_GENERATOR_GRADIENT, + CONVERT_TOKEN_FILTER_CROP, + CONVERT_TOKEN_FILTER_FX +} convert_token_type_t; + +typedef struct { + convert_token_type_t type; + char *value; + char *arg; + haio_type_t format; +} convert_token_t; + +typedef struct { + char *message; + char *token; +} convert_error_t; + +typedef struct { + convert_error_t error; + uint8_t token_count; + bool has_input; + bool has_generator; + bool output_is_stdout; + char *input_path; + char *output_path; + char *input_format_name; + char *output_format_name; + haio_type_t input_format; + haio_type_t output_format; + convert_token_t tokens[CONVERT_MAX_TOKENS]; +} convert_command_t; + +haio_type_t convert_format_from_path(char *path); +haio_type_t convert_input_format_from_bytes(const unsigned char *buffer, size_t len); +haio_type_t convert_input_format_from_file(FILE *f_in, char *path); +void convert_print_unsupported_format(const char *kind, char *format_name, char *fallback); +int convert_tokenize_args(convert_command_t *cmd, int argc, char* argv[]); +int convert_build_pipeline(convert_command_t *cmd, haio_pipeline_t *pipe); +void convert_print_error(convert_error_t error); + +bool convert_slice_iequals(char *txt, size_t len, const char *expected); +char *convert_find_format_separator(char *txt); +haio_type_t convert_format_from_name(char *name, size_t len); +bool convert_known_format_prefix(char *name, size_t len); diff --git a/src/Frontend_Convert/format.c b/src/Frontend_Convert/format.c new file mode 100644 index 0000000..8f8a246 --- /dev/null +++ b/src/Frontend_Convert/format.c @@ -0,0 +1,139 @@ +#include +#include + +#include "haio.h" +#include "haio/functions.h" +#include "convert.h" + +static char ascii_lower(char c) +{ + if ('A' <= c && c <= 'Z') { + return (char)(c + ('a' - 'A')); + } + + return c; +} + +bool convert_slice_iequals(char *txt, size_t len, const char *expected) +{ + if (strlen(expected) != len) { + return false; + } + + for (size_t i = 0; i < len; i++) { + if (ascii_lower(txt[i]) != ascii_lower(expected[i])) { + return false; + } + } + + return true; +} + +char *convert_find_format_separator(char *txt) +{ + char *p = txt; + + while (*p) { + if (*p == '/' || *p == '\\') { + return NULL; + } + + if (*p == ':') { + return p == txt ? NULL : p; + } + + p++; + } + + return NULL; +} + + +bool convert_known_format_prefix(char *name, size_t len) +{ + return convert_slice_iequals(name, len, "png") || + convert_slice_iequals(name, len, "ppm") || + convert_slice_iequals(name, len, "y4m") || + convert_slice_iequals(name, len, "jpg") || + convert_slice_iequals(name, len, "jpeg"); +} + +haio_type_t convert_format_from_name(char *name, size_t len) +{ + if (!name || !len) { + return HAIO_TYPE_NULL; + } + + if (convert_slice_iequals(name, len, "png")) { + return HAIO_TYPE_IMG_PNG; + } + + if (convert_slice_iequals(name, len, "ppm")) { + return HAIO_TYPE_IMG_PPM; + } + + if (convert_slice_iequals(name, len, "y4m")) { + return HAIO_TYPE_IMG_Y4M420; + } + + return HAIO_TYPE_NULL; +} + +haio_type_t convert_format_from_path(char *path) +{ + char *ext = GetExtensionFromString(path); + + if (!ext) { + return HAIO_TYPE_NULL; + } + + return GetFormatFromExtension(ext); +} + +haio_type_t convert_input_format_from_bytes(const unsigned char *buffer, size_t len) +{ + static const unsigned char png_signature[] = { + 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' + }; + + if (len >= sizeof(png_signature) && + memcmp(buffer, png_signature, sizeof(png_signature)) == 0) { + return HAIO_TYPE_IMG_PNG; + } + + return HAIO_TYPE_NULL; +} + +haio_type_t convert_input_format_from_file(FILE *f_in, char *path) +{ + unsigned char buffer[CONVERT_FORMAT_PROBE_SIZE]; + haio_type_t format; + size_t nread; + + nread = fread(buffer, sizeof(unsigned char), sizeof(buffer), f_in); + format = convert_input_format_from_bytes(buffer, nread); + + if (fseek(f_in, 0, SEEK_SET)) { + return HAIO_TYPE_NULL; + } + + if (format != HAIO_TYPE_NULL) { + return format; + } + + return convert_format_from_path(path); +} + +void convert_print_unsupported_format(const char *kind, char *format_name, char *fallback) +{ + char *sep = format_name ? convert_find_format_separator(format_name) : NULL; + + if (sep) { + printf("[error] unsupported %s format: ", kind); + fwrite(format_name, sizeof(char), (size_t)(sep - format_name), stdout); + printf("\n"); + return; + } + + printf("[error] unknown %s format: %s\n", kind, fallback); +} diff --git a/src/Frontend_Convert/pipeline_builder.c b/src/Frontend_Convert/pipeline_builder.c new file mode 100644 index 0000000..7259500 --- /dev/null +++ b/src/Frontend_Convert/pipeline_builder.c @@ -0,0 +1,81 @@ +#include + +#include "haio.h" +#include "haio/functions.h" +#include "convert.h" + +static void set_error(convert_command_t *cmd, char *message, char *token) +{ + cmd->error.message = message; + cmd->error.token = token; +} + +static char *unsupported_token_error(convert_token_type_t type) +{ + static char unsupported_generator[] = "generator inputs are not supported yet"; + static char unsupported_fx[] = "-fx is not supported by the pipeline yet"; + static char unsupported_token[] = "unsupported convert token"; + + switch (type) { + case CONVERT_TOKEN_GENERATOR_XC: + case CONVERT_TOKEN_GENERATOR_GRADIENT: + return unsupported_generator; + case CONVERT_TOKEN_FILTER_FX: + return unsupported_fx; + case CONVERT_TOKEN_INPUT_FILE: + case CONVERT_TOKEN_OUTPUT_FILE: + case CONVERT_TOKEN_FILTER_CROP: + break; + } + + return unsupported_token; +} + +int convert_build_pipeline(convert_command_t *cmd, haio_pipeline_t *pipe) +{ + static char unknown_input_format[] = "unknown input format"; + static char unknown_output_format[] = "unknown output format"; + + if (cmd->has_generator) { + set_error(cmd, unsupported_token_error(CONVERT_TOKEN_GENERATOR_XC), NULL); + return 1; + } + + if (cmd->input_format == HAIO_TYPE_NULL) { + set_error(cmd, unknown_input_format, cmd->input_path); + return 1; + } + + if (cmd->output_format == HAIO_TYPE_NULL) { + set_error(cmd, unknown_output_format, cmd->output_path); + return 1; + } + + PipelineBegin(pipe, HAIO_TYPE_BUFFER); + + if (PipelineStepAdd(pipe, cmd->input_format)) { + return 1; + } + + for (uint8_t i = 0; i < cmd->token_count; i++) { + convert_token_t *token = &cmd->tokens[i]; + + switch (token->type) { + case CONVERT_TOKEN_FILTER_CROP: + if (PipelineStepAdd(pipe, HAIO_TYPE_FILTER_CROP)) { + return 1; + } + break; + case CONVERT_TOKEN_INPUT_FILE: + case CONVERT_TOKEN_OUTPUT_FILE: + break; + case CONVERT_TOKEN_GENERATOR_XC: + case CONVERT_TOKEN_GENERATOR_GRADIENT: + case CONVERT_TOKEN_FILTER_FX: + set_error(cmd, unsupported_token_error(token->type), token->value); + return 1; + } + } + + return PipelineEnd(pipe, cmd->output_format); +} diff --git a/src/Frontend_Convert/tokenize.c b/src/Frontend_Convert/tokenize.c new file mode 100644 index 0000000..91cfdfa --- /dev/null +++ b/src/Frontend_Convert/tokenize.c @@ -0,0 +1,307 @@ +#include +#include +#include +#include + +#include "haio.h" +#include "convert.h" + +static bool string_equals(char *txt, const char *expected) +{ + return strcmp(txt, expected) == 0; +} + +static void convert_command_init(convert_command_t *cmd) +{ + memset(cmd, 0, sizeof(convert_command_t)); +} + +static void convert_set_error(convert_command_t *cmd, char *message, char *token) +{ + cmd->error.message = message; + cmd->error.token = token; +} + +void convert_print_error(convert_error_t error) +{ + if (!error.message) { + return; + } + + if (error.token) { + printf("[error] %s: %s\n", error.message, error.token); + return; + } + + printf("[error] %s\n", error.message); +} + +static bool known_source_prefix(char *name, size_t len) +{ + return convert_slice_iequals(name, len, "xc") || + convert_slice_iequals(name, len, "canvas") || + convert_slice_iequals(name, len, "gradient") || + convert_slice_iequals(name, len, "radial-gradient"); +} + +static int token_add( + convert_command_t *cmd, + convert_token_type_t type, + char *value, + char *arg, + haio_type_t format +) +{ + static char too_many_tokens[] = "too many convert tokens"; + + if (cmd->token_count >= CONVERT_MAX_TOKENS) { + convert_set_error(cmd, too_many_tokens, value); + return 1; + } + + cmd->tokens[cmd->token_count] = (convert_token_t) { + .type = type, + .value = value, + .arg = arg, + .format = format + }; + + cmd->token_count++; + + return 0; +} + +static int tokenize_generator( + convert_command_t *cmd, + convert_token_type_t type, + char *value, + char *size, + char *token +) +{ + static char multiple_inputs[] = "multiple convert inputs are not supported yet"; + + if (cmd->has_input) { + convert_set_error(cmd, multiple_inputs, token); + return 1; + } + + cmd->has_input = true; + cmd->has_generator = true; + + return token_add(cmd, type, value, size, HAIO_TYPE_NULL); +} + +static int tokenize_input_file(convert_command_t *cmd, char *token) +{ + static char multiple_inputs[] = "multiple convert inputs are not supported yet"; + static char missing_format_path[] = "missing path after format prefix"; + char *sep = convert_find_format_separator(token); + haio_type_t format = HAIO_TYPE_NULL; + + if (cmd->has_input) { + convert_set_error(cmd, multiple_inputs, token); + return 1; + } + + cmd->has_input = true; + + if (sep && convert_known_format_prefix(token, (size_t)(sep - token))) { + if (!sep[1]) { + convert_set_error(cmd, missing_format_path, token); + return 1; + } + + format = convert_format_from_name(token, (size_t)(sep - token)); + cmd->input_path = sep + 1; + cmd->input_format_name = token; + cmd->input_format = format; + } else { + cmd->input_path = token; + cmd->input_format = convert_format_from_path(token); + } + + return token_add( + cmd, + CONVERT_TOKEN_INPUT_FILE, + cmd->input_path, + cmd->input_format_name, + cmd->input_format + ); +} + +static int tokenize_output_file(convert_command_t *cmd, char *token) +{ + static char missing_format_path[] = "missing path after format prefix"; + char *sep = convert_find_format_separator(token); + haio_type_t format = HAIO_TYPE_NULL; + + if (sep && convert_known_format_prefix(token, (size_t)(sep - token))) { + if (!sep[1]) { + convert_set_error(cmd, missing_format_path, token); + return 1; + } + + format = convert_format_from_name(token, (size_t)(sep - token)); + cmd->output_path = sep + 1; + cmd->output_format_name = token; + cmd->output_format = format; + } else { + cmd->output_path = token; + cmd->output_format = convert_format_from_path(token); + } + + cmd->output_is_stdout = string_equals(cmd->output_path, "-"); + + return token_add( + cmd, + CONVERT_TOKEN_OUTPUT_FILE, + cmd->output_path, + cmd->output_format_name, + cmd->output_format + ); +} + +static int tokenize_source(convert_command_t *cmd, char *token, char *size) +{ + char *sep = convert_find_format_separator(token); + + if (!sep) { + return tokenize_input_file(cmd, token); + } + + if (convert_slice_iequals(token, (size_t)(sep - token), "xc") || + convert_slice_iequals(token, (size_t)(sep - token), "canvas")) { + return tokenize_generator( + cmd, + CONVERT_TOKEN_GENERATOR_XC, + sep + 1, + size, + token + ); + } + + if (convert_slice_iequals(token, (size_t)(sep - token), "gradient") || + convert_slice_iequals(token, (size_t)(sep - token), "radial-gradient")) { + return tokenize_generator( + cmd, + CONVERT_TOKEN_GENERATOR_GRADIENT, + sep + 1, + size, + token + ); + } + + return tokenize_input_file(cmd, token); +} + +int convert_tokenize_args(convert_command_t *cmd, int argc, char* argv[]) +{ + static char missing_input[] = "missing convert input"; + static char missing_option_arg[] = "missing convert option argument"; + static char unknown_option[] = "unknown convert option"; + static char size_without_generator[] = "-size must be followed by a generator source"; + char *pending_size = NULL; + + convert_command_init(cmd); + + if (argc <= 2) { + convert_set_error(cmd, missing_input, NULL); + return 1; + } + + for (int i = 1; i < argc - 1; i++) { + char *token = argv[i]; + + if (string_equals(token, "-size")) { + if ((i + 1) >= argc - 1) { + convert_set_error(cmd, missing_option_arg, token); + return 1; + } + + pending_size = argv[++i]; + continue; + } + + if (string_equals(token, "-fx")) { + if (pending_size) { + convert_set_error(cmd, size_without_generator, pending_size); + return 1; + } + + if ((i + 1) >= argc - 1) { + convert_set_error(cmd, missing_option_arg, token); + return 1; + } + + if (token_add( + cmd, + CONVERT_TOKEN_FILTER_FX, + argv[++i], + NULL, + HAIO_TYPE_NULL + )) { + return 1; + } + + pending_size = NULL; + continue; + } + + if (string_equals(token, "-crop")) { + if (pending_size) { + convert_set_error(cmd, size_without_generator, pending_size); + return 1; + } + + if (token_add( + cmd, + CONVERT_TOKEN_FILTER_CROP, + NULL, + NULL, + HAIO_TYPE_FILTER_CROP + )) { + return 1; + } + + pending_size = NULL; + continue; + } + + if (token[0] == '-' && !string_equals(token, "-")) { + convert_set_error(cmd, unknown_option, token); + return 1; + } + + if (pending_size) { + char *sep = convert_find_format_separator(token); + + if (!sep || !known_source_prefix(token, (size_t)(sep - token))) { + convert_set_error(cmd, size_without_generator, pending_size); + return 1; + } + } + + if (tokenize_source(cmd, token, pending_size)) { + return 1; + } + + pending_size = NULL; + } + + if (pending_size) { + convert_set_error(cmd, size_without_generator, pending_size); + return 1; + } + + if (!cmd->has_input) { + convert_set_error(cmd, missing_input, NULL); + return 1; + } + + if (tokenize_output_file(cmd, argv[argc - 1])) { + return 1; + } + + return 0; +} From fadfb3c8b49525c4fc4cac6b21ee9f6ca9cf16c1 Mon Sep 17 00:00:00 2001 From: guilhhotina Date: Fri, 8 May 2026 20:08:17 -0300 Subject: [PATCH 05/11] test: cover convert command tokenizer --- CMakeLists.txt | 2 + tests/test_convert_tokenize_args.c | 118 +++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 tests/test_convert_tokenize_args.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 0020e80..cba1f7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -167,6 +167,7 @@ if(HAIO_TEST) set(tests "test_get_format_from_extension,src/Common_String/get_format_from_ext.c" "test_get_codec_workers_from_formats,src/Backend_Workers/codecs.c" + "test_convert_tokenize_args,src/Frontend_Convert/tokenize.c,src/Frontend_Convert/format.c,src/Common_String/get_ext_from_str.c,src/Common_String/get_format_from_ext.c" ) foreach(deps IN LISTS tests) string(REPLACE "," ";${CMAKE_SOURCE_DIR}/" deps "${deps}") @@ -174,6 +175,7 @@ if(HAIO_TEST) list(REMOVE_AT deps 0) add_executable("${tests_name}" "${CMAKE_SOURCE_DIR}/tests/${tests_name}.c;${deps}") target_include_directories(${tests_name} PRIVATE "${CMAKE_SOURCE_DIR}/include;${CMAKE_BINARY_DIR}/include") + add_dependencies(${tests_name} haio_functions haio_codec_paths) add_test(NAME "${tests_name}" COMMAND "${tests_name}") endforeach() endif() diff --git a/tests/test_convert_tokenize_args.c b/tests/test_convert_tokenize_args.c new file mode 100644 index 0000000..46d1c90 --- /dev/null +++ b/tests/test_convert_tokenize_args.c @@ -0,0 +1,118 @@ +#include +#include + +#include "haio.h" +#include "../src/Frontend_Convert/convert.h" + +int main(void) +{ + convert_command_t cmd; + { + char *argv[] = { "convert", "input.png", "-crop", "output.ppm" }; + assert(!convert_tokenize_args(&cmd, 4, argv)); + assert(cmd.token_count == 3); + assert(cmd.tokens[0].type == CONVERT_TOKEN_INPUT_FILE); + assert(cmd.tokens[1].type == CONVERT_TOKEN_FILTER_CROP); + assert(cmd.tokens[2].type == CONVERT_TOKEN_OUTPUT_FILE); + assert(cmd.input_format == HAIO_TYPE_IMG_PNG); + assert(cmd.output_format == HAIO_TYPE_IMG_PPM); + } + { + char *argv[] = { + "convert", "-size", "512x512", "xc:white", "-fx", "j/h", "out.png" + }; + assert(!convert_tokenize_args(&cmd, 7, argv)); + assert(cmd.token_count == 3); + assert(cmd.has_generator); + assert(cmd.tokens[0].type == CONVERT_TOKEN_GENERATOR_XC); + assert(strcmp(cmd.tokens[0].value, "white") == 0); + assert(strcmp(cmd.tokens[0].arg, "512x512") == 0); + assert(cmd.tokens[1].type == CONVERT_TOKEN_FILTER_FX); + assert(strcmp(cmd.tokens[1].value, "j/h") == 0); + assert(cmd.tokens[2].type == CONVERT_TOKEN_OUTPUT_FILE); + assert(cmd.output_format == HAIO_TYPE_IMG_PNG); + } + { + char *argv[] = { "convert", "input.png", "ppm:-" }; + assert(!convert_tokenize_args(&cmd, 3, argv)); + assert(cmd.output_is_stdout); + assert(strcmp(cmd.output_path, "-") == 0); + assert(cmd.output_format == HAIO_TYPE_IMG_PPM); + } + { + char *argv[] = { "convert", "png:-", "ppm:-" }; + assert(!convert_tokenize_args(&cmd, 3, argv)); + assert(strcmp(cmd.input_path, "-") == 0); + assert(cmd.input_format == HAIO_TYPE_IMG_PNG); + assert(cmd.output_is_stdout); + } + { + char *argv[] = { "convert", "PNG:-", "PPM:-" }; + assert(!convert_tokenize_args(&cmd, 3, argv)); + assert(cmd.input_format == HAIO_TYPE_IMG_PNG); + assert(cmd.output_format == HAIO_TYPE_IMG_PPM); + assert(cmd.output_is_stdout); + } + { + char *argv[] = { "convert", "foo:bar.png", "out.ppm" }; + assert(!convert_tokenize_args(&cmd, 3, argv)); + assert(strcmp(cmd.input_path, "foo:bar.png") == 0); + assert(cmd.input_format == HAIO_TYPE_IMG_PNG); + } + { + char *argv[] = { "convert", "input.png", "foo:out.ppm" }; + assert(!convert_tokenize_args(&cmd, 3, argv)); + assert(strcmp(cmd.output_path, "foo:out.ppm") == 0); + assert(cmd.output_format == HAIO_TYPE_IMG_PPM); + } + { + char *argv[] = { "convert", "input.png", "jpeg:-" }; + assert(!convert_tokenize_args(&cmd, 3, argv)); + assert(cmd.output_is_stdout); + assert(strcmp(cmd.output_path, "-") == 0); + assert(cmd.output_format_name != NULL); + assert(cmd.output_format == HAIO_TYPE_NULL); + } + { + char *argv[] = { "convert", "jpeg:renamed.png", "out.ppm" }; + assert(!convert_tokenize_args(&cmd, 3, argv)); + assert(strcmp(cmd.input_path, "renamed.png") == 0); + assert(cmd.input_format_name != NULL); + assert(cmd.input_format == HAIO_TYPE_NULL); + } + { + char *argv[] = { "convert", "png:", "out.ppm" }; + assert(convert_tokenize_args(&cmd, 3, argv)); + assert(cmd.error.message != NULL); + assert(strcmp(cmd.error.token, "png:") == 0); + } + { + char *argv[] = { "convert", "png:-", "ppm:" }; + assert(convert_tokenize_args(&cmd, 3, argv)); + assert(cmd.error.message != NULL); + assert(strcmp(cmd.error.token, "ppm:") == 0); + } + { + char *argv[] = { "convert", "input.png", "-crop", "other.png", "out.ppm" }; + assert(convert_tokenize_args(&cmd, 5, argv)); + assert(cmd.error.message != NULL); + assert(strcmp(cmd.error.token, "other.png") == 0); + } + { + char *argv[] = { "convert", "-size", "out.ppm" }; + assert(convert_tokenize_args(&cmd, 3, argv)); + assert(cmd.error.message != NULL); + } + { + char *argv[] = { "convert", "-size", "512x512", "-crop", "xc:white", "out.ppm" }; + assert(convert_tokenize_args(&cmd, 6, argv)); + assert(cmd.error.message != NULL); + } + { + char *argv[] = { "convert", "input.png", "-wat", "out.ppm" }; + assert(convert_tokenize_args(&cmd, 4, argv)); + assert(cmd.error.message != NULL); + assert(strcmp(cmd.error.token, "-wat") == 0); + } + return 0; +} From 78760681aada50407d9c2f85fc1bef5699fab0b5 Mon Sep 17 00:00:00 2001 From: guilhhotina Date: Mon, 11 May 2026 23:58:01 -0300 Subject: [PATCH 06/11] fix: use defer cleanup in convert cli --- src/Frontend_Convert/cli.c | 55 +++++++++++++++----------------------- 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/src/Frontend_Convert/cli.c b/src/Frontend_Convert/cli.c index 13427b7..5db0ff8 100644 --- a/src/Frontend_Convert/cli.c +++ b/src/Frontend_Convert/cli.c @@ -83,22 +83,9 @@ static int OpenConvertOutput(convert_command_t *cmd, FILE **f_out) return 0; } -static void CloseConvertInput(FILE *f_in) -{ - if (f_in && f_in != stdin) { - fclose(f_in); - } -} - -static void CloseConvertOutput(FILE *f_out) -{ - if (f_out && f_out != stdout) { - fclose(f_out); - } -} - int FrontendConvertCli(int argc, char* argv[]) { + int err = 1; size_t nread; size_t nwrite; char buffer[BUFFER_SIZE]; @@ -111,32 +98,30 @@ int FrontendConvertCli(int argc, char* argv[]) if (argc <= 2) { PrintConvertUsage(); - return 1; + goto defer; } if (convert_tokenize_args(&cmd, argc, argv)) { convert_print_error(cmd.error); - return 1; + goto defer; } if (cmd.output_format == HAIO_TYPE_NULL) { convert_print_unsupported_format("output", cmd.output_format_name, argv[argc - 1]); - return 1; + goto defer; } if (OpenConvertInput(&cmd, &f_in)) { - return 1; + goto defer; } if (convert_build_pipeline(&cmd, &pipe)) { PrintConvertBuildError(&cmd, &pipe); - CloseConvertInput(f_in); - return 1; + goto defer; } if (OpenConvertOutput(&cmd, &f_out)) { - CloseConvertInput(f_in); - return 1; + goto defer; } while(PipelineIsRunning(&pipe)) { @@ -146,28 +131,30 @@ int FrontendConvertCli(int argc, char* argv[]) if (nwrite && fwrite(buffer, sizeof(char), nwrite, f_out) != nwrite) { printf("[error] could not write output: %s\n", cmd.output_path); - CloseConvertInput(f_in); - CloseConvertOutput(f_out); - return 1; + goto defer; } } if (ferror(f_in)) { printf("[error] could not read input: %s\n", cmd.input_path); - CloseConvertInput(f_in); - CloseConvertOutput(f_out); - return 1; + goto defer; } if (PipelineHasError(&pipe)) { printf("[error] %s\n", GetPipelineError(&pipe)); - CloseConvertInput(f_in); - CloseConvertOutput(f_out); - return 1; + goto defer; } - CloseConvertInput(f_in); - CloseConvertOutput(f_out); + err = 0; - return 0; +defer: + if (f_in && f_in != stdin) { + fclose(f_in); + } + + if (f_out && f_out != stdout) { + fclose(f_out); + } + + return err; } From d9444634b6db2b0253f00b72ac6f38ac2686999f Mon Sep 17 00:00:00 2001 From: guilhhotina Date: Mon, 11 May 2026 23:58:01 -0300 Subject: [PATCH 07/11] refactor: move format magic detection to common string --- CMakeLists.txt | 3 +- src/Common_String/get_format_from_magic.c | 18 ++++++++ src/Frontend_Convert/convert.h | 1 - src/Frontend_Convert/format.c | 51 +++++------------------ tests/test_get_format_from_magic.c | 19 +++++++++ 5 files changed, 49 insertions(+), 43 deletions(-) create mode 100644 src/Common_String/get_format_from_magic.c create mode 100644 tests/test_get_format_from_magic.c diff --git a/CMakeLists.txt b/CMakeLists.txt index cba1f7b..fe2cbdb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -166,8 +166,9 @@ if(HAIO_TEST) include(CTest) set(tests "test_get_format_from_extension,src/Common_String/get_format_from_ext.c" + "test_get_format_from_magic,src/Common_String/get_format_from_magic.c" "test_get_codec_workers_from_formats,src/Backend_Workers/codecs.c" - "test_convert_tokenize_args,src/Frontend_Convert/tokenize.c,src/Frontend_Convert/format.c,src/Common_String/get_ext_from_str.c,src/Common_String/get_format_from_ext.c" + "test_convert_tokenize_args,src/Frontend_Convert/tokenize.c,src/Frontend_Convert/format.c,src/Common_String/get_ext_from_str.c,src/Common_String/get_format_from_ext.c,src/Common_String/get_format_from_magic.c" ) foreach(deps IN LISTS tests) string(REPLACE "," ";${CMAKE_SOURCE_DIR}/" deps "${deps}") diff --git a/src/Common_String/get_format_from_magic.c b/src/Common_String/get_format_from_magic.c new file mode 100644 index 0000000..2448828 --- /dev/null +++ b/src/Common_String/get_format_from_magic.c @@ -0,0 +1,18 @@ +#include +#include + +#include "haio.h" + +haio_type_t GetFormatFromMagic(const unsigned char *buffer, size_t len) +{ + static const unsigned char png_signature[] = { + 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' + }; + + if (len >= sizeof(png_signature) && + memcmp(buffer, png_signature, sizeof(png_signature)) == 0) { + return HAIO_TYPE_IMG_PNG; + } + + return HAIO_TYPE_NULL; +} diff --git a/src/Frontend_Convert/convert.h b/src/Frontend_Convert/convert.h index 886f9ca..94a32eb 100644 --- a/src/Frontend_Convert/convert.h +++ b/src/Frontend_Convert/convert.h @@ -47,7 +47,6 @@ typedef struct { } convert_command_t; haio_type_t convert_format_from_path(char *path); -haio_type_t convert_input_format_from_bytes(const unsigned char *buffer, size_t len); haio_type_t convert_input_format_from_file(FILE *f_in, char *path); void convert_print_unsupported_format(const char *kind, char *format_name, char *fallback); int convert_tokenize_args(convert_command_t *cmd, int argc, char* argv[]); diff --git a/src/Frontend_Convert/format.c b/src/Frontend_Convert/format.c index 8f8a246..f79c21e 100644 --- a/src/Frontend_Convert/format.c +++ b/src/Frontend_Convert/format.c @@ -1,3 +1,4 @@ +#include #include #include @@ -5,15 +6,6 @@ #include "haio/functions.h" #include "convert.h" -static char ascii_lower(char c) -{ - if ('A' <= c && c <= 'Z') { - return (char)(c + ('a' - 'A')); - } - - return c; -} - bool convert_slice_iequals(char *txt, size_t len, const char *expected) { if (strlen(expected) != len) { @@ -21,7 +13,7 @@ bool convert_slice_iequals(char *txt, size_t len, const char *expected) } for (size_t i = 0; i < len; i++) { - if (ascii_lower(txt[i]) != ascii_lower(expected[i])) { + if (tolower((unsigned char)txt[i]) != tolower((unsigned char)expected[i])) { return false; } } @@ -51,32 +43,23 @@ char *convert_find_format_separator(char *txt) bool convert_known_format_prefix(char *name, size_t len) { - return convert_slice_iequals(name, len, "png") || - convert_slice_iequals(name, len, "ppm") || - convert_slice_iequals(name, len, "y4m") || - convert_slice_iequals(name, len, "jpg") || - convert_slice_iequals(name, len, "jpeg"); + return convert_format_from_name(name, len) != HAIO_TYPE_NULL; } haio_type_t convert_format_from_name(char *name, size_t len) { + char ext[4] = { 0 }; + if (!name || !len) { return HAIO_TYPE_NULL; } - if (convert_slice_iequals(name, len, "png")) { - return HAIO_TYPE_IMG_PNG; - } - - if (convert_slice_iequals(name, len, "ppm")) { - return HAIO_TYPE_IMG_PPM; - } - - if (convert_slice_iequals(name, len, "y4m")) { - return HAIO_TYPE_IMG_Y4M420; + if (len > sizeof(ext)) { + return HAIO_TYPE_NULL; } - return HAIO_TYPE_NULL; + memcpy(ext, name, len); + return GetFormatFromExtension(ext); } haio_type_t convert_format_from_path(char *path) @@ -90,20 +73,6 @@ haio_type_t convert_format_from_path(char *path) return GetFormatFromExtension(ext); } -haio_type_t convert_input_format_from_bytes(const unsigned char *buffer, size_t len) -{ - static const unsigned char png_signature[] = { - 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' - }; - - if (len >= sizeof(png_signature) && - memcmp(buffer, png_signature, sizeof(png_signature)) == 0) { - return HAIO_TYPE_IMG_PNG; - } - - return HAIO_TYPE_NULL; -} - haio_type_t convert_input_format_from_file(FILE *f_in, char *path) { unsigned char buffer[CONVERT_FORMAT_PROBE_SIZE]; @@ -111,7 +80,7 @@ haio_type_t convert_input_format_from_file(FILE *f_in, char *path) size_t nread; nread = fread(buffer, sizeof(unsigned char), sizeof(buffer), f_in); - format = convert_input_format_from_bytes(buffer, nread); + format = GetFormatFromMagic(buffer, nread); if (fseek(f_in, 0, SEEK_SET)) { return HAIO_TYPE_NULL; diff --git a/tests/test_get_format_from_magic.c b/tests/test_get_format_from_magic.c new file mode 100644 index 0000000..57c2b93 --- /dev/null +++ b/tests/test_get_format_from_magic.c @@ -0,0 +1,19 @@ +#include +#include + +#include "haio.h" +#include "haio/functions.h" + +int main() +{ + static const unsigned char png[] = { + 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' + }; + static const unsigned char ppm[] = { 'P', '6', '\n' }; + + assert(GetFormatFromMagic(png, sizeof(png)) == HAIO_TYPE_IMG_PNG); + assert(GetFormatFromMagic(ppm, sizeof(ppm)) == HAIO_TYPE_NULL); + assert(GetFormatFromMagic(NULL, 0) == HAIO_TYPE_NULL); + + return 0; +} From 87369fdebb03de5c7b5a9523fda218ca91290db4 Mon Sep 17 00:00:00 2001 From: guilhhotina Date: Mon, 11 May 2026 23:58:01 -0300 Subject: [PATCH 08/11] feat: parse crop geometry token --- src/Frontend_Convert/tokenize.c | 13 ++++++++++++- tests/test_convert_tokenize_args.c | 19 ++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Frontend_Convert/tokenize.c b/src/Frontend_Convert/tokenize.c index 91cfdfa..b3a7e36 100644 --- a/src/Frontend_Convert/tokenize.c +++ b/src/Frontend_Convert/tokenize.c @@ -44,6 +44,11 @@ static bool known_source_prefix(char *name, size_t len) convert_slice_iequals(name, len, "radial-gradient"); } +static bool is_crop_geometry(char *token) +{ + return strchr(token, 'x') && strchr(token, '+'); +} + static int token_add( convert_command_t *cmd, convert_token_type_t type, @@ -249,15 +254,21 @@ int convert_tokenize_args(convert_command_t *cmd, int argc, char* argv[]) } if (string_equals(token, "-crop")) { + char *crop = NULL; + if (pending_size) { convert_set_error(cmd, size_without_generator, pending_size); return 1; } + if ((i + 1) < argc - 1 && is_crop_geometry(argv[i + 1])) { + crop = argv[++i]; + } + if (token_add( cmd, CONVERT_TOKEN_FILTER_CROP, - NULL, + crop, NULL, HAIO_TYPE_FILTER_CROP )) { diff --git a/tests/test_convert_tokenize_args.c b/tests/test_convert_tokenize_args.c index 46d1c90..b1fc13a 100644 --- a/tests/test_convert_tokenize_args.c +++ b/tests/test_convert_tokenize_args.c @@ -17,6 +17,15 @@ int main(void) assert(cmd.input_format == HAIO_TYPE_IMG_PNG); assert(cmd.output_format == HAIO_TYPE_IMG_PPM); } + { + char *argv[] = { + "convert", "input.png", "-crop", "10x10+5+5", "output.ppm" + }; + assert(!convert_tokenize_args(&cmd, 5, argv)); + assert(cmd.token_count == 3); + assert(cmd.tokens[1].type == CONVERT_TOKEN_FILTER_CROP); + assert(strcmp(cmd.tokens[1].value, "10x10+5+5") == 0); + } { char *argv[] = { "convert", "-size", "512x512", "xc:white", "-fx", "j/h", "out.png" @@ -66,19 +75,19 @@ int main(void) assert(cmd.output_format == HAIO_TYPE_IMG_PPM); } { - char *argv[] = { "convert", "input.png", "jpeg:-" }; + char *argv[] = { "convert", "input.png", "png:-" }; assert(!convert_tokenize_args(&cmd, 3, argv)); assert(cmd.output_is_stdout); assert(strcmp(cmd.output_path, "-") == 0); assert(cmd.output_format_name != NULL); - assert(cmd.output_format == HAIO_TYPE_NULL); + assert(cmd.output_format == HAIO_TYPE_IMG_PNG); } { - char *argv[] = { "convert", "jpeg:renamed.png", "out.ppm" }; + char *argv[] = { "convert", "png:renamed.bin", "out.ppm" }; assert(!convert_tokenize_args(&cmd, 3, argv)); - assert(strcmp(cmd.input_path, "renamed.png") == 0); + assert(strcmp(cmd.input_path, "renamed.bin") == 0); assert(cmd.input_format_name != NULL); - assert(cmd.input_format == HAIO_TYPE_NULL); + assert(cmd.input_format == HAIO_TYPE_IMG_PNG); } { char *argv[] = { "convert", "png:", "out.ppm" }; From d64db451aaa53df9dc45dcd49bd0db84d545b9e2 Mon Sep 17 00:00:00 2001 From: guilhhotina Date: Tue, 12 May 2026 20:05:53 -0300 Subject: [PATCH 09/11] fix: use project cleanup block in convert cli --- src/Frontend_Convert/cli.c | 173 +++++++++++++++++++++---------------- 1 file changed, 99 insertions(+), 74 deletions(-) diff --git a/src/Frontend_Convert/cli.c b/src/Frontend_Convert/cli.c index 5db0ff8..ffd491d 100644 --- a/src/Frontend_Convert/cli.c +++ b/src/Frontend_Convert/cli.c @@ -32,60 +32,78 @@ static void PrintConvertBuildError(convert_command_t *cmd, haio_pipeline_t *pipe static int OpenConvertInput(convert_command_t *cmd, FILE **f_in) { - if (cmd->has_generator) { - *f_in = NULL; - return 0; - } + int err = 1; - if (cmd->input_format_name && cmd->input_format == HAIO_TYPE_NULL) { - convert_print_unsupported_format("input", cmd->input_format_name, cmd->input_path); - return 1; - } + do { + if (cmd->has_generator) { + *f_in = NULL; + err = 0; + break; + } - if (strcmp(cmd->input_path, "-") == 0) { - *f_in = stdin; - return 0; - } + if (cmd->input_format_name && cmd->input_format == HAIO_TYPE_NULL) { + convert_print_unsupported_format("input", cmd->input_format_name, cmd->input_path); + break; + } - *f_in = fopen(cmd->input_path, "rb"); - if (!*f_in) { - printf("[error] could not open input: %s\n", cmd->input_path); - return 1; - } + if (strcmp(cmd->input_path, "-") == 0) { + *f_in = stdin; + err = 0; + break; + } - if (!cmd->input_format_name) { - cmd->input_format = convert_input_format_from_file(*f_in, cmd->input_path); - } + *f_in = fopen(cmd->input_path, "rb"); + if (!*f_in) { + printf("[error] could not open input: %s\n", cmd->input_path); + break; + } - if (cmd->input_format == HAIO_TYPE_NULL) { - convert_print_unsupported_format("input", cmd->input_format_name, cmd->input_path); - fclose(*f_in); - *f_in = NULL; - return 1; + if (!cmd->input_format_name) { + cmd->input_format = convert_input_format_from_file(*f_in, cmd->input_path); + } + + if (cmd->input_format == HAIO_TYPE_NULL) { + convert_print_unsupported_format("input", cmd->input_format_name, cmd->input_path); + fclose(*f_in); + *f_in = NULL; + break; + } + + err = 0; } + while(0); - return 0; + return err; } static int OpenConvertOutput(convert_command_t *cmd, FILE **f_out) { - if (cmd->output_is_stdout) { - *f_out = stdout; - return 0; - } + int err = 1; + + do { + if (cmd->output_is_stdout) { + *f_out = stdout; + err = 0; + break; + } + + *f_out = fopen(cmd->output_path, "wb"); + if (!*f_out) { + printf("[error] could not open output: %s\n", cmd->output_path); + break; + } - *f_out = fopen(cmd->output_path, "wb"); - if (!*f_out) { - printf("[error] could not open output: %s\n", cmd->output_path); - return 1; + err = 0; } + while(0); - return 0; + return err; } int FrontendConvertCli(int argc, char* argv[]) { int err = 1; + int write_error = 0; size_t nread; size_t nwrite; char buffer[BUFFER_SIZE]; @@ -96,58 +114,65 @@ int FrontendConvertCli(int argc, char* argv[]) convert_command_t cmd; haio_pipeline_t pipe; - if (argc <= 2) { - PrintConvertUsage(); - goto defer; - } + do { + if (argc <= 2) { + PrintConvertUsage(); + break; + } - if (convert_tokenize_args(&cmd, argc, argv)) { - convert_print_error(cmd.error); - goto defer; - } + if (convert_tokenize_args(&cmd, argc, argv)) { + convert_print_error(cmd.error); + break; + } - if (cmd.output_format == HAIO_TYPE_NULL) { - convert_print_unsupported_format("output", cmd.output_format_name, argv[argc - 1]); - goto defer; - } + if (cmd.output_format == HAIO_TYPE_NULL) { + convert_print_unsupported_format("output", cmd.output_format_name, argv[argc - 1]); + break; + } - if (OpenConvertInput(&cmd, &f_in)) { - goto defer; - } + if (OpenConvertInput(&cmd, &f_in)) { + break; + } - if (convert_build_pipeline(&cmd, &pipe)) { - PrintConvertBuildError(&cmd, &pipe); - goto defer; - } + if (convert_build_pipeline(&cmd, &pipe)) { + PrintConvertBuildError(&cmd, &pipe); + break; + } - if (OpenConvertOutput(&cmd, &f_out)) { - goto defer; - } + if (OpenConvertOutput(&cmd, &f_out)) { + break; + } - while(PipelineIsRunning(&pipe)) { - nread = fread(buffer, sizeof(char), sizeof(buffer), f_in); + while(PipelineIsRunning(&pipe)) { + nread = fread(buffer, sizeof(char), sizeof(buffer), f_in); - nwrite = PipelineProcess(&pipe, buffer, nread, buffer, sizeof(buffer)); + nwrite = PipelineProcess(&pipe, buffer, nread, buffer, sizeof(buffer)); - if (nwrite && fwrite(buffer, sizeof(char), nwrite, f_out) != nwrite) { - printf("[error] could not write output: %s\n", cmd.output_path); - goto defer; + if (nwrite && fwrite(buffer, sizeof(char), nwrite, f_out) != nwrite) { + printf("[error] could not write output: %s\n", cmd.output_path); + write_error = 1; + break; + } } - } - if (ferror(f_in)) { - printf("[error] could not read input: %s\n", cmd.input_path); - goto defer; - } + if (write_error) { + break; + } - if (PipelineHasError(&pipe)) { - printf("[error] %s\n", GetPipelineError(&pipe)); - goto defer; - } + if (ferror(f_in)) { + printf("[error] could not read input: %s\n", cmd.input_path); + break; + } - err = 0; + if (PipelineHasError(&pipe)) { + printf("[error] %s\n", GetPipelineError(&pipe)); + break; + } + + err = 0; + } + while(0); -defer: if (f_in && f_in != stdin) { fclose(f_in); } From 8c3630a8bef678a5c6443ebd79201cc73ee28d50 Mon Sep 17 00:00:00 2001 From: guilhhotina Date: Tue, 12 May 2026 20:05:53 -0300 Subject: [PATCH 10/11] fix: parse crop geometry without offsets --- src/Frontend_Convert/tokenize.c | 41 +++++++++++++++++++++++++++++- tests/test_convert_tokenize_args.c | 30 ++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/Frontend_Convert/tokenize.c b/src/Frontend_Convert/tokenize.c index b3a7e36..508342e 100644 --- a/src/Frontend_Convert/tokenize.c +++ b/src/Frontend_Convert/tokenize.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -46,7 +47,45 @@ static bool known_source_prefix(char *name, size_t len) static bool is_crop_geometry(char *token) { - return strchr(token, 'x') && strchr(token, '+'); + char *p = token; + uint8_t offset_count = 0; + + if (!isdigit((unsigned char)*p)) { + return false; + } + + while (isdigit((unsigned char)*p)) { + p++; + } + + if (*p != 'x') { + return false; + } + + p++; + + if (!isdigit((unsigned char)*p)) { + return false; + } + + while (isdigit((unsigned char)*p)) { + p++; + } + + while (*p == '+' || *p == '-') { + offset_count++; + p++; + + if (!isdigit((unsigned char)*p)) { + return false; + } + + while (isdigit((unsigned char)*p)) { + p++; + } + } + + return *p == '\0' && offset_count != 1; } static int token_add( diff --git a/tests/test_convert_tokenize_args.c b/tests/test_convert_tokenize_args.c index b1fc13a..a577e36 100644 --- a/tests/test_convert_tokenize_args.c +++ b/tests/test_convert_tokenize_args.c @@ -26,6 +26,24 @@ int main(void) assert(cmd.tokens[1].type == CONVERT_TOKEN_FILTER_CROP); assert(strcmp(cmd.tokens[1].value, "10x10+5+5") == 0); } + { + char *argv[] = { + "convert", "input.png", "-crop", "10x10", "output.ppm" + }; + assert(!convert_tokenize_args(&cmd, 5, argv)); + assert(cmd.token_count == 3); + assert(cmd.tokens[1].type == CONVERT_TOKEN_FILTER_CROP); + assert(strcmp(cmd.tokens[1].value, "10x10") == 0); + } + { + char *argv[] = { + "convert", "input.png", "-crop", "10x10-5+5", "output.ppm" + }; + assert(!convert_tokenize_args(&cmd, 5, argv)); + assert(cmd.token_count == 3); + assert(cmd.tokens[1].type == CONVERT_TOKEN_FILTER_CROP); + assert(strcmp(cmd.tokens[1].value, "10x10-5+5") == 0); + } { char *argv[] = { "convert", "-size", "512x512", "xc:white", "-fx", "j/h", "out.png" @@ -107,6 +125,18 @@ int main(void) assert(cmd.error.message != NULL); assert(strcmp(cmd.error.token, "other.png") == 0); } + { + char *argv[] = { "convert", "input.png", "-crop", "10x", "out.ppm" }; + assert(convert_tokenize_args(&cmd, 5, argv)); + assert(cmd.error.message != NULL); + assert(strcmp(cmd.error.token, "10x") == 0); + } + { + char *argv[] = { "convert", "input.png", "-crop", "10x10+5", "out.ppm" }; + assert(convert_tokenize_args(&cmd, 5, argv)); + assert(cmd.error.message != NULL); + assert(strcmp(cmd.error.token, "10x10+5") == 0); + } { char *argv[] = { "convert", "-size", "out.ppm" }; assert(convert_tokenize_args(&cmd, 3, argv)); From 5788504eaeb241de8b8e587893eb171298511fcc Mon Sep 17 00:00:00 2001 From: guilhhotina Date: Tue, 12 May 2026 20:05:53 -0300 Subject: [PATCH 11/11] fix: reject unsupported crop geometry --- src/Frontend_Convert/pipeline_builder.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Frontend_Convert/pipeline_builder.c b/src/Frontend_Convert/pipeline_builder.c index 7259500..4a40944 100644 --- a/src/Frontend_Convert/pipeline_builder.c +++ b/src/Frontend_Convert/pipeline_builder.c @@ -14,6 +14,7 @@ static char *unsupported_token_error(convert_token_type_t type) { static char unsupported_generator[] = "generator inputs are not supported yet"; static char unsupported_fx[] = "-fx is not supported by the pipeline yet"; + static char unsupported_crop_geometry[] = "-crop geometry is not supported by the pipeline yet"; static char unsupported_token[] = "unsupported convert token"; switch (type) { @@ -22,9 +23,10 @@ static char *unsupported_token_error(convert_token_type_t type) return unsupported_generator; case CONVERT_TOKEN_FILTER_FX: return unsupported_fx; + case CONVERT_TOKEN_FILTER_CROP: + return unsupported_crop_geometry; case CONVERT_TOKEN_INPUT_FILE: case CONVERT_TOKEN_OUTPUT_FILE: - case CONVERT_TOKEN_FILTER_CROP: break; } @@ -62,6 +64,11 @@ int convert_build_pipeline(convert_command_t *cmd, haio_pipeline_t *pipe) switch (token->type) { case CONVERT_TOKEN_FILTER_CROP: + if (token->value) { + set_error(cmd, unsupported_token_error(token->type), token->value); + return 1; + } + if (PipelineStepAdd(pipe, HAIO_TYPE_FILTER_CROP)) { return 1; }