#define _XOPEN_SOURCE 700 #define _POSIX_C_SOURCE 200809L #include "../project/project_internal.h" #include "../graph/graph_internal.h" #include "xtc/xtc.h" #include #include #include #include #include #include #include #include #include typedef struct { FILE *file; size_t bytes; char line[1536]; char *tokens[12]; size_t count; } reader; /** * @brief Reads and tokenizes one bounded printable-ASCII project record. * * @param input Reader holding the file and reusable line/token buffers. * @return XTC_OK on success, or an error code describing the failure. */ static xtc_result line(reader *input) { size_t used = 0U; int byte; char *cursor; input->count = 0U; while ((byte = fgetc(input->file)) != EOF) { if (++input->bytes > 4194304U) return XTC_ERR_FORMAT; if (byte == '\n') break; if (byte < 32 || byte > 126 || used == sizeof(input->line) - 1U) return XTC_ERR_FORMAT; input->line[used++] = (char)byte; } if (byte == EOF) return ferror(input->file) ? XTC_ERR_IO : XTC_ERR_FORMAT; input->line[used] = '\0'; cursor = input->line; while (*cursor != '\0') { while (*cursor == ' ') ++cursor; if (*cursor == '\0') break; if (input->count == 12U) return XTC_ERR_FORMAT; input->tokens[input->count++] = cursor; while (*cursor != '\0' && *cursor != ' ') ++cursor; if (*cursor == ' ') *cursor++ = '\0'; } return input->count == 0U ? XTC_ERR_FORMAT : XTC_OK; } /** * @brief Parses an unsigned decimal integer with overflow detection. * * @param text NUL-terminated encoded text. * @param value Receives the decoded value. * @return Nonzero for a valid decimal value, zero for empty, malformed, or overflowing input. */ static int number(const char *text, uint64_t *value) { uint64_t result = 0U; const unsigned char *cursor = (const unsigned char *)text; if (*cursor == '\0') return 0; while (*cursor != '\0') { unsigned int digit; if (*cursor < '0' || *cursor > '9') return 0; digit = (unsigned int)(*cursor++ - '0'); if (result > (UINT64_MAX - digit) / 10U) return 0; result = result * 10U + digit; } *value = result; return 1; } /** * @brief Converts one ASCII hexadecimal digit to its numeric value. * * @param byte ASCII character to decode. * @return Digit value from 0 to 15, or -1 for a non-hexadecimal character. */ static int hex_digit(char byte) { if (byte >= '0' && byte <= '9') return byte - '0'; if (byte >= 'a' && byte <= 'f') return byte - 'a' + 10; if (byte >= 'A' && byte <= 'F') return byte - 'A' + 10; return -1; } /** * @brief Decodes a hexadecimal project name and validates its UTF-8 contents. * * @param text NUL-terminated encoded text. * @param name NUL-terminated name. * @return Nonzero for a valid decoded name, zero for malformed or invalid input. */ static int decode_name(const char *text, char *name) { size_t length = strlen(text), i; if (length == 0U || length % 2U != 0U || length > 126U) return 0; for (i = 0U; i < length / 2U; ++i) { int high = hex_digit(text[i * 2U]), low = hex_digit(text[i * 2U + 1U]); if (high < 0 || low < 0 || (high == 0 && low == 0)) return 0; name[i] = (char)(high * 16 + low); } name[length / 2U] = '\0'; return xtc_project_valid_name(name); } /* Binary-safe nonempty hexadecimal strings, excluding embedded NUL. */ static int decode_bytes(const char *text, char *out, size_t capacity) { size_t n = strlen(text), i; if (!n || n % 2 || n / 2 >= capacity) return 0; for (i = 0; i < n/2; ++i) { int hi = hex_digit(text[2*i]), lo = hex_digit(text[2*i+1]); if (hi < 0 || lo < 0 || (!hi && !lo)) return 0; out[i] = (char)(hi*16+lo); } out[n/2] = 0; return 1; } /** * @brief Decodes eight hexadecimal IEEE-754 bits into a finite float. * * @param text NUL-terminated encoded text. * @param value Receives the decoded value. * @return Nonzero for a finite decoded float, zero for an unsupported representation or invalid input. */ static int decode_float(const char *text, float *value) { uint32_t bits = 0U; size_t i; if (strlen(text) != 8U || (sizeof(*value) != sizeof(bits) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128)) return 0; for (i = 0U; i < 8U; ++i) { int digit = hex_digit(text[i]); if (digit < 0) return 0; bits = (bits << 4U) | (uint32_t)digit; } memcpy(value, &bits, sizeof(bits)); return isfinite(*value); } #define READ(tag, total) \ do { \ xtc_result read_result = line(input); \ if (read_result != XTC_OK) \ return read_result; \ if (input->count != (total) || strcmp(input->tokens[0], (tag)) != 0) \ return XTC_ERR_FORMAT; \ } while (0) #define NUM(index, variable) \ do { \ if (!number(input->tokens[(index)], &(variable))) \ return XTC_ERR_FORMAT; \ } while (0) /** * @brief Parses versioned project records into a temporary project. * * @param input Reader holding the file and reusable line/token buffers. * @param project Project model; borrowed for this call. * @return XTC_OK on success, or an error code describing the failure. */ static xtc_result read_project(reader *input, xtc_project *project, const char *directory) { uint64_t version, value, next_id, node_count, edge_count, format_version; size_t i, j; xtc_graph *graph = project->graph; READ("XTC_PROJECT", 2U); NUM(1U, version); if (version < 1U || version > 7U) return XTC_ERR_UNSUPPORTED; format_version = version; READ("APP", 4U); for (i = 1U; i < 4U; ++i) { NUM(i, value); if (value > UINT_MAX) return XTC_ERR_FORMAT; } READ("TEMPO", 2U); NUM(1U, value); if (value < 20000U || value > 300000U) return XTC_ERR_FORMAT; project->tempo = (unsigned int)value; READ("IDS", 3U); NUM(1U, project->next_track_id); NUM(2U, project->next_clip_id); READ("GRAPH", 5U); NUM(1U, value); NUM(2U, next_id); NUM(3U, node_count); NUM(4U, edge_count); if (value > 1U || next_id > SIZE_MAX || node_count > 64U || edge_count > node_count * node_count) return XTC_ERR_FORMAT; graph->require_output_nodes = (int)value; graph->next_id = (size_t)next_id; for (i = 0U; i < (size_t)node_count; ++i) { xtc_node node = {0}; READ("NODE", 4U); NUM(1U, value); NUM(2U, version); if (value >= next_id || value >= SIZE_MAX || version < XTC_NODE_GAIN || version > (format_version == 1U ? XTC_NODE_OUTPUT : format_version == 2U ? XTC_NODE_AUDIO : format_version < 5U ? XTC_NODE_INSTRUMENT : XTC_NODE_PLUGIN) || !decode_float(input->tokens[3], &node.value)) return XTC_ERR_FORMAT; if (version == XTC_NODE_NATIVE) return XTC_ERR_UNSUPPORTED; node.id = (size_t)value; node.type = (xtc_node_type)version; if (node.type == XTC_NODE_OSCILLATOR && node.value < 0.0f) return XTC_ERR_FORMAT; if ((node.type == XTC_NODE_METER || node.type == XTC_NODE_OUTPUT || node.type == XTC_NODE_AUDIO || node.type == XTC_NODE_INSTRUMENT) && node.value != 0.0f) return XTC_ERR_FORMAT; if (node.type == XTC_NODE_PLUGIN) { xtc_plugin_spec *p = &node.plugin; size_t k; if (node.value != 0) return XTC_ERR_FORMAT; READ("PLUGIN",6U); NUM(1U,value); if (value > UINT32_MAX) return XTC_ERR_FORMAT; p->unique_id = (unsigned int)value; NUM(2U,value); if (value >= 1024) return XTC_ERR_FORMAT; p->index = (unsigned int)value; NUM(3U,value); if (value > 1) return XTC_ERR_FORMAT; p->inputs = (unsigned int)value; NUM(4U,value); if (value > XTC_PLUGIN_CONTROLS) return XTC_ERR_FORMAT; p->control_count = (unsigned int)value; if (!decode_bytes(input->tokens[5],p->label,sizeof(p->label))) return XTC_ERR_FORMAT; READ("MODULE",2U); if (!decode_bytes(input->tokens[1],p->path,sizeof(p->path))) return XTC_ERR_FORMAT; for (k = 0; k < p->control_count; ++k) { READ("CONTROL",3U); NUM(1U,value); if (value >= 64 || !decode_float(input->tokens[2],p->values+k)) return XTC_ERR_FORMAT; p->ports[k] = (unsigned int)value; } p->tempo = project->tempo; if (format_version >= 6) { READ("PLUGIN_AUTOMATION",2U); NUM(1U,value); if (value > XTC_PLUGIN_POINTS) return XTC_ERR_FORMAT; p->point_count = (unsigned int)value; for (k = 0; k < p->point_count; ++k) { xtc_plugin_point *a = p->points+k; READ("PARAM",5U); NUM(1U,a->tick); NUM(2U,value); if (value >= 64) return XTC_ERR_FORMAT; a->port = (unsigned int)value; NUM(3U,value); if (value > 1) return XTC_ERR_FORMAT; a->step = (unsigned int)value; if (!decode_float(input->tokens[4],&a->value)) return XTC_ERR_FORMAT; } } if (xtc_plugin_spec_validate(p) != XTC_OK) return XTC_ERR_FORMAT; } for (j = 0U; j < i; ++j) if (graph->nodes[j].id == node.id) return XTC_ERR_FORMAT; graph->nodes[graph->node_count++] = node; } for (i = 0U; i < (size_t)edge_count; ++i) { uint64_t source, dest; READ("EDGE", 3U); NUM(1U, source); NUM(2U, dest); if (source >= SIZE_MAX || dest >= SIZE_MAX || xtc_graph_connect(graph, (size_t)source, (size_t)dest) != XTC_OK) return XTC_ERR_FORMAT; } READ("TRACKS", 2U); NUM(1U, value); if (value > XTC_PROJECT_MAX_TRACKS) return XTC_ERR_FORMAT; project->track_count = (size_t)value; for (i = 0U; i < project->track_count; ++i) { READ("TRACK", 3U); NUM(1U, project->tracks[i].id); if (!decode_name(input->tokens[2], project->tracks[i].name)) return XTC_ERR_FORMAT; project->tracks[i].gain = 1; if (format_version >= 4U) { READ("MIX", 5U); if (!decode_float(input->tokens[1], &project->tracks[i].gain) || !decode_float(input->tokens[2], &project->tracks[i].pan)) return XTC_ERR_FORMAT; NUM(3U, value); if (value > 1) return XTC_ERR_FORMAT; project->tracks[i].mute = (int)value; NUM(4U, value); if (value > 1) return XTC_ERR_FORMAT; project->tracks[i].solo = (int)value; } } READ("CLIPS", 2U); NUM(1U, value); if (value > XTC_PROJECT_MAX_CLIPS) return XTC_ERR_FORMAT; project->clip_count = (size_t)value; for (i = 0U; i < project->clip_count; ++i) { xtc_clip_info *clip = project->clips + i; READ("CLIP", 8U); NUM(1U, clip->id); NUM(2U, clip->track_id); NUM(3U, clip->start_tick); NUM(4U, clip->length_tick); NUM(5U, clip->offset_tick); NUM(6U, clip->source_length_tick); if (!decode_name(input->tokens[7], clip->name)) return XTC_ERR_FORMAT; if (format_version >= 2U) { size_t k, length; READ("ASSET", 2U); if (strcmp(input->tokens[1], "-")) { length = strlen(input->tokens[1]); if (!length || length % 2 || length >= XTC_ASSET_PATH_BYTES * 2U) return XTC_ERR_FORMAT; for (k = 0; k < length / 2; ++k) { int high = hex_digit(input->tokens[1][k * 2]), low = hex_digit(input->tokens[1][k * 2 + 1]); if (high < 0 || low < 0 || (high == 0 && low == 0)) return XTC_ERR_FORMAT; clip->asset_path[k] = (char)(high * 16 + low); } clip->asset_path[length / 2] = 0; } } if (format_version >= 3U) { READ("MIDI", 2U); NUM(1U, value); if (value > 1) return XTC_ERR_FORMAT; clip->midi = (int)value; } } if (format_version >= 3U) { READ("NOTES", 3U); NUM(1U, project->next_note_id); NUM(2U, value); if (value > XTC_PROJECT_MAX_NOTES) return XTC_ERR_FORMAT; project->note_count = (size_t)value; for (i = 0; i < project->note_count; ++i) { xtc_note_info *n = project->notes + i; READ("NOTE", 8U); NUM(1U, n->id); NUM(2U, n->clip_id); NUM(3U, n->start_tick); NUM(4U, n->length_tick); NUM(5U, value); if (value > 127) return XTC_ERR_FORMAT; n->pitch = (unsigned int)value; NUM(6U, value); if (!value || value > 127) return XTC_ERR_FORMAT; n->velocity = (unsigned int)value; NUM(7U, value); if (value > 15) return XTC_ERR_FORMAT; n->channel = (unsigned int)value; } } if (format_version >= 4U) { READ("AUTOMATION", 2U); NUM(1U, value); if (value > XTC_MAX_AUTOMATION_POINTS) return XTC_ERR_FORMAT; project->automation_count = (size_t)value; for (i = 0; i < project->automation_count; ++i) { xtc_automation_point *a = project->automation + i; READ("POINT", 5U); NUM(1U, a->track_id); NUM(2U, value); if (value > 1) return XTC_ERR_FORMAT; a->parameter = (xtc_automation_parameter)value; NUM(3U, a->tick); if (!decode_float(input->tokens[4], &a->value)) return XTC_ERR_FORMAT; } } READ("END", 1U); if (fgetc(input->file) != EOF) return XTC_ERR_FORMAT; if (ferror(input->file)) return XTC_ERR_IO; if (format_version >= 7) for (i = 0; i < project->clip_count; ++i) { char *asset = project->clips[i].asset_path; if (asset[0] && asset[0] != '/') { size_t root = strlen(directory), length = strlen(asset); if (root+length+2 > XTC_ASSET_PATH_BYTES) return XTC_ERR_CAPACITY; memmove(asset+root+1,asset,length+1); memcpy(asset,directory,root); asset[root] = '/'; } } return xtc_project_validate(project); } /** * @brief Writes a name as hexadecimal bytes without a terminator. * * @param file Open file stream at the required read or write position. * @param name NUL-terminated name. * @return Nonzero if every encoded byte was written, zero on a write failure. */ static int write_name(FILE *file, const char *name) { const unsigned char *cursor = (const unsigned char *)name; while (*cursor != '\0') if (fprintf(file, "%02x", (unsigned int)*cursor++) < 0) return 0; return fputc('\n', file) != EOF; } /** * @brief Serializes project metadata and graph records in the current format. * * @param file Open file stream at the required read or write position. * @param project Project model; borrowed for this call. * @return XTC_OK on success, or an error code describing the failure. */ static xtc_result write_project(FILE *file, const xtc_project *project, const char *directory) { const xtc_graph *graph = project->graph; size_t i; for (i = 0; i < graph->node_count; ++i) if (graph->nodes[i].type == XTC_NODE_NATIVE) return XTC_ERR_UNSUPPORTED; if (sizeof(float) != sizeof(uint32_t) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128) return XTC_ERR_UNSUPPORTED; if (fprintf(file, "XTC_PROJECT 7\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n", XTC_VERSION_MAJOR, XTC_VERSION_MINOR, XTC_VERSION_PATCH, project->tempo, project->next_track_id, project->next_clip_id, graph->require_output_nodes, graph->next_id, graph->node_count, graph->edge_count) < 0) return XTC_ERR_IO; for (i = 0U; i < graph->node_count; ++i) { uint32_t bits; memcpy(&bits, &graph->nodes[i].value, sizeof(bits)); if (fprintf(file, "NODE %zu %u %08" PRIx32 "\n", graph->nodes[i].id, (unsigned int)graph->nodes[i].type, bits) < 0) return XTC_ERR_IO; if (graph->nodes[i].type == XTC_NODE_PLUGIN) { const xtc_plugin_spec *p = &graph->nodes[i].plugin; size_t k; if (xtc_plugin_spec_validate(p) != XTC_OK) return XTC_ERR_FORMAT; if (fprintf(file,"PLUGIN %u %u %u %u ",p->unique_id,p->index,p->inputs,p->control_count) < 0 || !write_name(file,p->label) || fputs("MODULE ",file) == EOF || !write_name(file,p->path)) return XTC_ERR_IO; for (k = 0; k < p->control_count; ++k) { uint32_t value; memcpy(&value,p->values+k,4); if (fprintf(file,"CONTROL %u %08" PRIx32 "\n",p->ports[k],value) < 0) return XTC_ERR_IO; } if (fprintf(file,"PLUGIN_AUTOMATION %u\n",p->point_count) < 0) return XTC_ERR_IO; for (k = 0; k < p->point_count; ++k) { uint32_t value; memcpy(&value,&p->points[k].value,4); if (fprintf(file,"PARAM %" PRIu64 " %u %u %08" PRIx32 "\n",p->points[k].tick,p->points[k].port, p->points[k].step,value) < 0) return XTC_ERR_IO; } } } for (i = 0U; i < graph->edge_count; ++i) if (fprintf(file, "EDGE %zu %zu\n", graph->nodes[graph->edges[i].source].id, graph->nodes[graph->edges[i].destination].id) < 0) return XTC_ERR_IO; if (fprintf(file, "TRACKS %zu\n", project->track_count) < 0) return XTC_ERR_IO; for (i = 0U; i < project->track_count; ++i) { uint32_t gain, pan; const xtc_track_info *t = project->tracks + i; memcpy(&gain, &t->gain, sizeof(gain)); memcpy(&pan, &t->pan, sizeof(pan)); if (fprintf(file, "TRACK %" PRIu64 " ", t->id) < 0 || !write_name(file, t->name) || fprintf(file, "MIX %08" PRIx32 " %08" PRIx32 " %d %d\n", gain, pan, t->mute, t->solo) < 0) return XTC_ERR_IO; } if (fprintf(file, "CLIPS %zu\n", project->clip_count) < 0) return XTC_ERR_IO; for (i = 0U; i < project->clip_count; ++i) { const xtc_clip_info *clip = project->clips + i; if (fprintf(file, "CLIP %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " ", clip->id, clip->track_id, clip->start_tick, clip->length_tick, clip->offset_tick, clip->source_length_tick) < 0 || !write_name(file, clip->name)) return XTC_ERR_IO; if (fputs("ASSET ", file) == EOF) return XTC_ERR_IO; if (clip->asset_path[0]) { const char *asset = clip->asset_path; size_t root = strlen(directory); if (!strncmp(asset,directory,root) && asset[root] == '/' && asset[root+1]) asset += root+1; if (!write_name(file, asset)) return XTC_ERR_IO; } else if (fputs("-\n", file) == EOF) return XTC_ERR_IO; if (fprintf(file, "MIDI %d\n", clip->midi) < 0) return XTC_ERR_IO; } if (fprintf(file, "NOTES %" PRIu64 " %zu\n", project->next_note_id, project->note_count) < 0) return XTC_ERR_IO; for (i = 0; i < project->note_count; ++i) { const xtc_note_info *n = project->notes + i; if (fprintf(file, "NOTE %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %u %u %u\n", n->id, n->clip_id, n->start_tick, n->length_tick, n->pitch, n->velocity, n->channel) < 0) return XTC_ERR_IO; } if (fprintf(file, "AUTOMATION %zu\n", project->automation_count) < 0) return XTC_ERR_IO; for (i = 0; i < project->automation_count; ++i) { uint32_t bits; const xtc_automation_point *a = project->automation + i; memcpy(&bits, &a->value, sizeof(bits)); if (fprintf(file, "POINT %" PRIu64 " %u %" PRIu64 " %08" PRIx32 "\n", a->track_id, (unsigned int)a->parameter, a->tick, bits) < 0) return XTC_ERR_IO; } return fputs("END\n", file) == EOF ? XTC_ERR_IO : XTC_OK; } /** * @brief Measures a file path up to the supported 4096-byte bound. * * @param path NUL-terminated file or asset path. * @return Path length, capped at 4096; zero for NULL. */ static size_t path_length(const char *path) { size_t size = 0U; if (path == NULL) return 0U; while (size < 4096U && path[size] != '\0') ++size; return size == 4096U ? 0U : size; } /* Resolve only the containing folder: media need not exist. Save replaces the * named entry (including a symlink), so do not resolve the final component. */ static char *project_folder(const char *path) { char *copy = malloc(strlen(path)+2), *slash, *folder; if (!copy) return NULL; strcpy(copy,path); slash = strrchr(copy,'/'); if (!slash) strcpy(copy,"."); else if (slash == copy) slash[1] = 0; else *slash = 0; folder = realpath(copy,NULL); free(copy); return folder; } /** * @brief Loads and validates a project before transactionally replacing the current state. * * @param project Project model; borrowed for this call. * @param path NUL-terminated file or asset path. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_load(xtc_project *project, const char *path) { xtc_project *candidate = NULL; reader input = {0}; char *directory; xtc_result result; if (project == NULL || path_length(path) == 0U) return XTC_ERR_INVALID_ARGUMENT; input.file = fopen(path, "rb"); if (input.file == NULL) return XTC_ERR_IO; directory = project_folder(path); if (!directory) { fclose(input.file); return XTC_ERR_IO; } result = xtc_project_create(&candidate); if (result == XTC_OK) result = read_project(&input, candidate, directory); free(directory); if (fclose(input.file) != 0 && result == XTC_OK) result = XTC_ERR_IO; if (result == XTC_OK) { xtc_graph *old = project->graph; *project = *candidate; candidate->graph = old; } xtc_project_destroy(candidate); return result; } /** * @brief Saves a versioned project through a synced temporary file and atomic rename. * * @param project Project model; borrowed for this call. * @param path NUL-terminated file or asset path. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result xtc_project_save(const xtc_project *project, const char *path) { size_t length = path_length(path); char *temporary, *directory; FILE *file; int descriptor; xtc_result result; if (project == NULL || length == 0U) return XTC_ERR_INVALID_ARGUMENT; result = xtc_project_validate(project); if (result != XTC_OK) return result; temporary = malloc(length + 12U); if (temporary == NULL) return XTC_ERR_OUT_OF_MEMORY; memcpy(temporary, path, length); memcpy(temporary + length, ".tmp.XXXXXX", 12U); descriptor = mkstemp(temporary); if (descriptor < 0) { free(temporary); return XTC_ERR_IO; } file = fdopen(descriptor, "wb"); if (file == NULL) { (void)close(descriptor); (void)unlink(temporary); free(temporary); return XTC_ERR_IO; } directory = project_folder(path); result = directory ? write_project(file, project, directory) : XTC_ERR_IO; if (result == XTC_OK && fflush(file) != 0) result = XTC_ERR_IO; if (result == XTC_OK && fsync(descriptor) != 0) result = XTC_ERR_IO; if (fclose(file) != 0 && result == XTC_OK) result = XTC_ERR_IO; if (result == XTC_OK && rename(temporary, path) != 0) result = XTC_ERR_IO; if (result != XTC_OK) (void)unlink(temporary); if (result == XTC_OK) { int folder = open(directory,O_RDONLY); if (folder < 0) result = XTC_ERR_IO; else { if (fsync(folder)) result = XTC_ERR_IO; if (close(folder)) result = XTC_ERR_IO; } } free(directory); free(temporary); return result; }