#define STRVIEW_IMPLEMENTATION
#include "strview.h"
#include "parser.h"
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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", "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",
};
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"))) {
strview style;
bool style_quoted = false;
if (!next_token(¤t, &style, &style_quoted, error, line) || !style_quoted) {
fail(error, line, "burn style must be quoted%s", "");
goto failure;
}
instruction.kind = INST_BURN_STYLE;
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};
}