xtc/src/persistence/project_file.c

227 lines
10 KiB
C
Raw Normal View History

#define _POSIX_C_SOURCE 200809L
#include "../project/project_internal.h"
#include "../graph/graph_internal.h"
#include "xtc/xtc.h"
#include <float.h>
#include <inttypes.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
typedef struct { FILE *file; size_t bytes; char line[512]; char *tokens[12]; size_t count; } reader;
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 > 1048576U) 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;
}
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;
}
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;
}
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);
}
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)
static xtc_result read_project(reader *input, xtc_project *project)
{
uint64_t version, value, next_id, node_count, edge_count;
size_t i, j;
xtc_graph *graph = project->graph;
READ("XTC_PROJECT", 2U); NUM(1U, version);
if (version != 1U) return XTC_ERR_UNSUPPORTED;
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;
READ("NODE", 4U); NUM(1U, value); NUM(2U, version);
if (value >= next_id || value >= SIZE_MAX || version < XTC_NODE_GAIN || version > XTC_NODE_OUTPUT ||
!decode_float(input->tokens[3], &node.value)) return XTC_ERR_FORMAT;
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.value != 0.0f) 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;
}
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;
}
READ("END", 1U);
if (fgetc(input->file) != EOF) return XTC_ERR_FORMAT;
if (ferror(input->file)) return XTC_ERR_IO;
return xtc_project_validate(project);
}
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;
}
static xtc_result write_project(FILE *file, const xtc_project *project)
{
const xtc_graph *graph = project->graph;
size_t i;
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 1\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;
}
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)
if (fprintf(file, "TRACK %" PRIu64 " ", project->tracks[i].id) < 0 || !write_name(file, project->tracks[i].name)) 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;
}
return fputs("END\n", file) == EOF ? XTC_ERR_IO : XTC_OK;
}
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;
}
xtc_result xtc_project_load(xtc_project *project, const char *path)
{
xtc_project *candidate = NULL;
reader input = {0};
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;
result = xtc_project_create(&candidate);
if (result == XTC_OK) result = read_project(&input, candidate);
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;
}
xtc_result xtc_project_save(const xtc_project *project, const char *path)
{
size_t length = path_length(path);
char *temporary;
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; }
result = write_project(file, project);
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);
free(temporary); return result;
}