2026-10-27 00:10:45 +01:00
|
|
|
#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>
|
|
|
|
|
|
2026-10-27 00:31:03 +01:00
|
|
|
typedef struct { FILE *file; size_t bytes; char line[1536]; char *tokens[12]; size_t count; } reader;
|
2026-10-27 00:10:45 +01:00
|
|
|
static xtc_result line(reader *input)
|
|
|
|
|
{
|
|
|
|
|
size_t used = 0U;
|
|
|
|
|
int byte;
|
|
|
|
|
char *cursor;
|
|
|
|
|
input->count = 0U;
|
|
|
|
|
while ((byte = fgetc(input->file)) != EOF) {
|
2026-10-27 00:53:41 +01:00
|
|
|
if (++input->bytes > 4194304U) return XTC_ERR_FORMAT;
|
2026-10-27 00:10:45 +01:00
|
|
|
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)
|
|
|
|
|
{
|
2026-10-27 00:31:03 +01:00
|
|
|
uint64_t version, value, next_id, node_count, edge_count, format_version;
|
2026-10-27 00:10:45 +01:00
|
|
|
size_t i, j;
|
|
|
|
|
xtc_graph *graph = project->graph;
|
|
|
|
|
READ("XTC_PROJECT", 2U); NUM(1U, version);
|
2026-10-27 01:06:57 +01:00
|
|
|
if (version < 1U || version > 4U) return XTC_ERR_UNSUPPORTED;
|
2026-10-27 00:31:03 +01:00
|
|
|
format_version = version;
|
2026-10-27 00:10:45 +01:00
|
|
|
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);
|
2026-10-27 00:53:41 +01:00
|
|
|
if (value >= next_id || value >= SIZE_MAX || version < XTC_NODE_GAIN || version > (format_version == 1U ? XTC_NODE_OUTPUT : format_version == 2U ? XTC_NODE_AUDIO : XTC_NODE_INSTRUMENT) ||
|
2026-10-27 00:10:45 +01:00
|
|
|
!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;
|
2026-10-27 00:53:41 +01:00
|
|
|
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;
|
2026-10-27 00:10:45 +01:00
|
|
|
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;
|
2026-10-27 01:06:57 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2026-10-27 00:10:45 +01:00
|
|
|
}
|
|
|
|
|
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;
|
2026-10-27 00:53:41 +01:00
|
|
|
if (format_version >= 2U) {
|
2026-10-27 00:31:03 +01:00
|
|
|
size_t k, length;
|
|
|
|
|
READ("ASSET", 2U);
|
2026-10-27 00:53:41 +01:00
|
|
|
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;
|
2026-10-27 00:31:03 +01:00
|
|
|
}
|
2026-10-27 00:53:41 +01:00
|
|
|
}
|
|
|
|
|
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;
|
2026-10-27 00:31:03 +01:00
|
|
|
}
|
2026-10-27 00:10:45 +01:00
|
|
|
}
|
2026-10-27 01:06:57 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-27 00:10:45 +01:00
|
|
|
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;
|
2026-10-27 01:06:57 +01:00
|
|
|
if (fprintf(file, "XTC_PROJECT 4\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n",
|
2026-10-27 00:10:45 +01:00
|
|
|
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;
|
2026-10-27 01:06:57 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2026-10-27 00:10:45 +01:00
|
|
|
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;
|
2026-10-27 00:31:03 +01:00
|
|
|
if (fputs("ASSET ", file) == EOF) return XTC_ERR_IO;
|
|
|
|
|
if (clip->asset_path[0]) {
|
|
|
|
|
if (!write_name(file, clip->asset_path)) return XTC_ERR_IO;
|
|
|
|
|
} else if (fputs("-\n", file) == EOF) return XTC_ERR_IO;
|
2026-10-27 00:53:41 +01:00
|
|
|
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;
|
2026-10-27 00:10:45 +01:00
|
|
|
}
|
2026-10-27 01:06:57 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2026-10-27 00:10:45 +01:00
|
|
|
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;
|
|
|
|
|
}
|