#define STRVIEW_IMPLEMENTATION #include "strview.h" #include "parser.h" #include #include #include #include #include static const char* operation_names[] = { "dup", "drop", "swap", "over", "rot", "get-video", "get-audio", "get-subtitles", "split", "video-at", "audio-at", "subtitles-at", "take-video", "take-audio", "take-subtitles", "drop-video", "drop-audio", "drop-subtitles", "add-video", "add-audio", "add-subtitles", "replace-video", "replace-audio", "replace-subtitles", "move-video", "move-audio", "move-subtitles", "length", "slice", "take", "concat", "delay", "mux", "attach-subtitles", "video-to-combined", "burn-subtitles", "set-language", "set-title", "set-default", "set-forced", "chapter", "add-chapter", "drop-chapter", "move-chapter", "add-attachment", "drop-attachment", "move-attachment", "set-tag", "volume", "mix", "silence", "speed", "fade-in", "fade-out", "add", "sub", "mul", "div", "eq", "ne", "lt", "le", "gt", "ge", "and", "or", "not", "if", "else", "end", "begin", "while", "repeat", }; const char* type_name(ValueType type) { static const char* names[] = { "VIDEO", "AUDIO", "COMBINED", "SUBTITLES", "BURN_STYLE", "INDEX", "DURATION", "SCALAR", "BOOL", "STRING", "CHAPTER", "ATTACHMENT", }; return names[type]; } const char* operation_name(Operation operation) { return operation_names[operation]; } static bool fail(Error* error, int line, const char* format, const char* arg) { error->line = line; snprintf(error->message, sizeof(error->message), format, arg); return false; } static char* copy_view(Arena* arena, strview view) { char* copy = arena_alloc(arena, view.len + 1); if (copy == NULL) { return NULL; } memcpy(copy, view.data, view.len); copy[view.len] = '\0'; return copy; } static bool push_instruction(Program* program, Instruction instruction) { if (program->count == program->capacity) { size_t new_capacity = program->capacity == 0 ? 32 : program->capacity * 2; Instruction* items = arena_alloc_aligned( &program->arena, new_capacity * sizeof(*items), _Alignof(Instruction)); if (items == NULL) { return false; } if (program->items != NULL) { memcpy(items, program->items, program->count * sizeof(*items)); } program->items = items; program->capacity = new_capacity; } program->items[program->count++] = instruction; return true; } static bool next_token(strview* input, strview* token, bool* quoted, Error* error, int line) { sv_trim_left(input); *quoted = false; if (input->len == 0) { return false; } if (input->data[0] == '"') { *quoted = true; sv_chop_left(input, 1); size_t length = 0; while (length < input->len && input->data[length] != '"') { if (input->data[length] == '\\') { return fail(error, line, "escapes are not supported in quoted paths%s", ""); } length++; } if (length == input->len) { return fail(error, line, "unterminated quoted string%s", ""); } *token = sv_chop_left(input, length); sv_chop_left(input, 1); return true; } size_t length = 0; while (length < input->len && !sv_space_predicate(input->data[length])) { length++; } *token = sv_chop_left(input, length); return true; } static bool parse_number(strview input, double* result) { char buffer[128]; if (input.len == 0 || input.len >= sizeof(buffer)) { return false; } memcpy(buffer, input.data, input.len); buffer[input.len] = '\0'; char* end = NULL; errno = 0; *result = strtod(buffer, &end); return errno == 0 && *end == '\0' && isfinite(*result); } static bool parse_index(strview input, double* result) { if (!parse_number(input, result) || *result < 0 || floor(*result) != *result || *result > (double)SIZE_MAX) { return false; } return true; } static bool parse_duration(strview input, double* result) { if (input.len > 2 && sv_ends_with(input, svlit("ms"))) { sv_chop_right(&input, 2); return parse_number(input, result) && (*result /= 1000.0) >= 0; } if (input.len > 1 && input.data[input.len - 1] == 's') { sv_chop_right(&input, 1); return parse_number(input, result) && *result >= 0; } bool found = false; size_t colon = sv_find_char(input, ':', &found); if (!found) { return false; } strview minutes_view = sv_from_data(input.data, colon); strview seconds_view = sv_from_data(input.data + colon + 1, input.len - colon - 1); double minutes = 0; double seconds = 0; if (!parse_number(minutes_view, &minutes) || !parse_number(seconds_view, &seconds) || minutes < 0 || seconds < 0 || seconds >= 60) { return false; } *result = minutes * 60 + seconds; return true; } static bool source_kind(strview word, SourceKind* kind) { if (sv_equal(word, svlit("video"))) { *kind = SRC_VIDEO; } else if (sv_equal(word, svlit("audio"))) { *kind = SRC_AUDIO; } else if (sv_equal(word, svlit("combined"))) { *kind = SRC_COMBINED; } else if (sv_equal(word, svlit("subtitles"))) { *kind = SRC_SUBTITLES; } else { return false; } return true; } static int find_operation(strview word) { for (int i = 0; i < OP_COUNT; i++) { if (sv_equal(word, sv_from_cstr(operation_names[i]))) { return i; } } return -1; } static char* expand_string(Arena* arena, strview input, size_t argument_count, const char* const* arguments, Error* error, int line) { size_t output_size = 1; for (size_t i = 0; i < input.len;) { if (input.data[i] != '$' || i + 1 == input.len || !sv_numeric_predicate(input.data[i + 1])) { output_size++; i++; continue; } size_t index = 0; i++; if (input.data[i] == '0') { fail(error, line, "invalid script argument in '%s'", copy_view(arena, input)); return NULL; } while (i < input.len && sv_numeric_predicate(input.data[i])) { unsigned digit = (unsigned)(input.data[i] - '0'); if (index > (SIZE_MAX - digit) / 10) { fail(error, line, "invalid script argument in '%s'", copy_view(arena, input)); return NULL; } index = index * 10 + digit; i++; } if (index == 0 || index > argument_count) { fail(error, line, "invalid or missing script argument in '%s'", copy_view(arena, input)); return NULL; } size_t length = sv_from_cstr(arguments[index - 1]).len; if (length > SIZE_MAX - output_size) { fail(error, line, "expanded string is too large%s", ""); return NULL; } output_size += length; } char* output = arena_alloc(arena, output_size); if (output == NULL) { fail(error, line, "out of memory%s", ""); return NULL; } size_t written = 0; for (size_t i = 0; i < input.len;) { if (input.data[i] != '$' || i + 1 == input.len || !sv_numeric_predicate(input.data[i + 1])) { output[written++] = input.data[i++]; continue; } size_t index = 0; i++; while (i < input.len && sv_numeric_predicate(input.data[i])) { index = index * 10 + (unsigned)(input.data[i] - '0'); i++; } strview argument = sv_from_cstr(arguments[index - 1]); memcpy(output + written, argument.data, argument.len); written += argument.len; } output[written] = '\0'; return output; } bool parse_program_with_args(const char* text, size_t argument_count, const char* const* arguments, Program* out, Error* error) { *error = (Error){0}; *out = (Program){.arena = arena_create()}; strview input = sv_from_cstr(text); int line = 1; while (input.len != 0) { strview current = sv_chop_by_char(&input, '\n'); bool in_quotes = false; for (size_t i = 0; i < current.len; i++) { if (current.data[i] == '"') { in_quotes = !in_quotes; } else if (current.data[i] == '#' && !in_quotes) { current.len = i; break; } } sv_trim(¤t); if (current.len == 0) { line++; continue; } strview word; bool quoted = false; if (!next_token(¤t, &word, "ed, error, line)) { goto failure; } if (quoted) { fail(error, line, "unexpected quoted token%s", ""); goto failure; } Instruction instruction = {.line = line}; SourceKind kind; if (sv_equal(word, svlit("burn-style")) || sv_equal(word, svlit("string")) || sv_equal(word, svlit("attachment")) || sv_equal(word, svlit("encoding")) || sv_equal(word, svlit("ffmpeg-args"))) { strview style; bool style_quoted = false; if (!next_token(¤t, &style, &style_quoted, error, line) || !style_quoted) { fail(error, line, "string value must be quoted%s", ""); goto failure; } instruction.kind = sv_equal(word, svlit("burn-style")) ? INST_BURN_STYLE : sv_equal(word, svlit("string")) ? INST_STRING : sv_equal(word, svlit("attachment")) ? INST_ATTACHMENT : sv_equal(word, svlit("encoding")) ? INST_ENCODING : INST_FFMPEG_ARGS; instruction.as.text = expand_string(&out->arena, style, argument_count, arguments, error, line); if (instruction.as.text == NULL) goto failure; } else if (source_kind(word, &kind)) { strview path; bool path_quoted = false; if (!next_token(¤t, &path, &path_quoted, error, line)) { if (error->message[0] == '\0') { fail(error, line, "missing source path%s", ""); } goto failure; } if (!path_quoted) { fail(error, line, "source path must be quoted%s", ""); goto failure; } instruction.kind = INST_SOURCE; instruction.as.source.kind = kind; instruction.as.source.path = expand_string(&out->arena, path, argument_count, arguments, error, line); if (instruction.as.source.path == NULL) { if (error->message[0] == '\0') { fail(error, line, "out of memory%s", ""); } goto failure; } } else if (sv_equal(word, svlit("true")) || sv_equal(word, svlit("false"))) { instruction.kind = INST_BOOL; instruction.as.number = sv_equal(word, svlit("true")) ? 1.0 : 0.0; } else if (sv_equal(word, svlit("duration")) || sv_equal(word, svlit("scalar")) || sv_equal(word, svlit("index"))) { strview literal; bool literal_quoted = false; if (!next_token(¤t, &literal, &literal_quoted, error, line) || literal_quoted) { fail(error, line, "missing or invalid numeric literal%s", ""); goto failure; } instruction.kind = sv_equal(word, svlit("duration")) ? INST_DURATION : sv_equal(word, svlit("scalar")) ? INST_SCALAR : INST_INDEX; bool valid = instruction.kind == INST_DURATION ? parse_duration(literal, &instruction.as.number) : instruction.kind == INST_INDEX ? parse_index(literal, &instruction.as.number) : parse_number(literal, &instruction.as.number); if (!valid) { fail(error, line, instruction.kind == INST_DURATION ? "malformed duration '%s'" : instruction.kind == INST_INDEX ? "malformed index '%s'" : "malformed scalar '%s'", copy_view(&out->arena, literal)); goto failure; } } else { int operation = find_operation(word); if (operation < 0) { fail(error, line, "unknown instruction '%s'", copy_view(&out->arena, word)); goto failure; } instruction.kind = INST_OPERATION; instruction.as.op = (Operation)operation; } sv_trim(¤t); if (current.len != 0) { fail(error, line, "unexpected trailing input '%s'", copy_view(&out->arena, current)); goto failure; } if (!push_instruction(out, instruction)) { fail(error, line, "out of memory%s", ""); goto failure; } line++; } return true; failure: program_destroy(out); return false; } bool parse_program(const char* text, Program* out, Error* error) { return parse_program_with_args(text, 0, NULL, out, error); } void program_destroy(Program* program) { arena_destroy(&program->arena); *program = (Program){0}; }