1
0
Fork 0
forked from hanez/xtc
xtc/src/persistence/project_file.c

683 lines
26 KiB
C

#define _XOPEN_SOURCE 700
#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>
#include <fcntl.h>
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 > XTC_PROJECT_FORMAT_VERSION)
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;
}
project->tracks[i].bus_id = UINT64_MAX;
if (format_version >= 9U) {
READ("ROUTE", 3U);
NUM(1U, value);
if (value > 1) return XTC_ERR_FORMAT;
project->tracks[i].is_bus = (int)value;
NUM(2U, project->tracks[i].bus_id);
}
}
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;
}
clip->gain = 1.0f;
if (format_version >= 8U) {
READ("AUDIO", 4U);
if (!decode_float(input->tokens[1], &clip->gain)) return XTC_ERR_FORMAT;
NUM(2U, clip->fade_in_tick);
NUM(3U, clip->fade_out_tick);
}
}
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 %u\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n",
XTC_PROJECT_FORMAT_VERSION, 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\nROUTE %d %" PRIu64 "\n", gain, pan, t->mute, t->solo, t->is_bus, t->bus_id) < 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;
{
uint32_t bits;
memcpy(&bits, &clip->gain, sizeof(bits));
if (fprintf(file, "AUDIO %08" PRIx32 " %" PRIu64 " %" PRIu64 "\n", bits,
clip->fade_in_tick, clip->fade_out_tick) < 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;
{
char *canonical = realpath(path,NULL);
directory = canonical ? project_folder(canonical) : NULL;
free(canonical);
}
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;
}