style(c): expand compact synchronization code

This commit is contained in:
fy59 2026-09-17 14:43:24 +02:00
parent 34c6c0b369
commit 6cd6219ac5
10 changed files with 1893 additions and 564 deletions

View file

@ -17,19 +17,28 @@
typedef struct TrainlogGenerationMtpIo {
TrainlogStatus (*devices)(TrainlogUsbDevice *, size_t, size_t *);
TrainlogStatus (*storages)(unsigned int, unsigned int, TrainlogMtpStorage *, size_t, size_t *);
TrainlogStatus (*children)(unsigned int, unsigned int, uint32_t, uint32_t, TrainlogMtpEntry *, size_t, size_t *);
TrainlogStatus (*ensure_folder)(unsigned int, unsigned int, uint32_t, uint32_t, const char *, uint32_t *, bool *);
TrainlogStatus (*children)(
unsigned int, unsigned int, uint32_t, uint32_t, TrainlogMtpEntry *, size_t, size_t *);
TrainlogStatus (*ensure_folder)(
unsigned int, unsigned int, uint32_t, uint32_t, const char *, uint32_t *, bool *);
TrainlogStatus (*receive)(unsigned int, unsigned int, uint32_t, const char *);
TrainlogStatus (*send)(unsigned int, unsigned int, uint32_t, uint32_t, const char *, const char *, uint32_t *);
TrainlogStatus (*send)(
unsigned int, unsigned int, uint32_t, uint32_t, const char *, const char *, uint32_t *);
TrainlogStatus (*remove)(unsigned int, unsigned int, uint32_t);
TrainlogStatus (*rename)(unsigned int, unsigned int, uint32_t, const char *);
} TrainlogGenerationMtpIo;
/** Mirror remote generation-protocol objects to or from a private local root. */
TrainlogStatus trainlog_generation_mtp_pull(const TrainlogGenerationMtpIo *io,
const char *expected_peer_id, const char *local_root, char *diagnostic, size_t diagnostic_size);
const char *expected_peer_id,
const char *local_root,
char *diagnostic,
size_t diagnostic_size);
TrainlogStatus trainlog_generation_mtp_push(const TrainlogGenerationMtpIo *io,
const char *expected_peer_id, const char *local_root, char *diagnostic, size_t diagnostic_size);
const char *expected_peer_id,
const char *local_root,
char *diagnostic,
size_t diagnostic_size);
const TrainlogGenerationMtpIo *trainlog_generation_mtp_production_io(void);
#endif

View file

@ -27,41 +27,38 @@ static const TileContract contracts[TRAINLOG_DASHBOARD_LAYOUT_TILE_COUNT] = {
{"last-session", 3U, 7U, 3U, 6U},
{"max-records", 3U, 8U, 3U, 7U},
{"muscle-distribution", 5U, 12U, 4U, 9U},
{"cardio-recovery", 5U, 12U, 4U, 8U}
};
{"cardio-recovery", 5U, 12U, 4U, 8U}};
static const TrainlogDashboardTileLayout defaults[TRAINLOG_DASHBOARD_LAYOUT_TILE_COUNT] = {
{"next-session", 0U, 0U, 5U, 4U}, {"activity", 5U, 0U, 7U, 4U},
{"progression", 0U, 4U, 8U, 5U}, {"last-session", 8U, 4U, 4U, 3U},
{"next-session", 0U, 0U, 5U, 4U},
{"activity", 5U, 0U, 7U, 4U},
{"progression", 0U, 4U, 8U, 5U},
{"last-session", 8U, 4U, 4U, 3U},
{"max-records", 8U, 7U, 4U, 3U},
{"muscle-distribution", 0U, 9U, 6U, 5U},
{"cardio-recovery", 6U, 10U, 6U, 4U}
};
{"cardio-recovery", 6U, 10U, 6U, 4U}};
static int contract_index(const char *id)
{
static int contract_index(const char *id) {
size_t index;
for (index = 0U; index < TRAINLOG_DASHBOARD_LAYOUT_TILE_COUNT; ++index)
if (strcmp(id, contracts[index].id) == 0) return (int)index;
if (strcmp(id, contracts[index].id) == 0)
return (int)index;
return -1;
}
void trainlog_dashboard_layout_default(TrainlogDashboardLayout *layout)
{
if (layout == NULL) return;
void trainlog_dashboard_layout_default(TrainlogDashboardLayout *layout) {
if (layout == NULL)
return;
layout->revision = 0U;
(void)memcpy(layout->tiles, defaults, sizeof(defaults));
}
static bool overlap(const TrainlogDashboardTileLayout *a,
const TrainlogDashboardTileLayout *b)
{
return a->x < b->x + b->width && a->x + a->width > b->x &&
a->y < b->y + b->height && a->y + a->height > b->y;
static bool overlap(const TrainlogDashboardTileLayout *a, const TrainlogDashboardTileLayout *b) {
return a->x < b->x + b->width && a->x + a->width > b->x && a->y < b->y + b->height &&
a->y + a->height > b->y;
}
bool trainlog_dashboard_layout_validate(const TrainlogDashboardLayout *layout)
{
bool trainlog_dashboard_layout_validate(const TrainlogDashboardLayout *layout) {
bool seen[TRAINLOG_DASHBOARD_LAYOUT_TILE_COUNT] = {false};
size_t index;
if (layout == NULL || layout->revision > TRAINLOG_DASHBOARD_LAYOUT_MAX_REVISION)
@ -71,43 +68,43 @@ bool trainlog_dashboard_layout_validate(const TrainlogDashboardLayout *layout)
int found = contract_index(tile->id);
const TileContract *contract;
size_t other;
if (found < 0 || seen[(size_t)found]) return false;
if (found < 0 || seen[(size_t)found])
return false;
seen[(size_t)found] = true;
contract = &contracts[(size_t)found];
if (tile->y > TRAINLOG_DASHBOARD_LAYOUT_MAX_Y ||
tile->width < contract->min_width || tile->width > contract->max_width ||
tile->height < contract->min_height || tile->height > contract->max_height ||
if (tile->y > TRAINLOG_DASHBOARD_LAYOUT_MAX_Y || tile->width < contract->min_width ||
tile->width > contract->max_width || tile->height < contract->min_height ||
tile->height > contract->max_height ||
tile->x + tile->width > TRAINLOG_DASHBOARD_LAYOUT_COLUMNS)
return false;
for (other = index + 1U; other < TRAINLOG_DASHBOARD_LAYOUT_TILE_COUNT; ++other)
if (overlap(tile, &layout->tiles[other])) return false;
if (overlap(tile, &layout->tiles[other]))
return false;
}
return true;
}
static bool exact_object(const yyjson_val *value, size_t expected)
{
static bool exact_object(const yyjson_val *value, size_t expected) {
return yyjson_is_obj(value) && yyjson_obj_size(value) == expected;
}
static bool read_u16(yyjson_val *value, uint16_t *output)
{
static bool read_u16(yyjson_val *value, uint16_t *output) {
uint64_t number;
if (!yyjson_is_uint(value)) return false;
if (!yyjson_is_uint(value))
return false;
number = yyjson_get_uint(value);
if (number > UINT16_MAX) return false;
if (number > UINT16_MAX)
return false;
*output = (uint16_t)number;
return true;
}
static bool equals_uint(yyjson_val *value, uint64_t expected)
{
static bool equals_uint(yyjson_val *value, uint64_t expected) {
return yyjson_is_uint(value) && yyjson_get_uint(value) == expected;
}
TrainlogDashboardLayoutResult trainlog_dashboard_layout_parse(
const char *json, size_t size, TrainlogDashboardLayout *layout)
{
TrainlogDashboardLayoutResult
trainlog_dashboard_layout_parse(const char *json, size_t size, TrainlogDashboardLayout *layout) {
yyjson_read_err error;
yyjson_doc *document;
yyjson_val *root;
@ -117,19 +114,22 @@ TrainlogDashboardLayoutResult trainlog_dashboard_layout_parse(
size >= TRAINLOG_DASHBOARD_LAYOUT_JSON_CAPACITY)
return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
document = yyjson_read_opts((char *)json, size, YYJSON_READ_NOFLAG, NULL, &error);
if (document == NULL) return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
if (document == NULL)
return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
root = yyjson_doc_get_root(document);
if (!exact_object(root, 5U) ||
!yyjson_equals_str(yyjson_obj_get(root, "format"), "trainlog-dashboard-layout") ||
!equals_uint(yyjson_obj_get(root, "version"), 1U) ||
!equals_uint(yyjson_obj_get(root, "columns"), 12U) ||
!yyjson_is_uint(yyjson_obj_get(root, "revision"))) {
yyjson_doc_free(document); return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
yyjson_doc_free(document);
return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
}
layout->revision = yyjson_get_uint(yyjson_obj_get(root, "revision"));
tiles = yyjson_obj_get(root, "tiles");
if (!yyjson_is_arr(tiles) || yyjson_arr_size(tiles) != TRAINLOG_DASHBOARD_LAYOUT_TILE_COUNT) {
yyjson_doc_free(document); return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
yyjson_doc_free(document);
return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
}
for (index = 0U; index < TRAINLOG_DASHBOARD_LAYOUT_TILE_COUNT; ++index) {
yyjson_val *tile = yyjson_arr_get(tiles, index);
@ -141,116 +141,166 @@ TrainlogDashboardLayoutResult trainlog_dashboard_layout_parse(
!read_u16(yyjson_obj_get(tile, "y"), &layout->tiles[index].y) ||
!read_u16(yyjson_obj_get(tile, "width"), &layout->tiles[index].width) ||
!read_u16(yyjson_obj_get(tile, "height"), &layout->tiles[index].height)) {
yyjson_doc_free(document); return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
yyjson_doc_free(document);
return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
}
text = yyjson_get_str(id); length = yyjson_get_len(id);
text = yyjson_get_str(id);
length = yyjson_get_len(id);
if (length == 0U || length >= sizeof(layout->tiles[index].id)) {
yyjson_doc_free(document); return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
yyjson_doc_free(document);
return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
}
(void)memcpy(layout->tiles[index].id, text, length);
layout->tiles[index].id[length] = '\0';
}
yyjson_doc_free(document);
return trainlog_dashboard_layout_validate(layout)
? TRAINLOG_DASHBOARD_LAYOUT_OK : TRAINLOG_DASHBOARD_LAYOUT_INVALID;
return trainlog_dashboard_layout_validate(layout) ? TRAINLOG_DASHBOARD_LAYOUT_OK
: TRAINLOG_DASHBOARD_LAYOUT_INVALID;
}
bool trainlog_dashboard_layout_path(char *output, size_t capacity)
{
bool trainlog_dashboard_layout_path(char *output, size_t capacity) {
const char *base = getenv("XDG_CONFIG_HOME");
const char *home;
int length;
if (output == NULL || capacity == 0U) return false;
if (output == NULL || capacity == 0U)
return false;
if (base != NULL && base[0] != '\0')
length = snprintf(output, capacity, "%s/trainlog/web/dashboard-layout-v1.json", base);
else {
home = getenv("HOME");
if (home == NULL || home[0] == '\0') return false;
length = snprintf(output, capacity, "%s/.config/trainlog/web/dashboard-layout-v1.json", home);
if (home == NULL || home[0] == '\0')
return false;
length =
snprintf(output, capacity, "%s/.config/trainlog/web/dashboard-layout-v1.json", home);
}
return length > 0 && (size_t)length < capacity;
}
static bool ensure_private_directories(const char *path)
{
static bool ensure_private_directories(const char *path) {
char web[4096];
char trainlog[4096];
char *last;
struct stat status;
if (path == NULL || strlen(path) >= sizeof(web)) return false;
if (path == NULL || strlen(path) >= sizeof(web))
return false;
(void)snprintf(web, sizeof(web), "%s", path);
last = strrchr(web, '/'); if (last == NULL) return false; *last = '\0';
last = strrchr(web, '/');
if (last == NULL)
return false;
*last = '\0';
(void)snprintf(trainlog, sizeof(trainlog), "%s", web);
last = strrchr(trainlog, '/'); if (last == NULL) return false; *last = '\0';
last = strrchr(trainlog, '/');
if (last == NULL)
return false;
*last = '\0';
/* CONTRACT: only the two Trainlog-owned directories are created/chmodded;
* an existing symlink or non-directory is never followed. */
{
if (mkdir(trainlog, 0700) != 0 && errno != EEXIST) return false;
if (lstat(trainlog, &status) != 0 || !S_ISDIR(status.st_mode)) return false;
if (chmod(trainlog, 0700) != 0) return false;
if (mkdir(trainlog, 0700) != 0 && errno != EEXIST)
return false;
if (lstat(trainlog, &status) != 0 || !S_ISDIR(status.st_mode))
return false;
if (chmod(trainlog, 0700) != 0)
return false;
}
if (mkdir(web, 0700) != 0 && errno != EEXIST) return false;
if (lstat(web, &status) != 0 || !S_ISDIR(status.st_mode)) return false;
if (mkdir(web, 0700) != 0 && errno != EEXIST)
return false;
if (lstat(web, &status) != 0 || !S_ISDIR(status.st_mode))
return false;
return chmod(web, 0700) == 0;
}
static bool sync_parent(const char *path)
{
char parent[4096]; char *slash; int fd; bool ok;
if (strlen(path) >= sizeof(parent)) return false;
static bool sync_parent(const char *path) {
char parent[4096];
char *slash;
int fd;
bool ok;
if (strlen(path) >= sizeof(parent))
return false;
(void)snprintf(parent, sizeof(parent), "%s", path);
slash = strrchr(parent, '/'); if (slash == NULL) return false; *slash = '\0';
slash = strrchr(parent, '/');
if (slash == NULL)
return false;
*slash = '\0';
fd = open(parent, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
if (fd < 0) return false;
ok = fsync(fd) == 0; if (close(fd) != 0) ok = false; return ok;
if (fd < 0)
return false;
ok = fsync(fd) == 0;
if (close(fd) != 0)
ok = false;
return ok;
}
bool trainlog_dashboard_layout_serialize(const TrainlogDashboardLayout *layout,
TrainlogDashboardLayoutSource source, bool include_source,
char *output, size_t capacity, size_t *size)
{
size_t used = 0U; size_t index; int count;
const char *source_name = source == TRAINLOG_DASHBOARD_LAYOUT_PERSISTED
? "persisted" : source == TRAINLOG_DASHBOARD_LAYOUT_INVALID_PERSISTED
? "invalid_persisted" : "default";
TrainlogDashboardLayoutSource source,
bool include_source,
char *output,
size_t capacity,
size_t *size) {
size_t used = 0U;
size_t index;
int count;
const char *source_name = source == TRAINLOG_DASHBOARD_LAYOUT_PERSISTED ? "persisted"
: source == TRAINLOG_DASHBOARD_LAYOUT_INVALID_PERSISTED
? "invalid_persisted"
: "default";
if (!trainlog_dashboard_layout_validate(layout) || output == NULL || size == NULL)
return false;
#define APPEND(...) do { count = snprintf(output + used, capacity - used, __VA_ARGS__); \
if (count < 0 || (size_t)count >= capacity - used) return false; used += (size_t)count; } while (0)
#define APPEND(...) \
do { \
count = snprintf(output + used, capacity - used, __VA_ARGS__); \
if (count < 0 || (size_t)count >= capacity - used) \
return false; \
used += (size_t)count; \
} while (0)
APPEND("{\"format\":\"trainlog-dashboard-layout\",\"version\":1,"
"\"revision\":%llu,\"columns\":12", (unsigned long long)layout->revision);
if (include_source) APPEND(",\"source\":\"%s\"", source_name);
"\"revision\":%llu,\"columns\":12",
(unsigned long long)layout->revision);
if (include_source)
APPEND(",\"source\":\"%s\"", source_name);
APPEND(",\"tiles\":[");
for (index = 0U; index < TRAINLOG_DASHBOARD_LAYOUT_TILE_COUNT; ++index) {
const TrainlogDashboardTileLayout *tile = &layout->tiles[index];
APPEND("%s{\"id\":\"%s\",\"x\":%u,\"y\":%u,\"width\":%u,\"height\":%u}",
index == 0U ? "" : ",", tile->id, tile->x, tile->y,
tile->width, tile->height);
index == 0U ? "" : ",",
tile->id,
tile->x,
tile->y,
tile->width,
tile->height);
}
APPEND("]}\n");
#undef APPEND
*size = used; return true;
*size = used;
return true;
}
TrainlogDashboardLayoutResult trainlog_dashboard_layout_load(
TrainlogDashboardLayout *layout, TrainlogDashboardLayoutSource *source)
{
char path[4096]; char buffer[TRAINLOG_DASHBOARD_LAYOUT_JSON_CAPACITY];
struct stat status; ssize_t count; int fd; TrainlogDashboardLayoutResult result;
TrainlogDashboardLayoutResult
trainlog_dashboard_layout_load(TrainlogDashboardLayout *layout,
TrainlogDashboardLayoutSource *source) {
char path[4096];
char buffer[TRAINLOG_DASHBOARD_LAYOUT_JSON_CAPACITY];
struct stat status;
ssize_t count;
int fd;
TrainlogDashboardLayoutResult result;
if (layout == NULL || source == NULL || !trainlog_dashboard_layout_path(path, sizeof(path)))
return TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
trainlog_dashboard_layout_default(layout); *source = TRAINLOG_DASHBOARD_LAYOUT_DEFAULT;
trainlog_dashboard_layout_default(layout);
*source = TRAINLOG_DASHBOARD_LAYOUT_DEFAULT;
fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
if (fd < 0) {
if (errno == ENOENT || errno == ELOOP) {
if (errno == ELOOP) *source = TRAINLOG_DASHBOARD_LAYOUT_INVALID_PERSISTED;
if (errno == ELOOP)
*source = TRAINLOG_DASHBOARD_LAYOUT_INVALID_PERSISTED;
return TRAINLOG_DASHBOARD_LAYOUT_OK;
}
return TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
}
if (fstat(fd, &status) != 0 || !S_ISREG(status.st_mode) || status.st_size <= 0 ||
status.st_size >= (off_t)sizeof(buffer)) {
(void)close(fd); *source = TRAINLOG_DASHBOARD_LAYOUT_INVALID_PERSISTED;
(void)close(fd);
*source = TRAINLOG_DASHBOARD_LAYOUT_INVALID_PERSISTED;
return TRAINLOG_DASHBOARD_LAYOUT_OK;
}
count = read(fd, buffer, (size_t)status.st_size);
@ -262,45 +312,58 @@ TrainlogDashboardLayoutResult trainlog_dashboard_layout_load(
if (result != TRAINLOG_DASHBOARD_LAYOUT_OK) {
trainlog_dashboard_layout_default(layout);
*source = TRAINLOG_DASHBOARD_LAYOUT_INVALID_PERSISTED;
} else *source = TRAINLOG_DASHBOARD_LAYOUT_PERSISTED;
} else
*source = TRAINLOG_DASHBOARD_LAYOUT_PERSISTED;
return TRAINLOG_DASHBOARD_LAYOUT_OK;
}
static bool write_all(int fd, const char *data, size_t size)
{
static bool write_all(int fd, const char *data, size_t size) {
size_t used = 0U;
while (used < size) { ssize_t count = write(fd, data + used, size - used);
if (count < 0 && errno == EINTR) continue; if (count <= 0) return false; used += (size_t)count; }
while (used < size) {
ssize_t count = write(fd, data + used, size - used);
if (count < 0 && errno == EINTR)
continue;
if (count <= 0)
return false;
used += (size_t)count;
}
return true;
}
TrainlogDashboardLayoutResult trainlog_dashboard_layout_save(
const TrainlogDashboardLayout *layout, uint64_t expected_revision,
TrainlogDashboardLayout *saved)
{
TrainlogDashboardLayout current; TrainlogDashboardLayoutSource source;
TrainlogDashboardLayoutResult trainlog_dashboard_layout_save(const TrainlogDashboardLayout *layout,
uint64_t expected_revision,
TrainlogDashboardLayout *saved) {
TrainlogDashboardLayout current;
TrainlogDashboardLayoutSource source;
char path[4096], temporary[4128], json[TRAINLOG_DASHBOARD_LAYOUT_JSON_CAPACITY];
size_t size; int fd; bool ok;
size_t size;
int fd;
bool ok;
if (layout == NULL || saved == NULL || !trainlog_dashboard_layout_validate(layout) ||
layout->revision != expected_revision || expected_revision >= TRAINLOG_DASHBOARD_LAYOUT_MAX_REVISION)
layout->revision != expected_revision ||
expected_revision >= TRAINLOG_DASHBOARD_LAYOUT_MAX_REVISION)
return TRAINLOG_DASHBOARD_LAYOUT_INVALID;
if (trainlog_dashboard_layout_load(&current, &source) != TRAINLOG_DASHBOARD_LAYOUT_OK)
return TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
if (current.revision != expected_revision) return TRAINLOG_DASHBOARD_LAYOUT_CONFLICT;
*saved = *layout; saved->revision = expected_revision + 1U;
if (!trainlog_dashboard_layout_path(path, sizeof(path)) ||
!ensure_private_directories(path) ||
!trainlog_dashboard_layout_serialize(saved, TRAINLOG_DASHBOARD_LAYOUT_PERSISTED,
false, json, sizeof(json), &size) ||
if (current.revision != expected_revision)
return TRAINLOG_DASHBOARD_LAYOUT_CONFLICT;
*saved = *layout;
saved->revision = expected_revision + 1U;
if (!trainlog_dashboard_layout_path(path, sizeof(path)) || !ensure_private_directories(path) ||
!trainlog_dashboard_layout_serialize(
saved, TRAINLOG_DASHBOARD_LAYOUT_PERSISTED, false, json, sizeof(json), &size) ||
snprintf(temporary, sizeof(temporary), "%s.tmp.XXXXXX", path) >= (int)sizeof(temporary))
return TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
fd = mkstemp(temporary);
if (fd < 0) return TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
if (fd < 0)
return TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
ok = fchmod(fd, 0600) == 0 && fcntl(fd, F_SETFD, FD_CLOEXEC) == 0 &&
write_all(fd, json, size) && fsync(fd) == 0;
if (close(fd) != 0) ok = false;
write_all(fd, json, size) && fsync(fd) == 0;
if (close(fd) != 0)
ok = false;
if (!ok || rename(temporary, path) != 0) {
(void)unlink(temporary); return TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
(void)unlink(temporary);
return TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
}
/* INVARIANT: after rename the valid file is committed. Directory fsync is
* attempted for crash durability, but its failure cannot roll back or make
@ -309,16 +372,18 @@ TrainlogDashboardLayoutResult trainlog_dashboard_layout_save(
return TRAINLOG_DASHBOARD_LAYOUT_OK;
}
TrainlogDashboardLayoutResult trainlog_dashboard_layout_delete(uint64_t expected_revision)
{
TrainlogDashboardLayout current; TrainlogDashboardLayoutSource source; char path[4096];
TrainlogDashboardLayoutResult trainlog_dashboard_layout_delete(uint64_t expected_revision) {
TrainlogDashboardLayout current;
TrainlogDashboardLayoutSource source;
char path[4096];
if (trainlog_dashboard_layout_load(&current, &source) != TRAINLOG_DASHBOARD_LAYOUT_OK)
return TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
if (current.revision != expected_revision) return TRAINLOG_DASHBOARD_LAYOUT_CONFLICT;
if (!trainlog_dashboard_layout_path(path, sizeof(path))) return TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
if (current.revision != expected_revision)
return TRAINLOG_DASHBOARD_LAYOUT_CONFLICT;
if (!trainlog_dashboard_layout_path(path, sizeof(path)))
return TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
if (unlink(path) != 0) {
return errno == ENOENT ? TRAINLOG_DASHBOARD_LAYOUT_OK
: TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
return errno == ENOENT ? TRAINLOG_DASHBOARD_LAYOUT_OK : TRAINLOG_DASHBOARD_LAYOUT_IO_ERROR;
}
(void)sync_parent(path);
return TRAINLOG_DASHBOARD_LAYOUT_OK;

View file

@ -17,42 +17,53 @@ typedef struct Selection {
uint32_t root;
} Selection;
static void diag(char *output, size_t size, const char *value)
{
if (output != NULL && size > 0U) (void)snprintf(output, size, "%s", value);
static void diag(char *output, size_t size, const char *value) {
if (output != NULL && size > 0U)
(void)snprintf(output, size, "%s", value);
}
static bool safe_leaf(const char *name)
{
return name != NULL && name[0] != '\0' && strcmp(name, ".") != 0 &&
strcmp(name, "..") != 0 && strchr(name, '/') == NULL && strchr(name, '\\') == NULL;
static bool safe_leaf(const char *name) {
return name != NULL && name[0] != '\0' && strcmp(name, ".") != 0 && strcmp(name, "..") != 0 &&
strchr(name, '/') == NULL && strchr(name, '\\') == NULL;
}
static TrainlogStatus children(const TrainlogGenerationMtpIo *io, const Selection *selection,
uint32_t parent, TrainlogMtpEntry *entries, size_t *count)
{
return io->children(selection->bus, selection->device, selection->storage,
parent, entries, TRAINLOG_GENERATION_MTP_MAX_CHILDREN, count);
static TrainlogStatus children(const TrainlogGenerationMtpIo *io,
const Selection *selection,
uint32_t parent,
TrainlogMtpEntry *entries,
size_t *count) {
return io->children(selection->bus,
selection->device,
selection->storage,
parent,
entries,
TRAINLOG_GENERATION_MTP_MAX_CHILDREN,
count);
}
static TrainlogStatus one_named(const TrainlogGenerationMtpIo *io, const Selection *selection,
uint32_t parent, const char *name, bool folder, TrainlogMtpEntry *result)
{
static TrainlogStatus one_named(const TrainlogGenerationMtpIo *io,
const Selection *selection,
uint32_t parent,
const char *name,
bool folder,
TrainlogMtpEntry *result) {
TrainlogMtpEntry entries[TRAINLOG_GENERATION_MTP_MAX_CHILDREN];
size_t count = 0U, index, matches = 0U;
TrainlogStatus status = children(io, selection, parent, entries, &count);
if (status != TRAINLOG_STATUS_OK) return status;
if (status != TRAINLOG_STATUS_OK)
return status;
for (index = 0U; index < count; ++index) {
if (entries[index].folder == folder && strcmp(entries[index].name, name) == 0) {
*result = entries[index]; ++matches;
*result = entries[index];
++matches;
}
}
if (matches > 1U) return TRAINLOG_STATUS_CONFLICT;
if (matches > 1U)
return TRAINLOG_STATUS_CONFLICT;
return matches == 1U ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_NOT_FOUND;
}
static bool peer_document_matches(const char *path, const char *expected)
{
static bool peer_document_matches(const char *path, const char *expected) {
int fd = open(path, O_RDONLY | O_CLOEXEC);
char buffer[16385];
ssize_t count;
@ -60,35 +71,46 @@ static bool peer_document_matches(const char *path, const char *expected)
yyjson_val *root, *capabilities, *item;
size_t index, maximum;
bool manifest = false, acknowledgement = false, valid;
if (fd < 0) return false;
if (fd < 0)
return false;
count = read(fd, buffer, sizeof(buffer));
(void)close(fd);
if (count <= 0 || (size_t)count >= sizeof(buffer)) return false;
if (count <= 0 || (size_t)count >= sizeof(buffer))
return false;
buffer[count] = '\0';
document = yyjson_read(buffer, (size_t)count, YYJSON_READ_NOFLAG);
if (document == NULL) return false;
if (document == NULL)
return false;
root = yyjson_doc_get_root(document);
capabilities = yyjson_obj_get(root, "capabilities");
valid = yyjson_is_obj(root) && yyjson_obj_size(root) == 4U &&
yyjson_equals_str(yyjson_obj_get(root, "format"), "trainlog-sync-peer") &&
yyjson_is_uint(yyjson_obj_get(root, "version")) &&
yyjson_get_uint(yyjson_obj_get(root, "version")) == 1U &&
yyjson_equals_str(yyjson_obj_get(root, "peer_id"), expected) &&
yyjson_is_arr(capabilities);
yyjson_equals_str(yyjson_obj_get(root, "format"), "trainlog-sync-peer") &&
yyjson_is_uint(yyjson_obj_get(root, "version")) &&
yyjson_get_uint(yyjson_obj_get(root, "version")) == 1U &&
yyjson_equals_str(yyjson_obj_get(root, "peer_id"), expected) &&
yyjson_is_arr(capabilities);
if (valid) {
yyjson_arr_foreach(capabilities, index, maximum, item) {
if (!yyjson_is_str(item)) { valid = false; break; }
if (yyjson_equals_str(item, "generation-manifest-v1")) manifest = true;
if (yyjson_equals_str(item, "generation-ack-v1")) acknowledgement = true;
if (!yyjson_is_str(item)) {
valid = false;
break;
}
if (yyjson_equals_str(item, "generation-manifest-v1"))
manifest = true;
if (yyjson_equals_str(item, "generation-ack-v1"))
acknowledgement = true;
}
}
yyjson_doc_free(document);
return valid && manifest && acknowledgement;
}
static TrainlogStatus select_peer(const TrainlogGenerationMtpIo *io, const char *expected,
const char *local_root, Selection *output, char *diagnostic, size_t diagnostic_size)
{
static TrainlogStatus select_peer(const TrainlogGenerationMtpIo *io,
const char *expected,
const char *local_root,
Selection *output,
char *diagnostic,
size_t diagnostic_size) {
TrainlogUsbDevice devices[TRAINLOG_GENERATION_MTP_MAX_DEVICES];
TrainlogMtpStorage storages[TRAINLOG_GENERATION_MTP_MAX_STORAGES];
size_t device_count = 0U, storage_count, di, si, matches = 0U;
@ -100,202 +122,337 @@ static TrainlogStatus select_peer(const TrainlogGenerationMtpIo *io, const char
}
status = io->devices(devices, TRAINLOG_GENERATION_MTP_MAX_DEVICES, &device_count);
if (status != TRAINLOG_STATUS_OK || device_count == 0U) {
diag(diagnostic, diagnostic_size, "no MTP device available"); return TRAINLOG_STATUS_NOT_FOUND;
diag(diagnostic, diagnostic_size, "no MTP device available");
return TRAINLOG_STATUS_NOT_FOUND;
}
for (di = 0U; di < device_count; ++di) {
status = io->storages(devices[di].bus_number, devices[di].device_number,
storages, TRAINLOG_GENERATION_MTP_MAX_STORAGES, &storage_count);
if (status != TRAINLOG_STATUS_OK) continue;
status = io->storages(devices[di].bus_number,
devices[di].device_number,
storages,
TRAINLOG_GENERATION_MTP_MAX_STORAGES,
&storage_count);
if (status != TRAINLOG_STATUS_OK)
continue;
for (si = 0U; si < storage_count; ++si) {
Selection candidate = {devices[di].bus_number, devices[di].device_number,
storages[si].storage_id, 0U};
Selection candidate = {
devices[di].bus_number, devices[di].device_number, storages[si].storage_id, 0U};
TrainlogMtpEntry documents, root, peer;
if (one_named(io, &candidate, UINT32_MAX, "Documents", true, &documents) != TRAINLOG_STATUS_OK ||
one_named(io, &candidate, documents.item_id, "Trainlog", true, &root) != TRAINLOG_STATUS_OK) continue;
if (one_named(io, &candidate, UINT32_MAX, "Documents", true, &documents) !=
TRAINLOG_STATUS_OK ||
one_named(io, &candidate, documents.item_id, "Trainlog", true, &root) !=
TRAINLOG_STATUS_OK)
continue;
candidate.root = root.item_id;
if (one_named(io, &candidate, candidate.root, "android-peer-v1.json", false, &peer) != TRAINLOG_STATUS_OK) continue;
if (one_named(io, &candidate, candidate.root, "android-peer-v1.json", false, &peer) !=
TRAINLOG_STATUS_OK)
continue;
(void)unlink(peer_path);
if (peer.size_bytes > 16384U || io->receive(candidate.bus, candidate.device,
peer.item_id, peer_path) != TRAINLOG_STATUS_OK ||
!peer_document_matches(peer_path, expected)) continue;
*output = candidate; ++matches;
if (peer.size_bytes > 16384U ||
io->receive(candidate.bus, candidate.device, peer.item_id, peer_path) !=
TRAINLOG_STATUS_OK ||
!peer_document_matches(peer_path, expected))
continue;
*output = candidate;
++matches;
}
}
(void)unlink(peer_path);
if (matches == 0U) { diag(diagnostic, diagnostic_size, "expected Android peer not found"); return TRAINLOG_STATUS_NOT_FOUND; }
if (matches > 1U) { diag(diagnostic, diagnostic_size, "ambiguous compatible Android peers"); return TRAINLOG_STATUS_CONFLICT; }
if (matches == 0U) {
diag(diagnostic, diagnostic_size, "expected Android peer not found");
return TRAINLOG_STATUS_NOT_FOUND;
}
if (matches > 1U) {
diag(diagnostic, diagnostic_size, "ambiguous compatible Android peers");
return TRAINLOG_STATUS_CONFLICT;
}
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus mkdir_private(const char *path)
{
if (mkdir(path, 0700) == 0 || errno == EEXIST) return TRAINLOG_STATUS_OK;
static TrainlogStatus mkdir_private(const char *path) {
if (mkdir(path, 0700) == 0 || errno == EEXIST)
return TRAINLOG_STATUS_OK;
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
static TrainlogStatus pull_folder(const TrainlogGenerationMtpIo *io, const Selection *selection,
uint32_t parent, const char *local, unsigned int depth, uint64_t *total, size_t *objects)
{
static TrainlogStatus pull_folder(const TrainlogGenerationMtpIo *io,
const Selection *selection,
uint32_t parent,
const char *local,
unsigned int depth,
uint64_t *total,
size_t *objects) {
TrainlogMtpEntry entries[TRAINLOG_GENERATION_MTP_MAX_CHILDREN];
size_t count = 0U, index;
TrainlogStatus status;
if (depth > TRAINLOG_GENERATION_MTP_MAX_DEPTH) return TRAINLOG_STATUS_INVALID_ARGUMENT;
status = mkdir_private(local); if (status != TRAINLOG_STATUS_OK) return status;
status = children(io, selection, parent, entries, &count); if (status != TRAINLOG_STATUS_OK) return status;
if (*objects > TRAINLOG_GENERATION_MTP_MAX_CHILDREN - count) return TRAINLOG_STATUS_INVALID_ARGUMENT;
if (depth > TRAINLOG_GENERATION_MTP_MAX_DEPTH)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
status = mkdir_private(local);
if (status != TRAINLOG_STATUS_OK)
return status;
status = children(io, selection, parent, entries, &count);
if (status != TRAINLOG_STATUS_OK)
return status;
if (*objects > TRAINLOG_GENERATION_MTP_MAX_CHILDREN - count)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
*objects += count;
for (index = 0U; index < count; ++index) {
char path[1024], temporary[1060]; int written;
char path[1024], temporary[1060];
int written;
written = snprintf(path, sizeof(path), "%s/%s", local, entries[index].name);
if (!safe_leaf(entries[index].name) || written < 0 || (size_t)written >= sizeof(path)) return TRAINLOG_STATUS_INVALID_ARGUMENT;
if (!safe_leaf(entries[index].name) || written < 0 || (size_t)written >= sizeof(path))
return TRAINLOG_STATUS_INVALID_ARGUMENT;
if (entries[index].folder) {
status = pull_folder(io, selection, entries[index].item_id, path, depth + 1U, total, objects);
status = pull_folder(
io, selection, entries[index].item_id, path, depth + 1U, total, objects);
} else {
struct stat value;
if (entries[index].size_bytes > TRAINLOG_GENERATION_MTP_MAX_TOTAL_BYTES ||
*total > TRAINLOG_GENERATION_MTP_MAX_TOTAL_BYTES - entries[index].size_bytes) return TRAINLOG_STATUS_INVALID_ARGUMENT;
*total > TRAINLOG_GENERATION_MTP_MAX_TOTAL_BYTES - entries[index].size_bytes)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
*total += entries[index].size_bytes;
written = snprintf(temporary, sizeof(temporary), "%s.part.%ld", path, (long)getpid());
if (written < 0 || (size_t)written >= sizeof(temporary)) return TRAINLOG_STATUS_INVALID_ARGUMENT;
if (written < 0 || (size_t)written >= sizeof(temporary))
return TRAINLOG_STATUS_INVALID_ARGUMENT;
(void)unlink(temporary);
status = io->receive(selection->bus, selection->device, entries[index].item_id, temporary);
if (status == TRAINLOG_STATUS_OK && (stat(temporary, &value) != 0 || value.st_size < 0 ||
(uint64_t)value.st_size != entries[index].size_bytes || rename(temporary, path) != 0)) status = TRAINLOG_STATUS_SYSTEM_ERROR;
if (status != TRAINLOG_STATUS_OK) (void)unlink(temporary);
status =
io->receive(selection->bus, selection->device, entries[index].item_id, temporary);
if (status == TRAINLOG_STATUS_OK &&
(stat(temporary, &value) != 0 || value.st_size < 0 ||
(uint64_t)value.st_size != entries[index].size_bytes ||
rename(temporary, path) != 0))
status = TRAINLOG_STATUS_SYSTEM_ERROR;
if (status != TRAINLOG_STATUS_OK)
(void)unlink(temporary);
}
if (status != TRAINLOG_STATUS_OK) return status;
if (status != TRAINLOG_STATUS_OK)
return status;
}
return TRAINLOG_STATUS_OK;
}
static bool same_files(const char *left, const char *right)
{
static bool same_files(const char *left, const char *right) {
int a = open(left, O_RDONLY | O_CLOEXEC), b = open(right, O_RDONLY | O_CLOEXEC);
char ab[8192], bb[8192]; ssize_t ac = 0, bc; bool same = true;
if (a < 0 || b < 0) same = false;
char ab[8192], bb[8192];
ssize_t ac = 0, bc;
bool same = true;
if (a < 0 || b < 0)
same = false;
while (same && (ac = read(a, ab, sizeof(ab))) > 0) {
bc = read(b, bb, (size_t)ac); if (bc != ac || memcmp(ab, bb, (size_t)ac) != 0) same = false;
bc = read(b, bb, (size_t)ac);
if (bc != ac || memcmp(ab, bb, (size_t)ac) != 0)
same = false;
}
if (same && (ac < 0 || read(b, bb, 1U) != 0)) same = false;
if (a >= 0) (void)close(a); if (b >= 0) (void)close(b); return same;
if (same && (ac < 0 || read(b, bb, 1U) != 0))
same = false;
if (a >= 0)
(void)close(a);
if (b >= 0)
(void)close(b);
return same;
}
static TrainlogStatus push_file(const TrainlogGenerationMtpIo *io, const Selection *selection,
uint32_t parent, const char *local, const char *name)
{
TrainlogMtpEntry existing; TrainlogStatus status;
char comparison[1060], temporary_name[256]; uint32_t uploaded = 0U;
static TrainlogStatus push_file(const TrainlogGenerationMtpIo *io,
const Selection *selection,
uint32_t parent,
const char *local,
const char *name) {
TrainlogMtpEntry existing;
TrainlogStatus status;
char comparison[1060], temporary_name[256];
uint32_t uploaded = 0U;
status = one_named(io, selection, parent, name, false, &existing);
if (status == TRAINLOG_STATUS_OK) {
(void)snprintf(comparison, sizeof(comparison), "%s.compare.%ld", local, (long)getpid());
(void)unlink(comparison);
status = io->receive(selection->bus, selection->device, existing.item_id, comparison);
if (status != TRAINLOG_STATUS_OK) return status;
if (same_files(local, comparison)) { (void)unlink(comparison); return TRAINLOG_STATUS_OK; }
if (status != TRAINLOG_STATUS_OK)
return status;
if (same_files(local, comparison)) {
(void)unlink(comparison);
return TRAINLOG_STATUS_OK;
}
(void)unlink(comparison);
(void)snprintf(temporary_name, sizeof(temporary_name), ".trainlog-%ld-%s", (long)getpid(), name);
status = io->send(selection->bus, selection->device, selection->storage, parent, local, temporary_name, &uploaded);
if (status != TRAINLOG_STATUS_OK) return status;
(void)snprintf(
temporary_name, sizeof(temporary_name), ".trainlog-%ld-%s", (long)getpid(), name);
status = io->send(selection->bus,
selection->device,
selection->storage,
parent,
local,
temporary_name,
&uploaded);
if (status != TRAINLOG_STATUS_OK)
return status;
(void)snprintf(comparison, sizeof(comparison), "%s.verify.%ld", local, (long)getpid());
(void)unlink(comparison);
status = io->receive(selection->bus, selection->device, uploaded, comparison);
if (status != TRAINLOG_STATUS_OK || !same_files(local, comparison)) {
(void)unlink(comparison); (void)io->remove(selection->bus, selection->device, uploaded);
(void)unlink(comparison);
(void)io->remove(selection->bus, selection->device, uploaded);
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_CONFLICT : status;
}
(void)unlink(comparison);
status = io->remove(selection->bus, selection->device, existing.item_id);
if (status == TRAINLOG_STATUS_OK) status = io->rename(selection->bus, selection->device, uploaded, name);
if (status == TRAINLOG_STATUS_OK)
status = io->rename(selection->bus, selection->device, uploaded, name);
return status;
}
if (status != TRAINLOG_STATUS_NOT_FOUND) return status;
status = io->send(selection->bus, selection->device, selection->storage, parent, local, name, &uploaded);
if (status != TRAINLOG_STATUS_OK) return status;
if (status != TRAINLOG_STATUS_NOT_FOUND)
return status;
status = io->send(
selection->bus, selection->device, selection->storage, parent, local, name, &uploaded);
if (status != TRAINLOG_STATUS_OK)
return status;
(void)snprintf(comparison, sizeof(comparison), "%s.verify.%ld", local, (long)getpid());
(void)unlink(comparison);
status = io->receive(selection->bus, selection->device, uploaded, comparison);
if (status != TRAINLOG_STATUS_OK || !same_files(local, comparison)) {
(void)unlink(comparison); (void)io->remove(selection->bus, selection->device, uploaded);
(void)unlink(comparison);
(void)io->remove(selection->bus, selection->device, uploaded);
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_CONFLICT : status;
}
(void)unlink(comparison);
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus push_folder(const TrainlogGenerationMtpIo *io, const Selection *selection,
uint32_t parent, const char *local, unsigned int depth, bool manifests)
{
DIR *directory; struct dirent *entry; TrainlogStatus status = TRAINLOG_STATUS_OK;
if (depth > TRAINLOG_GENERATION_MTP_MAX_DEPTH || (directory = opendir(local)) == NULL) return TRAINLOG_STATUS_SYSTEM_ERROR;
static TrainlogStatus push_folder(const TrainlogGenerationMtpIo *io,
const Selection *selection,
uint32_t parent,
const char *local,
unsigned int depth,
bool manifests) {
DIR *directory;
struct dirent *entry;
TrainlogStatus status = TRAINLOG_STATUS_OK;
if (depth > TRAINLOG_GENERATION_MTP_MAX_DEPTH || (directory = opendir(local)) == NULL)
return TRAINLOG_STATUS_SYSTEM_ERROR;
while ((entry = readdir(directory)) != NULL && status == TRAINLOG_STATUS_OK) {
char path[1024]; struct stat value; uint32_t child; bool created;
if (!safe_leaf(entry->d_name) || entry->d_name[0] == '.') continue;
if ((strcmp(entry->d_name, "manifest.json") == 0) != manifests) continue;
char path[1024];
struct stat value;
uint32_t child;
bool created;
if (!safe_leaf(entry->d_name) || entry->d_name[0] == '.')
continue;
if ((strcmp(entry->d_name, "manifest.json") == 0) != manifests)
continue;
int written = snprintf(path, sizeof(path), "%s/%s", local, entry->d_name);
if (written < 0 || (size_t)written >= sizeof(path) || lstat(path, &value) != 0 || S_ISLNK(value.st_mode)) { status = TRAINLOG_STATUS_INVALID_ARGUMENT; break; }
if (written < 0 || (size_t)written >= sizeof(path) || lstat(path, &value) != 0 ||
S_ISLNK(value.st_mode)) {
status = TRAINLOG_STATUS_INVALID_ARGUMENT;
break;
}
if (S_ISDIR(value.st_mode)) {
if (manifests) continue;
status = io->ensure_folder(selection->bus, selection->device, selection->storage, parent, entry->d_name, &child, &created);
if (status == TRAINLOG_STATUS_OK) status = push_folder(io, selection, child, path, depth + 1U, false);
if (status == TRAINLOG_STATUS_OK) status = push_folder(io, selection, child, path, depth + 1U, true);
} else if (S_ISREG(value.st_mode)) status = push_file(io, selection, parent, path, entry->d_name);
if (manifests)
continue;
status = io->ensure_folder(selection->bus,
selection->device,
selection->storage,
parent,
entry->d_name,
&child,
&created);
if (status == TRAINLOG_STATUS_OK)
status = push_folder(io, selection, child, path, depth + 1U, false);
if (status == TRAINLOG_STATUS_OK)
status = push_folder(io, selection, child, path, depth + 1U, true);
} else if (S_ISREG(value.st_mode))
status = push_file(io, selection, parent, path, entry->d_name);
}
(void)closedir(directory); return status;
(void)closedir(directory);
return status;
}
static TrainlogStatus validate_local_folder(const char *local, unsigned int depth,
uint64_t *total, size_t *objects)
{
static TrainlogStatus
validate_local_folder(const char *local, unsigned int depth, uint64_t *total, size_t *objects) {
DIR *directory;
struct dirent *entry;
TrainlogStatus status = TRAINLOG_STATUS_OK;
if (depth > TRAINLOG_GENERATION_MTP_MAX_DEPTH || (directory = opendir(local)) == NULL)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
while ((entry = readdir(directory)) != NULL && status == TRAINLOG_STATUS_OK) {
char path[1024]; struct stat value; int written;
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
if (!safe_leaf(entry->d_name) || entry->d_name[0] == '.') { status = TRAINLOG_STATUS_INVALID_ARGUMENT; break; }
char path[1024];
struct stat value;
int written;
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue;
if (!safe_leaf(entry->d_name) || entry->d_name[0] == '.') {
status = TRAINLOG_STATUS_INVALID_ARGUMENT;
break;
}
written = snprintf(path, sizeof(path), "%s/%s", local, entry->d_name);
if (written < 0 || (size_t)written >= sizeof(path) || lstat(path, &value) != 0 ||
S_ISLNK(value.st_mode)) { status = TRAINLOG_STATUS_INVALID_ARGUMENT; break; }
if (*objects == TRAINLOG_GENERATION_MTP_MAX_CHILDREN) { status = TRAINLOG_STATUS_INVALID_ARGUMENT; break; }
S_ISLNK(value.st_mode)) {
status = TRAINLOG_STATUS_INVALID_ARGUMENT;
break;
}
if (*objects == TRAINLOG_GENERATION_MTP_MAX_CHILDREN) {
status = TRAINLOG_STATUS_INVALID_ARGUMENT;
break;
}
++*objects;
if (S_ISDIR(value.st_mode)) status = validate_local_folder(path, depth + 1U, total, objects);
if (S_ISDIR(value.st_mode))
status = validate_local_folder(path, depth + 1U, total, objects);
else if (S_ISREG(value.st_mode)) {
if (value.st_size < 0 || (uint64_t)value.st_size > TRAINLOG_GENERATION_MTP_MAX_TOTAL_BYTES - *total)
if (value.st_size < 0 ||
(uint64_t)value.st_size > TRAINLOG_GENERATION_MTP_MAX_TOTAL_BYTES - *total)
status = TRAINLOG_STATUS_INVALID_ARGUMENT;
else *total += (uint64_t)value.st_size;
} else status = TRAINLOG_STATUS_INVALID_ARGUMENT;
else
*total += (uint64_t)value.st_size;
} else
status = TRAINLOG_STATUS_INVALID_ARGUMENT;
}
(void)closedir(directory);
return status;
}
TrainlogStatus trainlog_generation_mtp_pull(const TrainlogGenerationMtpIo *io,
const char *expected, const char *local, char *diagnostic, size_t diagnostic_size)
{
Selection selection; uint64_t total = 0U; size_t objects = 0U; TrainlogStatus status;
if (io == NULL || expected == NULL || local == NULL) return TRAINLOG_STATUS_INVALID_ARGUMENT;
status = mkdir_private(local); if (status == TRAINLOG_STATUS_OK) status = select_peer(io, expected, local, &selection, diagnostic, diagnostic_size);
if (status == TRAINLOG_STATUS_OK) status = pull_folder(io, &selection, selection.root, local, 0U, &total, &objects);
const char *expected,
const char *local,
char *diagnostic,
size_t diagnostic_size) {
Selection selection;
uint64_t total = 0U;
size_t objects = 0U;
TrainlogStatus status;
if (io == NULL || expected == NULL || local == NULL)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
status = mkdir_private(local);
if (status == TRAINLOG_STATUS_OK)
status = select_peer(io, expected, local, &selection, diagnostic, diagnostic_size);
if (status == TRAINLOG_STATUS_OK)
status = pull_folder(io, &selection, selection.root, local, 0U, &total, &objects);
return status;
}
TrainlogStatus trainlog_generation_mtp_push(const TrainlogGenerationMtpIo *io,
const char *expected, const char *local, char *diagnostic, size_t diagnostic_size)
{
Selection selection; TrainlogStatus status; uint64_t total = 0U; size_t objects = 0U;
if (io == NULL || expected == NULL || local == NULL) return TRAINLOG_STATUS_INVALID_ARGUMENT;
const char *expected,
const char *local,
char *diagnostic,
size_t diagnostic_size) {
Selection selection;
TrainlogStatus status;
uint64_t total = 0U;
size_t objects = 0U;
if (io == NULL || expected == NULL || local == NULL)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
status = validate_local_folder(local, 0U, &total, &objects);
if (status == TRAINLOG_STATUS_OK) status = select_peer(io, expected, local, &selection, diagnostic, diagnostic_size);
if (status == TRAINLOG_STATUS_OK) status = push_folder(io, &selection, selection.root, local, 0U, false);
if (status == TRAINLOG_STATUS_OK) status = push_folder(io, &selection, selection.root, local, 0U, true);
if (status == TRAINLOG_STATUS_OK)
status = select_peer(io, expected, local, &selection, diagnostic, diagnostic_size);
if (status == TRAINLOG_STATUS_OK)
status = push_folder(io, &selection, selection.root, local, 0U, false);
if (status == TRAINLOG_STATUS_OK)
status = push_folder(io, &selection, selection.root, local, 0U, true);
return status;
}
static const TrainlogGenerationMtpIo production = {
trainlog_usb_list_mtp_devices, trainlog_mtp_list_storages, trainlog_mtp_list_folder,
trainlog_mtp_ensure_folder, trainlog_mtp_receive_file, trainlog_mtp_send_text_file,
trainlog_mtp_delete_object, trainlog_mtp_rename_object
};
const TrainlogGenerationMtpIo *trainlog_generation_mtp_production_io(void) { return &production; }
static const TrainlogGenerationMtpIo production = {trainlog_usb_list_mtp_devices,
trainlog_mtp_list_storages,
trainlog_mtp_list_folder,
trainlog_mtp_ensure_folder,
trainlog_mtp_receive_file,
trainlog_mtp_send_text_file,
trainlog_mtp_delete_object,
trainlog_mtp_rename_object};
const TrainlogGenerationMtpIo *trainlog_generation_mtp_production_io(void) {
return &production;
}

View file

@ -5,33 +5,87 @@
#include <utf8proc.h>
static bool append(TrainlogJsonWriter *w, const char *s, size_t n) {
if (w == NULL || s == NULL || w->failed || n > w->capacity - (w->size < w->capacity ? w->size : w->capacity) - 1U) {
if (w != NULL) w->failed = true; return false;
if (w == NULL || s == NULL || w->failed ||
n > w->capacity - (w->size < w->capacity ? w->size : w->capacity) - 1U) {
if (w != NULL)
w->failed = true;
return false;
}
(void)memcpy(w->data + w->size, s, n); w->size += n; w->data[w->size] = '\0'; return true;
(void)memcpy(w->data + w->size, s, n);
w->size += n;
w->data[w->size] = '\0';
return true;
}
void trainlog_json_init(TrainlogJsonWriter *w, char *data, size_t capacity) {
if (w == NULL) return; w->data=data; w->capacity=capacity; w->size=0U; w->failed=data==NULL||capacity==0U; if (!w->failed) data[0]='\0';
if (w == NULL)
return;
w->data = data;
w->capacity = capacity;
w->size = 0U;
w->failed = data == NULL || capacity == 0U;
if (!w->failed)
data[0] = '\0';
}
bool trainlog_json_raw(TrainlogJsonWriter *w,const char *v){return v!=NULL&&append(w,v,strlen(v));}
bool trainlog_json_string(TrainlogJsonWriter *w,const char *v){
const unsigned char *p=(const unsigned char *)v; char escape[7];
if(v==NULL||!append(w,"\"",1U))return false;
while(*p!='\0'){
bool trainlog_json_raw(TrainlogJsonWriter *w, const char *v) {
return v != NULL && append(w, v, strlen(v));
}
bool trainlog_json_string(TrainlogJsonWriter *w, const char *v) {
const unsigned char *p = (const unsigned char *)v;
char escape[7];
if (v == NULL || !append(w, "\"", 1U))
return false;
while (*p != '\0') {
utf8proc_int32_t codepoint;
utf8proc_ssize_t sequence=utf8proc_iterate(p,-1,&codepoint);
utf8proc_ssize_t sequence = utf8proc_iterate(p, -1, &codepoint);
/* CONTRACT: valid UTF-8 is preserved byte-for-byte; corrupt storage
* must fail serialization instead of producing invalid JSON. */
(void)codepoint;
if(sequence<=0){w->failed=true;return false;}
if(*p=='\"'||*p=='\\'){escape[0]='\\';escape[1]=(char)*p;if(!append(w,escape,2U))return false;}
else if(*p=='\b'||*p=='\f'||*p=='\n'||*p=='\r'||*p=='\t'){const char codes[]="bfnrt";const char chars[]="\b\f\n\r\t";size_t i;escape[0]='\\';for(i=0U;i<5U&&chars[i]!=(char)*p;++i){}escape[1]=codes[i];if(!append(w,escape,2U))return false;}
else if(*p<0x20U){int n=snprintf(escape,sizeof(escape),"\\u%04x",(unsigned int)*p);if(n!=6||!append(w,escape,6U))return false;}
else if(!append(w,(const char *)p,(size_t)sequence))return false;
p+=(size_t)sequence;
if (sequence <= 0) {
w->failed = true;
return false;
}
if (*p == '\"' || *p == '\\') {
escape[0] = '\\';
escape[1] = (char)*p;
if (!append(w, escape, 2U))
return false;
} else if (*p == '\b' || *p == '\f' || *p == '\n' || *p == '\r' || *p == '\t') {
const char codes[] = "bfnrt";
const char chars[] = "\b\f\n\r\t";
size_t i;
escape[0] = '\\';
for (i = 0U; i < 5U && chars[i] != (char)*p; ++i) {
}
escape[1] = codes[i];
if (!append(w, escape, 2U))
return false;
} else if (*p < 0x20U) {
int n = snprintf(escape, sizeof(escape), "\\u%04x", (unsigned int)*p);
if (n != 6 || !append(w, escape, 6U))
return false;
} else if (!append(w, (const char *)p, (size_t)sequence))
return false;
p += (size_t)sequence;
}
return append(w,"\"",1U);
return append(w, "\"", 1U);
}
bool trainlog_json_size(TrainlogJsonWriter *w, size_t v) {
char b[32];
int n = snprintf(b, sizeof(b), "%zu", v);
return n > 0 && (size_t)n < sizeof(b) && append(w, b, (size_t)n);
}
bool trainlog_json_i64(TrainlogJsonWriter *w, long long v) {
char b[32];
int n = snprintf(b, sizeof(b), "%lld", v);
return n > 0 && (size_t)n < sizeof(b) && append(w, b, (size_t)n);
}
bool trainlog_json_double(TrainlogJsonWriter *w, double v) {
char b[64];
int n;
if (!isfinite(v)) {
w->failed = true;
return false;
}
n = snprintf(b, sizeof(b), "%.17g", v);
return n > 0 && (size_t)n < sizeof(b) && append(w, b, (size_t)n);
}
bool trainlog_json_size(TrainlogJsonWriter*w,size_t v){char b[32];int n=snprintf(b,sizeof(b),"%zu",v);return n>0&&(size_t)n<sizeof(b)&&append(w,b,(size_t)n);}
bool trainlog_json_i64(TrainlogJsonWriter*w,long long v){char b[32];int n=snprintf(b,sizeof(b),"%lld",v);return n>0&&(size_t)n<sizeof(b)&&append(w,b,(size_t)n);}
bool trainlog_json_double(TrainlogJsonWriter*w,double v){char b[64];int n;if(!isfinite(v)){w->failed=true;return false;}n=snprintf(b,sizeof(b),"%.17g",v);return n>0&&(size_t)n<sizeof(b)&&append(w,b,(size_t)n);}

View file

@ -16,92 +16,368 @@
#define SECONDS_PER_DAY INT64_C(86400)
static bool copy_column(sqlite3_stmt *s, int column, char *out, size_t cap) {
const unsigned char *text; int bytes;
if (sqlite3_column_type(s,column)!=SQLITE_TEXT) return false;
text=sqlite3_column_text(s,column); bytes=sqlite3_column_bytes(s,column);
if(text==NULL||bytes<0||(size_t)bytes>=cap)return false;
(void)memcpy(out,text,(size_t)bytes);out[bytes]='\0';return true;
const unsigned char *text;
int bytes;
if (sqlite3_column_type(s, column) != SQLITE_TEXT)
return false;
text = sqlite3_column_text(s, column);
bytes = sqlite3_column_bytes(s, column);
if (text == NULL || bytes < 0 || (size_t)bytes >= cap)
return false;
(void)memcpy(out, text, (size_t)bytes);
out[bytes] = '\0';
return true;
}
static bool read_size(sqlite3_stmt*s,int c,size_t*out){sqlite3_int64 v;if(sqlite3_column_type(s,c)!=SQLITE_INTEGER)return false;v=sqlite3_column_int64(s,c);if(v<0||(uint64_t)v>(uint64_t)SIZE_MAX)return false;*out=(size_t)v;return true;}
static bool prepare(TrainlogDatabase*d,const char*sql,sqlite3_stmt**s){return sqlite3_prepare_v2(d->connection,sql,-1,s,NULL)==SQLITE_OK;}
static bool format_generated(int64_t now,char out[TRAINLOG_TIMESTAMP_MAX+1U]){
time_t value=(time_t)now;struct tm utc;if((int64_t)value!=now||gmtime_r(&value,&utc)==NULL)return false;
return strftime(out,TRAINLOG_TIMESTAMP_MAX+1U,"%Y-%m-%dT%H:%M:%SZ",&utc)>0U;
static bool read_size(sqlite3_stmt *s, int c, size_t *out) {
sqlite3_int64 v;
if (sqlite3_column_type(s, c) != SQLITE_INTEGER)
return false;
v = sqlite3_column_int64(s, c);
if (v < 0 || (uint64_t)v > (uint64_t)SIZE_MAX)
return false;
*out = (size_t)v;
return true;
}
static bool format_local_date(time_t value,char out[11]){struct tm local;return localtime_r(&value,&local)!=NULL&&strftime(out,11U,"%Y-%m-%d",&local)==10U;}
static bool load_user(char out[TRAINLOG_NAME_MAX+1U]){
struct passwd value;struct passwd *result=NULL;char buffer[4096];int rc=getpwuid_r(geteuid(),&value,buffer,sizeof(buffer),&result);
if(rc!=0||result==NULL||result->pw_name==NULL||result->pw_name[0]=='\0'||strlen(result->pw_name)>TRAINLOG_NAME_MAX)return false;
(void)snprintf(out,TRAINLOG_NAME_MAX+1U,"%s",result->pw_name);return true;
static bool prepare(TrainlogDatabase *d, const char *sql, sqlite3_stmt **s) {
return sqlite3_prepare_v2(d->connection, sql, -1, s, NULL) == SQLITE_OK;
}
static TrainlogStatus load_activity(TrainlogDatabase*d,int64_t now,TrainlogWebDashboardSnapshot*out){
static const char SQL[]=
static bool format_generated(int64_t now, char out[TRAINLOG_TIMESTAMP_MAX + 1U]) {
time_t value = (time_t)now;
struct tm utc;
if ((int64_t)value != now || gmtime_r(&value, &utc) == NULL)
return false;
return strftime(out, TRAINLOG_TIMESTAMP_MAX + 1U, "%Y-%m-%dT%H:%M:%SZ", &utc) > 0U;
}
static bool format_local_date(time_t value, char out[11]) {
struct tm local;
return localtime_r(&value, &local) != NULL && strftime(out, 11U, "%Y-%m-%d", &local) == 10U;
}
static bool load_user(char out[TRAINLOG_NAME_MAX + 1U]) {
struct passwd value;
struct passwd *result = NULL;
char buffer[4096];
int rc = getpwuid_r(geteuid(), &value, buffer, sizeof(buffer), &result);
if (rc != 0 || result == NULL || result->pw_name == NULL || result->pw_name[0] == '\0' ||
strlen(result->pw_name) > TRAINLOG_NAME_MAX)
return false;
(void)snprintf(out, TRAINLOG_NAME_MAX + 1U, "%s", result->pw_name);
return true;
}
static TrainlogStatus
load_activity(TrainlogDatabase *d, int64_t now, TrainlogWebDashboardSnapshot *out) {
static const char SQL[] =
"SELECT substr(s.started_at,1,10),COUNT(DISTINCT s.id),COUNT(ps.id) FROM sessions s "
"LEFT JOIN session_exercises se ON se.session_row_id=s.id LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id "
"WHERE unixepoch(s.started_at)<=?1 AND substr(s.started_at,1,10)>=?2 AND (EXISTS(SELECT 1 FROM session_exercises x JOIN performed_sets p ON p.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) OR EXISTS(SELECT 1 FROM session_exercises x JOIN continuous_activity c ON c.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) OR EXISTS(SELECT 1 FROM session_exercises x JOIN max_results m ON m.session_exercise_row_id=x.id WHERE x.session_row_id=s.id)) GROUP BY substr(s.started_at,1,10) ORDER BY 1;";
sqlite3_stmt*s=NULL;int step;size_t i;time_t reference=(time_t)now;struct tm current;
if((int64_t)reference!=now)return TRAINLOG_STATUS_DATABASE_ERROR;
if(localtime_r(&reference,&current)==NULL)return TRAINLOG_STATUS_DATABASE_ERROR;
out->activity_count=TRAINLOG_WEB_ACTIVITY_DAYS;
for(i=0U;i<TRAINLOG_WEB_ACTIVITY_DAYS;++i){struct tm day=current;time_t normalized;day.tm_mday-=(int)(TRAINLOG_WEB_ACTIVITY_DAYS-1U-i);day.tm_isdst=-1;normalized=mktime(&day);if(normalized==(time_t)-1||!format_local_date(normalized,out->activity[i].date))return TRAINLOG_STATUS_DATABASE_ERROR;}
if(!prepare(d,SQL,&s)||sqlite3_bind_int64(s,1,now)!=SQLITE_OK||sqlite3_bind_text(s,2,out->activity[0].date,-1,SQLITE_TRANSIENT)!=SQLITE_OK)goto fail;
while((step=sqlite3_step(s))==SQLITE_ROW){char date[11];size_t sessions,sets;if(!copy_column(s,0,date,sizeof(date))||!read_size(s,1,&sessions)||!read_size(s,2,&sets))goto fail;for(i=0U;i<out->activity_count;++i)if(strcmp(date,out->activity[i].date)==0){out->activity[i].active=true;out->activity[i].session_count=sessions;out->activity[i].set_count=sets;break;}}
if(step!=SQLITE_DONE||sqlite3_finalize(s)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR;return TRAINLOG_STATUS_OK;
fail:if(s!=NULL)(void)sqlite3_finalize(s);return TRAINLOG_STATUS_DATABASE_ERROR;
"LEFT JOIN session_exercises se ON se.session_row_id=s.id LEFT JOIN performed_sets ps ON "
"ps.session_exercise_row_id=se.id "
"WHERE unixepoch(s.started_at)<=?1 AND substr(s.started_at,1,10)>=?2 AND (EXISTS(SELECT 1 "
"FROM session_exercises x JOIN performed_sets p ON p.session_exercise_row_id=x.id WHERE "
"x.session_row_id=s.id) OR EXISTS(SELECT 1 FROM session_exercises x JOIN "
"continuous_activity c ON c.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) OR "
"EXISTS(SELECT 1 FROM session_exercises x JOIN max_results m ON "
"m.session_exercise_row_id=x.id WHERE x.session_row_id=s.id)) GROUP BY "
"substr(s.started_at,1,10) ORDER BY 1;";
sqlite3_stmt *s = NULL;
int step;
size_t i;
time_t reference = (time_t)now;
struct tm current;
if ((int64_t)reference != now)
return TRAINLOG_STATUS_DATABASE_ERROR;
if (localtime_r(&reference, &current) == NULL)
return TRAINLOG_STATUS_DATABASE_ERROR;
out->activity_count = TRAINLOG_WEB_ACTIVITY_DAYS;
for (i = 0U; i < TRAINLOG_WEB_ACTIVITY_DAYS; ++i) {
struct tm day = current;
time_t normalized;
day.tm_mday -= (int)(TRAINLOG_WEB_ACTIVITY_DAYS - 1U - i);
day.tm_isdst = -1;
normalized = mktime(&day);
if (normalized == (time_t)-1 || !format_local_date(normalized, out->activity[i].date))
return TRAINLOG_STATUS_DATABASE_ERROR;
}
if (!prepare(d, SQL, &s) || sqlite3_bind_int64(s, 1, now) != SQLITE_OK ||
sqlite3_bind_text(s, 2, out->activity[0].date, -1, SQLITE_TRANSIENT) != SQLITE_OK)
goto fail;
while ((step = sqlite3_step(s)) == SQLITE_ROW) {
char date[11];
size_t sessions, sets;
if (!copy_column(s, 0, date, sizeof(date)) || !read_size(s, 1, &sessions) ||
!read_size(s, 2, &sets))
goto fail;
for (i = 0U; i < out->activity_count; ++i)
if (strcmp(date, out->activity[i].date) == 0) {
out->activity[i].active = true;
out->activity[i].session_count = sessions;
out->activity[i].set_count = sets;
break;
}
}
if (step != SQLITE_DONE || sqlite3_finalize(s) != SQLITE_OK)
return TRAINLOG_STATUS_DATABASE_ERROR;
return TRAINLOG_STATUS_OK;
fail:
if (s != NULL)
(void)sqlite3_finalize(s);
return TRAINLOG_STATUS_DATABASE_ERROR;
}
static TrainlogStatus load_last(TrainlogDatabase*d,int64_t now,TrainlogWebLastSession*out){
static const char SESSION_SQL[]=
"SELECT s.session_id,s.started_at,COALESCE(s.ended_at,''),COUNT(DISTINCT CASE WHEN ps.id IS NOT NULL OR ca.id IS NOT NULL OR mr.session_exercise_row_id IS NOT NULL THEN se.id END),COUNT(DISTINCT ps.id),COUNT(DISTINCT ca.id),COUNT(DISTINCT mr.session_exercise_row_id) FROM sessions s JOIN session_exercises se ON se.session_row_id=s.id LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id LEFT JOIN continuous_activity ca ON ca.session_exercise_row_id=se.id LEFT JOIN max_results mr ON mr.session_exercise_row_id=se.id WHERE unixepoch(s.started_at)<=?1 AND (ps.id IS NOT NULL OR ca.id IS NOT NULL OR mr.session_exercise_row_id IS NOT NULL) GROUP BY s.id ORDER BY unixepoch(s.started_at) DESC,s.session_id DESC LIMIT 1;";
static const char ZONE_SQL[]=
"SELECT z.zone_id,COUNT(DISTINCT se.id),COUNT(DISTINCT ps.id) FROM sessions s JOIN session_exercises se ON se.session_row_id=s.id JOIN exercises e ON e.id=se.exercise_row_id JOIN exercise_body_zones z ON z.exercise_row_id=e.id AND z.role='primary' LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id WHERE s.session_id=?1 AND (EXISTS(SELECT 1 FROM performed_sets p WHERE p.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM continuous_activity c WHERE c.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM max_results m WHERE m.session_exercise_row_id=se.id)) GROUP BY z.zone_id ORDER BY z.zone_id;";
sqlite3_stmt*s=NULL;int step;TrainlogTimestampKey start,end;
if(!prepare(d,SESSION_SQL,&s)||sqlite3_bind_int64(s,1,now)!=SQLITE_OK)goto fail;step=sqlite3_step(s);if(step==SQLITE_DONE){(void)sqlite3_finalize(s);return TRAINLOG_STATUS_OK;}if(step!=SQLITE_ROW||!copy_column(s,0,out->session_id,sizeof(out->session_id))||!copy_column(s,1,out->started_at,sizeof(out->started_at))||!copy_column(s,2,out->ended_at,sizeof(out->ended_at))||!read_size(s,3,&out->exercise_count)||!read_size(s,4,&out->set_count)||!read_size(s,5,&out->continuous_count)||!read_size(s,6,&out->max_count))goto fail;
out->available=true;out->has_ended_at=out->ended_at[0]!='\0';
if(!trainlog_timestamp_parse(out->started_at,strlen(out->started_at),&start)){out->available=false;goto invalid;}
if(out->has_ended_at){if(!trainlog_timestamp_parse(out->ended_at,strlen(out->ended_at),&end)||end.utc_second<start.utc_second)goto invalid;out->has_duration=true;out->duration_seconds=(uint64_t)(end.utc_second-start.utc_second);}
if(sqlite3_step(s)!=SQLITE_DONE||sqlite3_finalize(s)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR;s=NULL;
if(!prepare(d,ZONE_SQL,&s)||sqlite3_bind_text(s,1,out->session_id,-1,SQLITE_TRANSIENT)!=SQLITE_OK)goto fail;
while((step=sqlite3_step(s))==SQLITE_ROW){TrainlogWebWorkedZone*z;const TrainlogBodyZone*catalog;if(out->zone_count==TRAINLOG_WEB_ZONE_CAPACITY){return TRAINLOG_STATUS_DATABASE_ERROR;}z=&out->zones[out->zone_count];if(!copy_column(s,0,z->zone_id,sizeof(z->zone_id))||!read_size(s,1,&z->occurrence_count)||!read_size(s,2,&z->set_count))goto fail;z->session_count=1U;catalog=trainlog_body_zone_catalog_lookup(z->zone_id);if(catalog==NULL||strlen(catalog->display_name)>TRAINLOG_NAME_MAX)goto invalid;(void)snprintf(z->label,sizeof(z->label),"%s",catalog->display_name);++out->zone_count;}
if(step!=SQLITE_DONE||sqlite3_finalize(s)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR;return TRAINLOG_STATUS_OK;
invalid:if(s!=NULL)(void)sqlite3_finalize(s);return TRAINLOG_STATUS_INVALID_ARGUMENT;
fail:if(s!=NULL)(void)sqlite3_finalize(s);return TRAINLOG_STATUS_DATABASE_ERROR;
static TrainlogStatus load_last(TrainlogDatabase *d, int64_t now, TrainlogWebLastSession *out) {
static const char SESSION_SQL[] =
"SELECT s.session_id,s.started_at,COALESCE(s.ended_at,''),COUNT(DISTINCT CASE WHEN ps.id "
"IS NOT NULL OR ca.id IS NOT NULL OR mr.session_exercise_row_id IS NOT NULL THEN se.id "
"END),COUNT(DISTINCT ps.id),COUNT(DISTINCT ca.id),COUNT(DISTINCT "
"mr.session_exercise_row_id) FROM sessions s JOIN session_exercises se ON "
"se.session_row_id=s.id LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id "
"LEFT JOIN continuous_activity ca ON ca.session_exercise_row_id=se.id LEFT JOIN "
"max_results mr ON mr.session_exercise_row_id=se.id WHERE unixepoch(s.started_at)<=?1 AND "
"(ps.id IS NOT NULL OR ca.id IS NOT NULL OR mr.session_exercise_row_id IS NOT NULL) GROUP "
"BY s.id ORDER BY unixepoch(s.started_at) DESC,s.session_id DESC LIMIT 1;";
static const char ZONE_SQL[] =
"SELECT z.zone_id,COUNT(DISTINCT se.id),COUNT(DISTINCT ps.id) FROM sessions s JOIN "
"session_exercises se ON se.session_row_id=s.id JOIN exercises e ON "
"e.id=se.exercise_row_id JOIN exercise_body_zones z ON z.exercise_row_id=e.id AND "
"z.role='primary' LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id WHERE "
"s.session_id=?1 AND (EXISTS(SELECT 1 FROM performed_sets p WHERE "
"p.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM continuous_activity c WHERE "
"c.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM max_results m WHERE "
"m.session_exercise_row_id=se.id)) GROUP BY z.zone_id ORDER BY z.zone_id;";
sqlite3_stmt *s = NULL;
int step;
TrainlogTimestampKey start, end;
if (!prepare(d, SESSION_SQL, &s) || sqlite3_bind_int64(s, 1, now) != SQLITE_OK)
goto fail;
step = sqlite3_step(s);
if (step == SQLITE_DONE) {
(void)sqlite3_finalize(s);
return TRAINLOG_STATUS_OK;
}
if (step != SQLITE_ROW || !copy_column(s, 0, out->session_id, sizeof(out->session_id)) ||
!copy_column(s, 1, out->started_at, sizeof(out->started_at)) ||
!copy_column(s, 2, out->ended_at, sizeof(out->ended_at)) ||
!read_size(s, 3, &out->exercise_count) || !read_size(s, 4, &out->set_count) ||
!read_size(s, 5, &out->continuous_count) || !read_size(s, 6, &out->max_count))
goto fail;
out->available = true;
out->has_ended_at = out->ended_at[0] != '\0';
if (!trainlog_timestamp_parse(out->started_at, strlen(out->started_at), &start)) {
out->available = false;
goto invalid;
}
if (out->has_ended_at) {
if (!trainlog_timestamp_parse(out->ended_at, strlen(out->ended_at), &end) ||
end.utc_second < start.utc_second)
goto invalid;
out->has_duration = true;
out->duration_seconds = (uint64_t)(end.utc_second - start.utc_second);
}
if (sqlite3_step(s) != SQLITE_DONE || sqlite3_finalize(s) != SQLITE_OK)
return TRAINLOG_STATUS_DATABASE_ERROR;
s = NULL;
if (!prepare(d, ZONE_SQL, &s) ||
sqlite3_bind_text(s, 1, out->session_id, -1, SQLITE_TRANSIENT) != SQLITE_OK)
goto fail;
while ((step = sqlite3_step(s)) == SQLITE_ROW) {
TrainlogWebWorkedZone *z;
const TrainlogBodyZone *catalog;
if (out->zone_count == TRAINLOG_WEB_ZONE_CAPACITY) {
return TRAINLOG_STATUS_DATABASE_ERROR;
}
z = &out->zones[out->zone_count];
if (!copy_column(s, 0, z->zone_id, sizeof(z->zone_id)) ||
!read_size(s, 1, &z->occurrence_count) || !read_size(s, 2, &z->set_count))
goto fail;
z->session_count = 1U;
catalog = trainlog_body_zone_catalog_lookup(z->zone_id);
if (catalog == NULL || strlen(catalog->display_name) > TRAINLOG_NAME_MAX)
goto invalid;
(void)snprintf(z->label, sizeof(z->label), "%s", catalog->display_name);
++out->zone_count;
}
if (step != SQLITE_DONE || sqlite3_finalize(s) != SQLITE_OK)
return TRAINLOG_STATUS_DATABASE_ERROR;
return TRAINLOG_STATUS_OK;
invalid:
if (s != NULL)
(void)sqlite3_finalize(s);
return TRAINLOG_STATUS_INVALID_ARGUMENT;
fail:
if (s != NULL)
(void)sqlite3_finalize(s);
return TRAINLOG_STATUS_DATABASE_ERROR;
}
static TrainlogStatus load_maxima(TrainlogDatabase*d,int64_t now,TrainlogWebDashboardSnapshot*out){
static const char SQL[]="SELECT s.session_id,se.entry_id,e.exercise_id,e.name,COALESCE(se.equipment_id,''),m.max_weight_kg,s.started_at FROM max_results m JOIN session_exercises se ON se.id=m.session_exercise_row_id JOIN sessions s ON s.id=se.session_row_id JOIN exercises e ON e.id=se.exercise_row_id WHERE unixepoch(s.started_at)<=?1 ORDER BY unixepoch(s.started_at) DESC,s.session_id DESC,se.entry_id DESC LIMIT ?2;";
sqlite3_stmt*s=NULL;int step;if(!prepare(d,SQL,&s)||sqlite3_bind_int64(s,1,now)!=SQLITE_OK||sqlite3_bind_int64(s,2,(sqlite3_int64)(TRAINLOG_WEB_MAX_RECORDS+1U))!=SQLITE_OK)goto fail;
while((step=sqlite3_step(s))==SQLITE_ROW){TrainlogWebMaxRecord*r;if(out->max_record_count==TRAINLOG_WEB_MAX_RECORDS){out->partial=true;continue;}r=&out->max_records[out->max_record_count];if(!copy_column(s,0,r->session_id,sizeof(r->session_id))||!copy_column(s,1,r->entry_id,sizeof(r->entry_id))||!copy_column(s,2,r->exercise_id,sizeof(r->exercise_id))||!copy_column(s,3,r->exercise_name,sizeof(r->exercise_name))||!copy_column(s,4,r->equipment_id,sizeof(r->equipment_id))||!copy_column(s,6,r->timestamp,sizeof(r->timestamp))||(sqlite3_column_type(s,5)!=SQLITE_FLOAT&&sqlite3_column_type(s,5)!=SQLITE_INTEGER)||!isfinite(sqlite3_column_double(s,5))||sqlite3_column_double(s,5)<=0.0){out->invalid_data=true;continue;}r->weight_kg=sqlite3_column_double(s,5);++out->max_record_count;}
if(step!=SQLITE_DONE||sqlite3_finalize(s)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR;return TRAINLOG_STATUS_OK;
fail:if(s!=NULL)(void)sqlite3_finalize(s);return TRAINLOG_STATUS_DATABASE_ERROR;
static TrainlogStatus
load_maxima(TrainlogDatabase *d, int64_t now, TrainlogWebDashboardSnapshot *out) {
static const char SQL[] =
"SELECT "
"s.session_id,se.entry_id,e.exercise_id,e.name,COALESCE(se.equipment_id,''),m.max_weight_"
"kg,s.started_at FROM max_results m JOIN session_exercises se ON "
"se.id=m.session_exercise_row_id JOIN sessions s ON s.id=se.session_row_id JOIN exercises "
"e ON e.id=se.exercise_row_id WHERE unixepoch(s.started_at)<=?1 ORDER BY "
"unixepoch(s.started_at) DESC,s.session_id DESC,se.entry_id DESC LIMIT ?2;";
sqlite3_stmt *s = NULL;
int step;
if (!prepare(d, SQL, &s) || sqlite3_bind_int64(s, 1, now) != SQLITE_OK ||
sqlite3_bind_int64(s, 2, (sqlite3_int64)(TRAINLOG_WEB_MAX_RECORDS + 1U)) != SQLITE_OK)
goto fail;
while ((step = sqlite3_step(s)) == SQLITE_ROW) {
TrainlogWebMaxRecord *r;
if (out->max_record_count == TRAINLOG_WEB_MAX_RECORDS) {
out->partial = true;
continue;
}
r = &out->max_records[out->max_record_count];
if (!copy_column(s, 0, r->session_id, sizeof(r->session_id)) ||
!copy_column(s, 1, r->entry_id, sizeof(r->entry_id)) ||
!copy_column(s, 2, r->exercise_id, sizeof(r->exercise_id)) ||
!copy_column(s, 3, r->exercise_name, sizeof(r->exercise_name)) ||
!copy_column(s, 4, r->equipment_id, sizeof(r->equipment_id)) ||
!copy_column(s, 6, r->timestamp, sizeof(r->timestamp)) ||
(sqlite3_column_type(s, 5) != SQLITE_FLOAT &&
sqlite3_column_type(s, 5) != SQLITE_INTEGER) ||
!isfinite(sqlite3_column_double(s, 5)) || sqlite3_column_double(s, 5) <= 0.0) {
out->invalid_data = true;
continue;
}
r->weight_kg = sqlite3_column_double(s, 5);
++out->max_record_count;
}
if (step != SQLITE_DONE || sqlite3_finalize(s) != SQLITE_OK)
return TRAINLOG_STATUS_DATABASE_ERROR;
return TRAINLOG_STATUS_OK;
fail:
if (s != NULL)
(void)sqlite3_finalize(s);
return TRAINLOG_STATUS_DATABASE_ERROR;
}
static TrainlogStatus load_zones(TrainlogDatabase*d,int64_t now,TrainlogWebDashboardSnapshot*out){
static const char SQL[]="SELECT z.zone_id,COUNT(DISTINCT s.id),COUNT(DISTINCT se.id),COUNT(DISTINCT ps.id) FROM sessions s JOIN session_exercises se ON se.session_row_id=s.id JOIN exercises e ON e.id=se.exercise_row_id JOIN exercise_body_zones z ON z.exercise_row_id=e.id AND z.role='primary' LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id WHERE substr(s.started_at,1,10)>=?1 AND unixepoch(s.started_at)<=?2 AND (EXISTS(SELECT 1 FROM performed_sets p WHERE p.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM continuous_activity c WHERE c.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM max_results m WHERE m.session_exercise_row_id=se.id)) GROUP BY z.zone_id ORDER BY z.zone_id;";
sqlite3_stmt*s=NULL;int step;if(!prepare(d,SQL,&s)||sqlite3_bind_text(s,1,out->activity[TRAINLOG_WEB_ACTIVITY_DAYS-30U].date,-1,SQLITE_TRANSIENT)!=SQLITE_OK||sqlite3_bind_int64(s,2,now)!=SQLITE_OK)goto fail;
while((step=sqlite3_step(s))==SQLITE_ROW){TrainlogWebWorkedZone*z;const TrainlogBodyZone*catalog;if(out->muscle_zone_count==TRAINLOG_WEB_ZONE_CAPACITY){out->partial=true;continue;}z=&out->muscle_zones[out->muscle_zone_count];if(!copy_column(s,0,z->zone_id,sizeof(z->zone_id))||!read_size(s,1,&z->session_count)||!read_size(s,2,&z->occurrence_count)||!read_size(s,3,&z->set_count))goto fail;catalog=trainlog_body_zone_catalog_lookup(z->zone_id);if(catalog==NULL||strlen(catalog->display_name)>TRAINLOG_NAME_MAX){out->invalid_data=true;continue;}(void)snprintf(z->label,sizeof(z->label),"%s",catalog->display_name);++out->muscle_zone_count;}
if(step!=SQLITE_DONE||sqlite3_finalize(s)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR;return TRAINLOG_STATUS_OK;
fail:if(s!=NULL)(void)sqlite3_finalize(s);return TRAINLOG_STATUS_DATABASE_ERROR;
static TrainlogStatus
load_zones(TrainlogDatabase *d, int64_t now, TrainlogWebDashboardSnapshot *out) {
static const char SQL[] =
"SELECT z.zone_id,COUNT(DISTINCT s.id),COUNT(DISTINCT se.id),COUNT(DISTINCT ps.id) FROM "
"sessions s JOIN session_exercises se ON se.session_row_id=s.id JOIN exercises e ON "
"e.id=se.exercise_row_id JOIN exercise_body_zones z ON z.exercise_row_id=e.id AND "
"z.role='primary' LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id WHERE "
"substr(s.started_at,1,10)>=?1 AND unixepoch(s.started_at)<=?2 AND (EXISTS(SELECT 1 FROM "
"performed_sets p WHERE p.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM "
"continuous_activity c WHERE c.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM "
"max_results m WHERE m.session_exercise_row_id=se.id)) GROUP BY z.zone_id ORDER BY "
"z.zone_id;";
sqlite3_stmt *s = NULL;
int step;
if (!prepare(d, SQL, &s) ||
sqlite3_bind_text(
s, 1, out->activity[TRAINLOG_WEB_ACTIVITY_DAYS - 30U].date, -1, SQLITE_TRANSIENT) !=
SQLITE_OK ||
sqlite3_bind_int64(s, 2, now) != SQLITE_OK)
goto fail;
while ((step = sqlite3_step(s)) == SQLITE_ROW) {
TrainlogWebWorkedZone *z;
const TrainlogBodyZone *catalog;
if (out->muscle_zone_count == TRAINLOG_WEB_ZONE_CAPACITY) {
out->partial = true;
continue;
}
z = &out->muscle_zones[out->muscle_zone_count];
if (!copy_column(s, 0, z->zone_id, sizeof(z->zone_id)) ||
!read_size(s, 1, &z->session_count) || !read_size(s, 2, &z->occurrence_count) ||
!read_size(s, 3, &z->set_count))
goto fail;
catalog = trainlog_body_zone_catalog_lookup(z->zone_id);
if (catalog == NULL || strlen(catalog->display_name) > TRAINLOG_NAME_MAX) {
out->invalid_data = true;
continue;
}
(void)snprintf(z->label, sizeof(z->label), "%s", catalog->display_name);
++out->muscle_zone_count;
}
if (step != SQLITE_DONE || sqlite3_finalize(s) != SQLITE_OK)
return TRAINLOG_STATUS_DATABASE_ERROR;
return TRAINLOG_STATUS_OK;
fail:
if (s != NULL)
(void)sqlite3_finalize(s);
return TRAINLOG_STATUS_DATABASE_ERROR;
}
static TrainlogStatus scan_quality(TrainlogDatabase*d,TrainlogWebDashboardSnapshot*out){
static const char SQL[]="SELECT s.started_at,COALESCE(s.ended_at,'') FROM sessions s WHERE EXISTS(SELECT 1 FROM session_exercises x JOIN performed_sets p ON p.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) OR EXISTS(SELECT 1 FROM session_exercises x JOIN continuous_activity c ON c.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) OR EXISTS(SELECT 1 FROM session_exercises x JOIN max_results m ON m.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) ORDER BY s.id LIMIT 4097;";
sqlite3_stmt*s=NULL;int step;size_t count=0U;if(!prepare(d,SQL,&s))goto fail;while((step=sqlite3_step(s))==SQLITE_ROW){char started[TRAINLOG_TIMESTAMP_MAX+1U],ended[TRAINLOG_TIMESTAMP_MAX+1U];TrainlogTimestampKey start_key,end_key;++count;if(count>4096U){out->partial=true;continue;}if(!copy_column(s,0,started,sizeof(started))||!copy_column(s,1,ended,sizeof(ended))||!trainlog_timestamp_parse(started,strlen(started),&start_key)||(ended[0]!='\0'&&(!trainlog_timestamp_parse(ended,strlen(ended),&end_key)||trainlog_timestamp_compare(&end_key,&start_key)<0)))out->invalid_data=true;}if(step!=SQLITE_DONE||sqlite3_finalize(s)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR;return TRAINLOG_STATUS_OK;fail:if(s!=NULL)(void)sqlite3_finalize(s);return TRAINLOG_STATUS_DATABASE_ERROR;
static TrainlogStatus scan_quality(TrainlogDatabase *d, TrainlogWebDashboardSnapshot *out) {
static const char SQL[] =
"SELECT s.started_at,COALESCE(s.ended_at,'') FROM sessions s WHERE EXISTS(SELECT 1 FROM "
"session_exercises x JOIN performed_sets p ON p.session_exercise_row_id=x.id WHERE "
"x.session_row_id=s.id) OR EXISTS(SELECT 1 FROM session_exercises x JOIN "
"continuous_activity c ON c.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) OR "
"EXISTS(SELECT 1 FROM session_exercises x JOIN max_results m ON "
"m.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) ORDER BY s.id LIMIT 4097;";
sqlite3_stmt *s = NULL;
int step;
size_t count = 0U;
if (!prepare(d, SQL, &s))
goto fail;
while ((step = sqlite3_step(s)) == SQLITE_ROW) {
char started[TRAINLOG_TIMESTAMP_MAX + 1U], ended[TRAINLOG_TIMESTAMP_MAX + 1U];
TrainlogTimestampKey start_key, end_key;
++count;
if (count > 4096U) {
out->partial = true;
continue;
}
if (!copy_column(s, 0, started, sizeof(started)) ||
!copy_column(s, 1, ended, sizeof(ended)) ||
!trainlog_timestamp_parse(started, strlen(started), &start_key) ||
(ended[0] != '\0' && (!trainlog_timestamp_parse(ended, strlen(ended), &end_key) ||
trainlog_timestamp_compare(&end_key, &start_key) < 0)))
out->invalid_data = true;
}
if (step != SQLITE_DONE || sqlite3_finalize(s) != SQLITE_OK)
return TRAINLOG_STATUS_DATABASE_ERROR;
return TRAINLOG_STATUS_OK;
fail:
if (s != NULL)
(void)sqlite3_finalize(s);
return TRAINLOG_STATUS_DATABASE_ERROR;
}
TrainlogStatus trainlog_web_dashboard_load(TrainlogDatabase*d,const TrainlogWebDashboardQuery*q,TrainlogWebDashboardSnapshot*out){
TrainlogStatus status,end_status;TrainlogDashboardQuery progression_query;
if(d==NULL||q==NULL||out==NULL)return TRAINLOG_STATUS_INVALID_ARGUMENT;(void)memset(out,0,sizeof(*out));
out->next_session_reason="no_persisted_executable_plan";out->cardio_reason="no_cardio_data_source";
if(!format_generated(q->reference_unix_second,out->generated_at)||!load_user(out->user_display_name))return TRAINLOG_STATUS_DATABASE_ERROR;
status=trainlog_database_read_snapshot_begin(d);if(status!=TRAINLOG_STATUS_OK)return status;
status=load_activity(d,q->reference_unix_second,out);
if(status==TRAINLOG_STATUS_OK)status=scan_quality(d,out);
if(status==TRAINLOG_STATUS_OK){progression_query.period=TRAINLOG_DASHBOARD_90_DAYS;progression_query.reference_unix_second=q->reference_unix_second;status=trainlog_dashboard_load(d,&progression_query,&out->progression);out->partial=out->partial||out->progression.partial;out->invalid_data=out->invalid_data||out->progression.invalid_data;}
if(status==TRAINLOG_STATUS_OK){status=load_last(d,q->reference_unix_second,&out->last_session);if(status==TRAINLOG_STATUS_INVALID_ARGUMENT){out->invalid_data=true;status=TRAINLOG_STATUS_OK;}}
if(status==TRAINLOG_STATUS_OK)status=load_maxima(d,q->reference_unix_second,out);
if(status==TRAINLOG_STATUS_OK)status=load_zones(d,q->reference_unix_second,out);
end_status=trainlog_database_read_snapshot_end(d,status==TRAINLOG_STATUS_OK);if(end_status!=TRAINLOG_STATUS_OK)status=end_status;return status;
TrainlogStatus trainlog_web_dashboard_load(TrainlogDatabase *d,
const TrainlogWebDashboardQuery *q,
TrainlogWebDashboardSnapshot *out) {
TrainlogStatus status, end_status;
TrainlogDashboardQuery progression_query;
if (d == NULL || q == NULL || out == NULL)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
(void)memset(out, 0, sizeof(*out));
out->next_session_reason = "no_persisted_executable_plan";
out->cardio_reason = "no_cardio_data_source";
if (!format_generated(q->reference_unix_second, out->generated_at) ||
!load_user(out->user_display_name))
return TRAINLOG_STATUS_DATABASE_ERROR;
status = trainlog_database_read_snapshot_begin(d);
if (status != TRAINLOG_STATUS_OK)
return status;
status = load_activity(d, q->reference_unix_second, out);
if (status == TRAINLOG_STATUS_OK)
status = scan_quality(d, out);
if (status == TRAINLOG_STATUS_OK) {
progression_query.period = TRAINLOG_DASHBOARD_90_DAYS;
progression_query.reference_unix_second = q->reference_unix_second;
status = trainlog_dashboard_load(d, &progression_query, &out->progression);
out->partial = out->partial || out->progression.partial;
out->invalid_data = out->invalid_data || out->progression.invalid_data;
}
if (status == TRAINLOG_STATUS_OK) {
status = load_last(d, q->reference_unix_second, &out->last_session);
if (status == TRAINLOG_STATUS_INVALID_ARGUMENT) {
out->invalid_data = true;
status = TRAINLOG_STATUS_OK;
}
}
if (status == TRAINLOG_STATUS_OK)
status = load_maxima(d, q->reference_unix_second, out);
if (status == TRAINLOG_STATUS_OK)
status = load_zones(d, q->reference_unix_second, out);
end_status = trainlog_database_read_snapshot_end(d, status == TRAINLOG_STATUS_OK);
if (end_status != TRAINLOG_STATUS_OK)
status = end_status;
return status;
}

View file

@ -3,26 +3,251 @@
#include <string.h>
#define RAW(value) do { if (!trainlog_json_raw(&w,(value))) goto overflow; } while (0)
#define STR(value) do { if (!trainlog_json_string(&w,(value))) goto overflow; } while (0)
#define NUM(value) do { if (!trainlog_json_size(&w,(value))) goto overflow; } while (0)
#define RAW(value) \
do { \
if (!trainlog_json_raw(&w, (value))) \
goto overflow; \
} while (0)
#define STR(value) \
do { \
if (!trainlog_json_string(&w, (value))) \
goto overflow; \
} while (0)
#define NUM(value) \
do { \
if (!trainlog_json_size(&w, (value))) \
goto overflow; \
} while (0)
static const char *tracking_name(TrainlogTrackingMode value){return value==TRAINLOG_TRACKING_DURATION?"duration":"reps";}
static const char *load_name(TrainlogLoadMode value){if(value==TRAINLOG_LOAD_EXTERNAL)return "external";if(value==TRAINLOG_LOAD_ASSISTANCE)return "assistance";return "none";}
static bool point_improved(const TrainlogDashboardSnapshot*p,size_t i){const TrainlogExercisePerformancePoint*a,*b;if(i==0U)return false;a=&p->performance[i-1U];b=&p->performance[i];if(!a->has_weight&&!b->has_weight)return b->metric_value>a->metric_value;if(a->has_weight!=b->has_weight)return false;if(p->performance_load_mode==TRAINLOG_LOAD_ASSISTANCE)return b->weight_kg<a->weight_kg||(b->weight_kg==a->weight_kg&&b->metric_value>a->metric_value);return b->weight_kg>a->weight_kg||(b->weight_kg==a->weight_kg&&b->metric_value>a->metric_value);}
static bool write_zone(TrainlogJsonWriter*w0,const TrainlogWebWorkedZone*z){TrainlogJsonWriter w=*w0;RAW("{\"zone_id\":");STR(z->zone_id);RAW(",\"label\":");STR(z->label);RAW(",\"session_count\":");NUM(z->session_count);RAW(",\"occurrence_count\":");NUM(z->occurrence_count);RAW(",\"set_count\":");NUM(z->set_count);RAW("}");*w0=w;return true;overflow:*w0=w;return false;}
TrainlogStatus trainlog_web_dashboard_serialize(const TrainlogWebDashboardSnapshot*s,char*out,size_t cap,size_t*out_size){
TrainlogJsonWriter w;size_t i;if(s==NULL||out==NULL||out_size==NULL)return TRAINLOG_STATUS_INVALID_ARGUMENT;*out_size=0U;trainlog_json_init(&w,out,cap);
RAW("{\"api_version\":1,\"data\":{\"footer\":{\"user\":");STR(s->user_display_name);RAW(",\"last_session_date\":");if(s->last_session.available){char date[11];(void)memcpy(date,s->last_session.started_at,10U);date[10]='\0';STR(date);}else RAW("null");RAW(",\"last_zones\":[");for(i=0U;i<s->last_session.zone_count;++i){if(i>0U)RAW(",");STR(s->last_session.zones[i].label);}RAW("]},");
RAW("\"next_session\":{\"available\":false,\"reason\":");STR(s->next_session_reason);RAW("},");
RAW("\"activity\":{\"available\":true,\"window_days\":90,\"days\":[");for(i=0U;i<s->activity_count;++i){const TrainlogWebActivityDay*d=&s->activity[i];if(i>0U)RAW(",");RAW("{\"date\":");STR(d->date);RAW(",\"active\":");RAW(d->active?"true":"false");RAW(",\"session_count\":");NUM(d->session_count);RAW(",\"set_count\":");NUM(d->set_count);RAW("}");}RAW("]},");
RAW("\"progression\":{");if(!s->progression.has_performance){RAW("\"available\":false,\"reason\":\"no_comparable_performance\"");}else{RAW("\"available\":true,\"identity\":{\"exercise_id\":");STR(s->progression.exercise.exercise_id);RAW(",\"exercise_name\":");STR(s->progression.exercise.name);RAW(",\"equipment_id\":");STR(s->progression.performance_equipment_id);RAW(",\"equipment_label\":");STR(s->progression.performance_equipment_label);RAW(",\"tracking_mode\":");STR(tracking_name(s->progression.performance_tracking_mode));RAW(",\"load_mode\":");STR(load_name(s->progression.performance_load_mode));RAW(",\"dose\":");if(!trainlog_json_i64(&w,(long long)s->progression.performance_dose))goto overflow;RAW("},\"points\":[");for(i=0U;i<s->progression.performance_count;++i){const TrainlogExercisePerformancePoint*p=&s->progression.performance[i];if(i>0U)RAW(",");RAW("{\"session_id\":");STR(p->session_id);RAW(",\"timestamp\":");STR(p->started_at);RAW(",\"metric_value\":");if(!trainlog_json_i64(&w,(long long)p->metric_value))goto overflow;RAW(",\"weight_kg\":");if(p->has_weight){if(!trainlog_json_double(&w,p->weight_kg))goto overflow;}else RAW("null");RAW(",\"improved\":");RAW(point_improved(&s->progression,i)?"true":"false");RAW("}");}RAW("]");}RAW("},");
RAW("\"last_session\":{");if(!s->last_session.available){RAW("\"available\":false,\"reason\":\"no_observable_session\"");}else{const TrainlogWebLastSession*l=&s->last_session;RAW("\"available\":true,\"session_id\":");STR(l->session_id);RAW(",\"started_at\":");STR(l->started_at);RAW(",\"ended_at\":");if(l->has_ended_at)STR(l->ended_at);else RAW("null");RAW(",\"duration_seconds\":");if(l->has_duration){if(!trainlog_json_i64(&w,(long long)l->duration_seconds))goto overflow;}else RAW("null");RAW(",\"exercise_count\":");NUM(l->exercise_count);RAW(",\"set_count\":");NUM(l->set_count);RAW(",\"continuous_count\":");NUM(l->continuous_count);RAW(",\"max_count\":");NUM(l->max_count);RAW(",\"primary_zones\":[");for(i=0U;i<l->zone_count;++i){if(i>0U)RAW(",");if(!write_zone(&w,&l->zones[i]))goto overflow;}RAW("]");}RAW("},");
RAW("\"max_records\":{\"available\":");RAW(s->max_record_count>0U?"true":"false");if(s->max_record_count==0U)RAW(",\"reason\":\"no_explicit_max_results\"");RAW(",\"records\":[");for(i=0U;i<s->max_record_count;++i){const TrainlogWebMaxRecord*r=&s->max_records[i];if(i>0U)RAW(",");RAW("{\"session_id\":");STR(r->session_id);RAW(",\"entry_id\":");STR(r->entry_id);RAW(",\"exercise_id\":");STR(r->exercise_id);RAW(",\"exercise_name\":");STR(r->exercise_name);RAW(",\"equipment_id\":");STR(r->equipment_id);RAW(",\"weight_kg\":");if(!trainlog_json_double(&w,r->weight_kg))goto overflow;RAW(",\"timestamp\":");STR(r->timestamp);RAW("}");}RAW("]},");
RAW("\"muscle_distribution\":{\"available\":");RAW(s->muscle_zone_count>0U?"true":"false");if(s->muscle_zone_count==0U)RAW(",\"reason\":\"no_worked_primary_zones\"");RAW(",\"window_days\":30,\"primary_zones\":[");for(i=0U;i<s->muscle_zone_count;++i){if(i>0U)RAW(",");if(!write_zone(&w,&s->muscle_zones[i]))goto overflow;}RAW("]},");
RAW("\"cardio\":{\"available\":false,\"reason\":");STR(s->cardio_reason);RAW("}},\"meta\":{\"partial\":");RAW(s->partial?"true":"false");RAW(",\"invalid_data\":");RAW(s->invalid_data?"true":"false");RAW(",\"generated_at\":");STR(s->generated_at);RAW("}}\n");
if(w.failed)goto overflow;*out_size=w.size;return TRAINLOG_STATUS_OK;
overflow:if(cap>0U)out[0]='\0';return TRAINLOG_STATUS_SYSTEM_ERROR;
static const char *tracking_name(TrainlogTrackingMode value) {
return value == TRAINLOG_TRACKING_DURATION ? "duration" : "reps";
}
static const char *load_name(TrainlogLoadMode value) {
if (value == TRAINLOG_LOAD_EXTERNAL)
return "external";
if (value == TRAINLOG_LOAD_ASSISTANCE)
return "assistance";
return "none";
}
static bool point_improved(const TrainlogDashboardSnapshot *p, size_t i) {
const TrainlogExercisePerformancePoint *a, *b;
if (i == 0U)
return false;
a = &p->performance[i - 1U];
b = &p->performance[i];
if (!a->has_weight && !b->has_weight)
return b->metric_value > a->metric_value;
if (a->has_weight != b->has_weight)
return false;
if (p->performance_load_mode == TRAINLOG_LOAD_ASSISTANCE)
return b->weight_kg < a->weight_kg ||
(b->weight_kg == a->weight_kg && b->metric_value > a->metric_value);
return b->weight_kg > a->weight_kg ||
(b->weight_kg == a->weight_kg && b->metric_value > a->metric_value);
}
static bool write_zone(TrainlogJsonWriter *w0, const TrainlogWebWorkedZone *z) {
TrainlogJsonWriter w = *w0;
RAW("{\"zone_id\":");
STR(z->zone_id);
RAW(",\"label\":");
STR(z->label);
RAW(",\"session_count\":");
NUM(z->session_count);
RAW(",\"occurrence_count\":");
NUM(z->occurrence_count);
RAW(",\"set_count\":");
NUM(z->set_count);
RAW("}");
*w0 = w;
return true;
overflow:
*w0 = w;
return false;
}
TrainlogStatus trainlog_web_dashboard_serialize(const TrainlogWebDashboardSnapshot *s,
char *out,
size_t cap,
size_t *out_size) {
TrainlogJsonWriter w;
size_t i;
if (s == NULL || out == NULL || out_size == NULL)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
*out_size = 0U;
trainlog_json_init(&w, out, cap);
RAW("{\"api_version\":1,\"data\":{\"footer\":{\"user\":");
STR(s->user_display_name);
RAW(",\"last_session_date\":");
if (s->last_session.available) {
char date[11];
(void)memcpy(date, s->last_session.started_at, 10U);
date[10] = '\0';
STR(date);
} else
RAW("null");
RAW(",\"last_zones\":[");
for (i = 0U; i < s->last_session.zone_count; ++i) {
if (i > 0U)
RAW(",");
STR(s->last_session.zones[i].label);
}
RAW("]},");
RAW("\"next_session\":{\"available\":false,\"reason\":");
STR(s->next_session_reason);
RAW("},");
RAW("\"activity\":{\"available\":true,\"window_days\":90,\"days\":[");
for (i = 0U; i < s->activity_count; ++i) {
const TrainlogWebActivityDay *d = &s->activity[i];
if (i > 0U)
RAW(",");
RAW("{\"date\":");
STR(d->date);
RAW(",\"active\":");
RAW(d->active ? "true" : "false");
RAW(",\"session_count\":");
NUM(d->session_count);
RAW(",\"set_count\":");
NUM(d->set_count);
RAW("}");
}
RAW("]},");
RAW("\"progression\":{");
if (!s->progression.has_performance) {
RAW("\"available\":false,\"reason\":\"no_comparable_performance\"");
} else {
RAW("\"available\":true,\"identity\":{\"exercise_id\":");
STR(s->progression.exercise.exercise_id);
RAW(",\"exercise_name\":");
STR(s->progression.exercise.name);
RAW(",\"equipment_id\":");
STR(s->progression.performance_equipment_id);
RAW(",\"equipment_label\":");
STR(s->progression.performance_equipment_label);
RAW(",\"tracking_mode\":");
STR(tracking_name(s->progression.performance_tracking_mode));
RAW(",\"load_mode\":");
STR(load_name(s->progression.performance_load_mode));
RAW(",\"dose\":");
if (!trainlog_json_i64(&w, (long long)s->progression.performance_dose))
goto overflow;
RAW("},\"points\":[");
for (i = 0U; i < s->progression.performance_count; ++i) {
const TrainlogExercisePerformancePoint *p = &s->progression.performance[i];
if (i > 0U)
RAW(",");
RAW("{\"session_id\":");
STR(p->session_id);
RAW(",\"timestamp\":");
STR(p->started_at);
RAW(",\"metric_value\":");
if (!trainlog_json_i64(&w, (long long)p->metric_value))
goto overflow;
RAW(",\"weight_kg\":");
if (p->has_weight) {
if (!trainlog_json_double(&w, p->weight_kg))
goto overflow;
} else
RAW("null");
RAW(",\"improved\":");
RAW(point_improved(&s->progression, i) ? "true" : "false");
RAW("}");
}
RAW("]");
}
RAW("},");
RAW("\"last_session\":{");
if (!s->last_session.available) {
RAW("\"available\":false,\"reason\":\"no_observable_session\"");
} else {
const TrainlogWebLastSession *l = &s->last_session;
RAW("\"available\":true,\"session_id\":");
STR(l->session_id);
RAW(",\"started_at\":");
STR(l->started_at);
RAW(",\"ended_at\":");
if (l->has_ended_at)
STR(l->ended_at);
else
RAW("null");
RAW(",\"duration_seconds\":");
if (l->has_duration) {
if (!trainlog_json_i64(&w, (long long)l->duration_seconds))
goto overflow;
} else
RAW("null");
RAW(",\"exercise_count\":");
NUM(l->exercise_count);
RAW(",\"set_count\":");
NUM(l->set_count);
RAW(",\"continuous_count\":");
NUM(l->continuous_count);
RAW(",\"max_count\":");
NUM(l->max_count);
RAW(",\"primary_zones\":[");
for (i = 0U; i < l->zone_count; ++i) {
if (i > 0U)
RAW(",");
if (!write_zone(&w, &l->zones[i]))
goto overflow;
}
RAW("]");
}
RAW("},");
RAW("\"max_records\":{\"available\":");
RAW(s->max_record_count > 0U ? "true" : "false");
if (s->max_record_count == 0U)
RAW(",\"reason\":\"no_explicit_max_results\"");
RAW(",\"records\":[");
for (i = 0U; i < s->max_record_count; ++i) {
const TrainlogWebMaxRecord *r = &s->max_records[i];
if (i > 0U)
RAW(",");
RAW("{\"session_id\":");
STR(r->session_id);
RAW(",\"entry_id\":");
STR(r->entry_id);
RAW(",\"exercise_id\":");
STR(r->exercise_id);
RAW(",\"exercise_name\":");
STR(r->exercise_name);
RAW(",\"equipment_id\":");
STR(r->equipment_id);
RAW(",\"weight_kg\":");
if (!trainlog_json_double(&w, r->weight_kg))
goto overflow;
RAW(",\"timestamp\":");
STR(r->timestamp);
RAW("}");
}
RAW("]},");
RAW("\"muscle_distribution\":{\"available\":");
RAW(s->muscle_zone_count > 0U ? "true" : "false");
if (s->muscle_zone_count == 0U)
RAW(",\"reason\":\"no_worked_primary_zones\"");
RAW(",\"window_days\":30,\"primary_zones\":[");
for (i = 0U; i < s->muscle_zone_count; ++i) {
if (i > 0U)
RAW(",");
if (!write_zone(&w, &s->muscle_zones[i]))
goto overflow;
}
RAW("]},");
RAW("\"cardio\":{\"available\":false,\"reason\":");
STR(s->cardio_reason);
RAW("}},\"meta\":{\"partial\":");
RAW(s->partial ? "true" : "false");
RAW(",\"invalid_data\":");
RAW(s->invalid_data ? "true" : "false");
RAW(",\"generated_at\":");
STR(s->generated_at);
RAW("}}\n");
if (w.failed)
goto overflow;
*out_size = w.size;
return TRAINLOG_STATUS_OK;
overflow:
if (cap > 0U)
out[0] = '\0';
return TRAINLOG_STATUS_SYSTEM_ERROR;
}

View file

@ -6,19 +6,221 @@
#include <unistd.h>
#include "trainlog/generation_mtp.h"
typedef struct Node { uint32_t id,parent; bool folder; char path[1024],name[256]; uint64_t size; } Node;
static Node nodes[2048]; static size_t node_count; static const char *remote;
static uint32_t scan(const char *path,uint32_t parent){DIR *d;struct dirent *e;uint32_t self=(uint32_t)(node_count+1U);Node *n=&nodes[node_count++];struct stat st;lstat(path,&st);n->id=self;n->parent=parent;n->folder=S_ISDIR(st.st_mode);n->size=(uint64_t)st.st_size;snprintf(n->path,sizeof(n->path),"%s",path);const char *slash=strrchr(path,'/');snprintf(n->name,sizeof(n->name),"%s",slash?slash+1:path);if(n->folder&&(d=opendir(path))!=NULL){while((e=readdir(d)))if(strcmp(e->d_name,".")&&strcmp(e->d_name,"..")){char child[1024];snprintf(child,sizeof(child),"%s/%s",path,e->d_name);scan(child,self);}closedir(d);}return self;}
static void rebuild(void){node_count=0U;scan(remote,UINT32_MAX);}
static Node *node(uint32_t id){size_t i;for(i=0;i<node_count;i++)if(nodes[i].id==id)return &nodes[i];return NULL;}
static TrainlogStatus devices(TrainlogUsbDevice *o,size_t c,size_t *n){if(c<1U)return TRAINLOG_STATUS_INVALID_ARGUMENT;memset(o,0,sizeof(*o));o->bus_number=1;o->device_number=1;*n=1;return TRAINLOG_STATUS_OK;}
static TrainlogStatus storages(unsigned int b,unsigned int d,TrainlogMtpStorage *o,size_t c,size_t *n){(void)b;(void)d;if(c<1U)return TRAINLOG_STATUS_INVALID_ARGUMENT;memset(o,0,sizeof(*o));o->storage_id=1;*n=1;return TRAINLOG_STATUS_OK;}
static TrainlogStatus children(unsigned int b,unsigned int d,uint32_t s,uint32_t p,TrainlogMtpEntry *o,size_t c,size_t *n){size_t i,k=0;(void)b;(void)d;(void)s;rebuild();if(p==UINT32_MAX)p=nodes[0].id;for(i=0;i<node_count;i++)if(nodes[i].parent==p){if(k>=c)return TRAINLOG_STATUS_INVALID_ARGUMENT;memset(&o[k],0,sizeof(o[k]));o[k].item_id=nodes[i].id;o[k].parent_id=p;o[k].storage_id=1;o[k].folder=nodes[i].folder;o[k].size_bytes=nodes[i].size;snprintf(o[k].name,sizeof(o[k].name),"%s",nodes[i].name);k++;}*n=k;return TRAINLOG_STATUS_OK;}
static TrainlogStatus ensure(unsigned int b,unsigned int d,uint32_t s,uint32_t p,const char *name,uint32_t *id,bool *created){Node *parent;char path[1024];(void)b;(void)d;(void)s;rebuild();parent=node(p);if(!parent)return TRAINLOG_STATUS_NOT_FOUND;snprintf(path,sizeof(path),"%s/%s",parent->path,name);if(mkdir(path,0700)==0)*created=true;else if(access(path,F_OK)==0)*created=false;else return TRAINLOG_STATUS_SYSTEM_ERROR;rebuild();size_t i;for(i=0;i<node_count;i++)if(strcmp(nodes[i].path,path)==0){*id=nodes[i].id;return TRAINLOG_STATUS_OK;}return TRAINLOG_STATUS_SYSTEM_ERROR;}
static int copy(const char *a,const char *b){FILE *in=fopen(a,"rb"),*out=fopen(b,"wb");char buf[8192];size_t n;if(!in||!out)return -1;while((n=fread(buf,1,sizeof(buf),in))>0)if(fwrite(buf,1,n,out)!=n)return -1;return fclose(in)|fclose(out);}
static TrainlogStatus receive(unsigned int b,unsigned int d,uint32_t id,const char *path){Node *n;(void)b;(void)d;rebuild();n=node(id);return n&&copy(n->path,path)==0?TRAINLOG_STATUS_OK:TRAINLOG_STATUS_SYSTEM_ERROR;}
static TrainlogStatus send_file(unsigned int b,unsigned int d,uint32_t s,uint32_t p,const char *local,const char *name,uint32_t *id){Node *parent;char path[1024];(void)b;(void)d;(void)s;rebuild();parent=node(p);if(!parent)return TRAINLOG_STATUS_NOT_FOUND;snprintf(path,sizeof(path),"%s/%s",parent->path,name);if(copy(local,path)!=0)return TRAINLOG_STATUS_SYSTEM_ERROR;rebuild();size_t i;for(i=0;i<node_count;i++)if(strcmp(nodes[i].path,path)==0){*id=nodes[i].id;return TRAINLOG_STATUS_OK;}return TRAINLOG_STATUS_SYSTEM_ERROR;}
static TrainlogStatus remove_object(unsigned int b,unsigned int d,uint32_t id){Node *n;(void)b;(void)d;rebuild();n=node(id);return n&&unlink(n->path)==0?TRAINLOG_STATUS_OK:TRAINLOG_STATUS_SYSTEM_ERROR;}
static TrainlogStatus rename_object(unsigned int b,unsigned int d,uint32_t id,const char *name){Node *n;char path[1024],*slash;(void)b;(void)d;rebuild();n=node(id);if(!n)return TRAINLOG_STATUS_NOT_FOUND;snprintf(path,sizeof(path),"%s",n->path);slash=strrchr(path,'/');if(!slash)return TRAINLOG_STATUS_SYSTEM_ERROR;snprintf(slash+1,(size_t)(path+sizeof(path)-(slash+1)),"%s",name);return rename(n->path,path)==0?TRAINLOG_STATUS_OK:TRAINLOG_STATUS_SYSTEM_ERROR;}
static const TrainlogGenerationMtpIo io={devices,storages,children,ensure,receive,send_file,remove_object,rename_object};
int main(int argc,char **argv){char diag[1024]={0};TrainlogStatus status;remote=getenv("TRAINLOG_MTP_DOUBLE_ROOT");if(argc!=4||!remote)return 64;if(strcmp(argv[1],"pull")==0)status=trainlog_generation_mtp_pull(&io,argv[2],argv[3],diag,sizeof(diag));else if(strcmp(argv[1],"push")==0)status=trainlog_generation_mtp_push(&io,argv[2],argv[3],diag,sizeof(diag));else return 64;if(status!=TRAINLOG_STATUS_OK){fprintf(stderr,"double failed %d %s\n",status,diag);return 2;}return 0;}
typedef struct Node {
uint32_t id, parent;
bool folder;
char path[1024], name[256];
uint64_t size;
} Node;
static Node nodes[2048];
static size_t node_count;
static const char *remote;
static uint32_t scan(const char *path, uint32_t parent) {
DIR *d;
struct dirent *e;
uint32_t self = (uint32_t)(node_count + 1U);
Node *n = &nodes[node_count++];
struct stat st;
lstat(path, &st);
n->id = self;
n->parent = parent;
n->folder = S_ISDIR(st.st_mode);
n->size = (uint64_t)st.st_size;
snprintf(n->path, sizeof(n->path), "%s", path);
const char *slash = strrchr(path, '/');
snprintf(n->name, sizeof(n->name), "%s", slash ? slash + 1 : path);
if (n->folder && (d = opendir(path)) != NULL) {
while ((e = readdir(d)))
if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..")) {
char child[1024];
snprintf(child, sizeof(child), "%s/%s", path, e->d_name);
scan(child, self);
}
closedir(d);
}
return self;
}
static void rebuild(void) {
node_count = 0U;
scan(remote, UINT32_MAX);
}
static Node *node(uint32_t id) {
size_t i;
for (i = 0; i < node_count; i++)
if (nodes[i].id == id)
return &nodes[i];
return NULL;
}
static TrainlogStatus devices(TrainlogUsbDevice *o, size_t c, size_t *n) {
if (c < 1U)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
memset(o, 0, sizeof(*o));
o->bus_number = 1;
o->device_number = 1;
*n = 1;
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus
storages(unsigned int b, unsigned int d, TrainlogMtpStorage *o, size_t c, size_t *n) {
(void)b;
(void)d;
if (c < 1U)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
memset(o, 0, sizeof(*o));
o->storage_id = 1;
*n = 1;
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus children(unsigned int b,
unsigned int d,
uint32_t s,
uint32_t p,
TrainlogMtpEntry *o,
size_t c,
size_t *n) {
size_t i, k = 0;
(void)b;
(void)d;
(void)s;
rebuild();
if (p == UINT32_MAX)
p = nodes[0].id;
for (i = 0; i < node_count; i++)
if (nodes[i].parent == p) {
if (k >= c)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
memset(&o[k], 0, sizeof(o[k]));
o[k].item_id = nodes[i].id;
o[k].parent_id = p;
o[k].storage_id = 1;
o[k].folder = nodes[i].folder;
o[k].size_bytes = nodes[i].size;
snprintf(o[k].name, sizeof(o[k].name), "%s", nodes[i].name);
k++;
}
*n = k;
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus ensure(unsigned int b,
unsigned int d,
uint32_t s,
uint32_t p,
const char *name,
uint32_t *id,
bool *created) {
Node *parent;
char path[1024];
(void)b;
(void)d;
(void)s;
rebuild();
parent = node(p);
if (!parent)
return TRAINLOG_STATUS_NOT_FOUND;
snprintf(path, sizeof(path), "%s/%s", parent->path, name);
if (mkdir(path, 0700) == 0)
*created = true;
else if (access(path, F_OK) == 0)
*created = false;
else
return TRAINLOG_STATUS_SYSTEM_ERROR;
rebuild();
size_t i;
for (i = 0; i < node_count; i++)
if (strcmp(nodes[i].path, path) == 0) {
*id = nodes[i].id;
return TRAINLOG_STATUS_OK;
}
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
static int copy(const char *a, const char *b) {
FILE *in = fopen(a, "rb"), *out = fopen(b, "wb");
char buf[8192];
size_t n;
if (!in || !out)
return -1;
while ((n = fread(buf, 1, sizeof(buf), in)) > 0)
if (fwrite(buf, 1, n, out) != n)
return -1;
return fclose(in) | fclose(out);
}
static TrainlogStatus receive(unsigned int b, unsigned int d, uint32_t id, const char *path) {
Node *n;
(void)b;
(void)d;
rebuild();
n = node(id);
return n && copy(n->path, path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
}
static TrainlogStatus send_file(unsigned int b,
unsigned int d,
uint32_t s,
uint32_t p,
const char *local,
const char *name,
uint32_t *id) {
Node *parent;
char path[1024];
(void)b;
(void)d;
(void)s;
rebuild();
parent = node(p);
if (!parent)
return TRAINLOG_STATUS_NOT_FOUND;
snprintf(path, sizeof(path), "%s/%s", parent->path, name);
if (copy(local, path) != 0)
return TRAINLOG_STATUS_SYSTEM_ERROR;
rebuild();
size_t i;
for (i = 0; i < node_count; i++)
if (strcmp(nodes[i].path, path) == 0) {
*id = nodes[i].id;
return TRAINLOG_STATUS_OK;
}
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
static TrainlogStatus remove_object(unsigned int b, unsigned int d, uint32_t id) {
Node *n;
(void)b;
(void)d;
rebuild();
n = node(id);
return n && unlink(n->path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
}
static TrainlogStatus rename_object(unsigned int b, unsigned int d, uint32_t id, const char *name) {
Node *n;
char path[1024], *slash;
(void)b;
(void)d;
rebuild();
n = node(id);
if (!n)
return TRAINLOG_STATUS_NOT_FOUND;
snprintf(path, sizeof(path), "%s", n->path);
slash = strrchr(path, '/');
if (!slash)
return TRAINLOG_STATUS_SYSTEM_ERROR;
snprintf(slash + 1, (size_t)(path + sizeof(path) - (slash + 1)), "%s", name);
return rename(n->path, path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
}
static const TrainlogGenerationMtpIo io = {
devices, storages, children, ensure, receive, send_file, remove_object, rename_object};
int main(int argc, char **argv) {
char diag[1024] = {0};
TrainlogStatus status;
remote = getenv("TRAINLOG_MTP_DOUBLE_ROOT");
if (argc != 4 || !remote)
return 64;
if (strcmp(argv[1], "pull") == 0)
status = trainlog_generation_mtp_pull(&io, argv[2], argv[3], diag, sizeof(diag));
else if (strcmp(argv[1], "push") == 0)
status = trainlog_generation_mtp_push(&io, argv[2], argv[3], diag, sizeof(diag));
else
return 64;
if (status != TRAINLOG_STATUS_OK) {
fprintf(stderr, "double failed %d %s\n", status, diag);
return 2;
}
return 0;
}

View file

@ -5,22 +5,277 @@
#include <unistd.h>
#include "trainlog/generation_mtp.h"
#define CHECK(x) do { if (!(x)) { fprintf(stderr,"CHECK %s:%d: %s\n",__FILE__,__LINE__,#x); return 1; } } while (0)
typedef struct Object { uint32_t id,parent; bool folder; char name[256]; char data[4096]; size_t size; } Object;
static Object objects[64]; static size_t object_count; static unsigned int device_count=1U; static char send_order[1024];
static void add(uint32_t id,uint32_t parent,bool folder,const char *name,const char *data){Object *o=&objects[object_count++];memset(o,0,sizeof(*o));o->id=id;o->parent=parent;o->folder=folder;snprintf(o->name,sizeof(o->name),"%s",name);if(data){o->size=strlen(data);memcpy(o->data,data,o->size);}}
static void reset(void){object_count=0U;device_count=1U;send_order[0]='\0';add(1,UINT32_MAX,true,"Documents",NULL);add(2,1,true,"Trainlog",NULL);add(3,2,false,"android-peer-v1.json","{\"format\":\"trainlog-sync-peer\",\"version\":1,\"peer_id\":\"peer_11111111-1111-4111-8111-111111111111\",\"capabilities\":[\"generation-manifest-v1\",\"generation-ack-v1\"]}");}
static TrainlogStatus devices(TrainlogUsbDevice *out,size_t cap,size_t *count){size_t i;if(cap<device_count)return TRAINLOG_STATUS_INVALID_ARGUMENT;*count=device_count;for(i=0;i<device_count;i++){memset(&out[i],0,sizeof(out[i]));out[i].bus_number=1U;out[i].device_number=(unsigned int)i+1U;}return TRAINLOG_STATUS_OK;}
static TrainlogStatus storages(unsigned int bus,unsigned int dev,TrainlogMtpStorage *out,size_t cap,size_t *count){(void)bus;if(cap<1U)return TRAINLOG_STATUS_INVALID_ARGUMENT;memset(out,0,sizeof(*out));out->storage_id=dev;*count=1U;return TRAINLOG_STATUS_OK;}
static TrainlogStatus children(unsigned int bus,unsigned int dev,uint32_t storage,uint32_t parent,TrainlogMtpEntry *out,size_t cap,size_t *count){size_t i,n=0;(void)bus;(void)storage;for(i=0;i<object_count;i++)if(objects[i].parent==parent && (dev==1U || objects[i].id<=3U)){if(n>=cap)return TRAINLOG_STATUS_INVALID_ARGUMENT;memset(&out[n],0,sizeof(out[n]));out[n].item_id=objects[i].id;out[n].parent_id=parent;out[n].storage_id=dev;out[n].folder=objects[i].folder;out[n].size_bytes=objects[i].size;snprintf(out[n].name,sizeof(out[n].name),"%s",objects[i].name);n++;}*count=n;return TRAINLOG_STATUS_OK;}
static TrainlogStatus ensure_folder(unsigned int b,unsigned int d,uint32_t s,uint32_t p,const char *name,uint32_t *id,bool *created){size_t i;(void)b;(void)d;(void)s;for(i=0;i<object_count;i++)if(objects[i].parent==p&&objects[i].folder&&strcmp(objects[i].name,name)==0){*id=objects[i].id;*created=false;return TRAINLOG_STATUS_OK;}*id=(uint32_t)(100U+object_count);*created=true;add(*id,p,true,name,NULL);return TRAINLOG_STATUS_OK;}
static Object *find(uint32_t id){size_t i;for(i=0;i<object_count;i++)if(objects[i].id==id)return &objects[i];return NULL;}
static TrainlogStatus receive(unsigned int b,unsigned int d,uint32_t id,const char *path){FILE *f;Object *o=find(id);size_t bytes;(void)b;(void)d;if(!o)return TRAINLOG_STATUS_NOT_FOUND;f=fopen(path,"wb");if(!f)return TRAINLOG_STATUS_SYSTEM_ERROR;bytes=id==9U?3U:o->size;if(fwrite(o->data,1,bytes,f)!=bytes){fclose(f);return TRAINLOG_STATUS_SYSTEM_ERROR;}return fclose(f)==0?TRAINLOG_STATUS_OK:TRAINLOG_STATUS_SYSTEM_ERROR;}
static TrainlogStatus send_file(unsigned int b,unsigned int d,uint32_t s,uint32_t p,const char *path,const char *name,uint32_t *id){FILE *f;Object *o;long size;(void)b;(void)d;(void)s;f=fopen(path,"rb");if(!f)return TRAINLOG_STATUS_SYSTEM_ERROR;fseek(f,0,SEEK_END);size=ftell(f);rewind(f);if(size<0||size>(long)sizeof(objects[0].data)){fclose(f);return TRAINLOG_STATUS_INVALID_ARGUMENT;}*id=(uint32_t)(100U+object_count);add(*id,p,false,name,NULL);o=&objects[object_count-1];o->size=(size_t)size;if(fread(o->data,1,o->size,f)!=o->size){fclose(f);return TRAINLOG_STATUS_SYSTEM_ERROR;}fclose(f);strncat(send_order,name,sizeof(send_order)-strlen(send_order)-2U);strncat(send_order,"\n",sizeof(send_order)-strlen(send_order)-1U);return TRAINLOG_STATUS_OK;}
static TrainlogStatus remove_object(unsigned int b,unsigned int d,uint32_t id){Object *o=find(id);(void)b;(void)d;if(!o)return TRAINLOG_STATUS_NOT_FOUND;o->name[0]='\0';return TRAINLOG_STATUS_OK;}
static TrainlogStatus rename_object(unsigned int b,unsigned int d,uint32_t id,const char *name){Object *o=find(id);(void)b;(void)d;if(!o)return TRAINLOG_STATUS_NOT_FOUND;snprintf(o->name,sizeof(o->name),"%s",name);return TRAINLOG_STATUS_OK;}
static const TrainlogGenerationMtpIo io={devices,storages,children,ensure_folder,receive,send_file,remove_object,rename_object};
static int write_file(const char *path,const char *value){FILE *f=fopen(path,"wb");CHECK(f!=NULL);CHECK(fwrite(value,1,strlen(value),f)==strlen(value));CHECK(fclose(f)==0);return 0;}
static int test_pull_and_manifest_last(void){char root[]="/tmp/trainlog-generation-mtp-XXXXXX",path[512],diag[256]={0};char *manifest;reset();CHECK(mkdtemp(root)!=NULL);add(4,2,true,"android-objects",NULL);add(5,4,true,"generations",NULL);add(6,5,true,"gen_11111111-1111-4111-8111-111111111111",NULL);add(7,6,false,"payload.json","payload");add(8,6,false,"manifest.json","manifest");CHECK(trainlog_generation_mtp_pull(&io,"peer_11111111-1111-4111-8111-111111111111",root,diag,sizeof(diag))==TRAINLOG_STATUS_OK);snprintf(path,sizeof(path),"%s/android-objects/generations/gen_11111111-1111-4111-8111-111111111111/payload.json",root);CHECK(access(path,R_OK)==0);snprintf(path,sizeof(path),"%s/out",root);CHECK(mkdir(path,0700)==0);char gen[512];snprintf(gen,sizeof(gen),"%s/generation",path);CHECK(mkdir(gen,0700)==0);char artifact[512];snprintf(artifact,sizeof(artifact),"%s/artifact.json",gen);CHECK(write_file(artifact,"a")==0);char marker[512];snprintf(marker,sizeof(marker),"%s/manifest.json",gen);CHECK(write_file(marker,"m")==0);CHECK(trainlog_generation_mtp_push(&io,"peer_11111111-1111-4111-8111-111111111111",root,diag,sizeof(diag))==TRAINLOG_STATUS_OK);manifest=strstr(send_order,"manifest.json\n");CHECK(manifest!=NULL);CHECK(strstr(send_order,"artifact.json\n")<manifest);return 0;}
static int test_ambiguity_and_truncation(void){char root[]="/tmp/trainlog-generation-mtp-error-XXXXXX",diag[256]={0},path[512];TrainlogStatus status;Object *peer;reset();CHECK(mkdtemp(root)!=NULL);device_count=2U;CHECK(trainlog_generation_mtp_pull(&io,"peer_11111111-1111-4111-8111-111111111111",root,diag,sizeof(diag))==TRAINLOG_STATUS_CONFLICT);reset();peer=find(3);CHECK(peer!=NULL);snprintf(peer->data,sizeof(peer->data),"not-json containing peer_11111111-1111-4111-8111-111111111111 generation-manifest-v1 generation-ack-v1");peer->size=strlen(peer->data);CHECK(trainlog_generation_mtp_pull(&io,"peer_11111111-1111-4111-8111-111111111111",root,diag,sizeof(diag))==TRAINLOG_STATUS_NOT_FOUND);reset();add(9,2,false,"bad.bin","abc");find(9)->size=99U;status=trainlog_generation_mtp_pull(&io,"peer_11111111-1111-4111-8111-111111111111",root,diag,sizeof(diag));CHECK(status!=TRAINLOG_STATUS_OK);snprintf(path,sizeof(path),"%s/bad.bin",root);CHECK(access(path,F_OK)!=0);return 0;}
int main(void){CHECK(test_pull_and_manifest_last()==0);CHECK(test_ambiguity_and_truncation()==0);return 0;}
#define CHECK(x) \
do { \
if (!(x)) { \
fprintf(stderr, "CHECK %s:%d: %s\n", __FILE__, __LINE__, #x); \
return 1; \
} \
} while (0)
typedef struct Object {
uint32_t id, parent;
bool folder;
char name[256];
char data[4096];
size_t size;
} Object;
static Object objects[64];
static size_t object_count;
static unsigned int device_count = 1U;
static char send_order[1024];
static void add(uint32_t id, uint32_t parent, bool folder, const char *name, const char *data) {
Object *o = &objects[object_count++];
memset(o, 0, sizeof(*o));
o->id = id;
o->parent = parent;
o->folder = folder;
snprintf(o->name, sizeof(o->name), "%s", name);
if (data) {
o->size = strlen(data);
memcpy(o->data, data, o->size);
}
}
static void reset(void) {
object_count = 0U;
device_count = 1U;
send_order[0] = '\0';
add(1, UINT32_MAX, true, "Documents", NULL);
add(2, 1, true, "Trainlog", NULL);
add(3,
2,
false,
"android-peer-v1.json",
"{\"format\":\"trainlog-sync-peer\",\"version\":1,\"peer_id\":\"peer_11111111-1111-4111-"
"8111-111111111111\",\"capabilities\":[\"generation-manifest-v1\",\"generation-ack-v1\"]}");
}
static TrainlogStatus devices(TrainlogUsbDevice *out, size_t cap, size_t *count) {
size_t i;
if (cap < device_count)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
*count = device_count;
for (i = 0; i < device_count; i++) {
memset(&out[i], 0, sizeof(out[i]));
out[i].bus_number = 1U;
out[i].device_number = (unsigned int)i + 1U;
}
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus
storages(unsigned int bus, unsigned int dev, TrainlogMtpStorage *out, size_t cap, size_t *count) {
(void)bus;
if (cap < 1U)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
memset(out, 0, sizeof(*out));
out->storage_id = dev;
*count = 1U;
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus children(unsigned int bus,
unsigned int dev,
uint32_t storage,
uint32_t parent,
TrainlogMtpEntry *out,
size_t cap,
size_t *count) {
size_t i, n = 0;
(void)bus;
(void)storage;
for (i = 0; i < object_count; i++)
if (objects[i].parent == parent && (dev == 1U || objects[i].id <= 3U)) {
if (n >= cap)
return TRAINLOG_STATUS_INVALID_ARGUMENT;
memset(&out[n], 0, sizeof(out[n]));
out[n].item_id = objects[i].id;
out[n].parent_id = parent;
out[n].storage_id = dev;
out[n].folder = objects[i].folder;
out[n].size_bytes = objects[i].size;
snprintf(out[n].name, sizeof(out[n].name), "%s", objects[i].name);
n++;
}
*count = n;
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus ensure_folder(unsigned int b,
unsigned int d,
uint32_t s,
uint32_t p,
const char *name,
uint32_t *id,
bool *created) {
size_t i;
(void)b;
(void)d;
(void)s;
for (i = 0; i < object_count; i++)
if (objects[i].parent == p && objects[i].folder && strcmp(objects[i].name, name) == 0) {
*id = objects[i].id;
*created = false;
return TRAINLOG_STATUS_OK;
}
*id = (uint32_t)(100U + object_count);
*created = true;
add(*id, p, true, name, NULL);
return TRAINLOG_STATUS_OK;
}
static Object *find(uint32_t id) {
size_t i;
for (i = 0; i < object_count; i++)
if (objects[i].id == id)
return &objects[i];
return NULL;
}
static TrainlogStatus receive(unsigned int b, unsigned int d, uint32_t id, const char *path) {
FILE *f;
Object *o = find(id);
size_t bytes;
(void)b;
(void)d;
if (!o)
return TRAINLOG_STATUS_NOT_FOUND;
f = fopen(path, "wb");
if (!f)
return TRAINLOG_STATUS_SYSTEM_ERROR;
bytes = id == 9U ? 3U : o->size;
if (fwrite(o->data, 1, bytes, f) != bytes) {
fclose(f);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
return fclose(f) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
}
static TrainlogStatus send_file(unsigned int b,
unsigned int d,
uint32_t s,
uint32_t p,
const char *path,
const char *name,
uint32_t *id) {
FILE *f;
Object *o;
long size;
(void)b;
(void)d;
(void)s;
f = fopen(path, "rb");
if (!f)
return TRAINLOG_STATUS_SYSTEM_ERROR;
fseek(f, 0, SEEK_END);
size = ftell(f);
rewind(f);
if (size < 0 || size > (long)sizeof(objects[0].data)) {
fclose(f);
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
*id = (uint32_t)(100U + object_count);
add(*id, p, false, name, NULL);
o = &objects[object_count - 1];
o->size = (size_t)size;
if (fread(o->data, 1, o->size, f) != o->size) {
fclose(f);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
fclose(f);
strncat(send_order, name, sizeof(send_order) - strlen(send_order) - 2U);
strncat(send_order, "\n", sizeof(send_order) - strlen(send_order) - 1U);
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus remove_object(unsigned int b, unsigned int d, uint32_t id) {
Object *o = find(id);
(void)b;
(void)d;
if (!o)
return TRAINLOG_STATUS_NOT_FOUND;
o->name[0] = '\0';
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus rename_object(unsigned int b, unsigned int d, uint32_t id, const char *name) {
Object *o = find(id);
(void)b;
(void)d;
if (!o)
return TRAINLOG_STATUS_NOT_FOUND;
snprintf(o->name, sizeof(o->name), "%s", name);
return TRAINLOG_STATUS_OK;
}
static const TrainlogGenerationMtpIo io = {
devices, storages, children, ensure_folder, receive, send_file, remove_object, rename_object};
static int write_file(const char *path, const char *value) {
FILE *f = fopen(path, "wb");
CHECK(f != NULL);
CHECK(fwrite(value, 1, strlen(value), f) == strlen(value));
CHECK(fclose(f) == 0);
return 0;
}
static int test_pull_and_manifest_last(void) {
char root[] = "/tmp/trainlog-generation-mtp-XXXXXX", path[512], diag[256] = {0};
char *manifest;
reset();
CHECK(mkdtemp(root) != NULL);
add(4, 2, true, "android-objects", NULL);
add(5, 4, true, "generations", NULL);
add(6, 5, true, "gen_11111111-1111-4111-8111-111111111111", NULL);
add(7, 6, false, "payload.json", "payload");
add(8, 6, false, "manifest.json", "manifest");
CHECK(trainlog_generation_mtp_pull(
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
TRAINLOG_STATUS_OK);
snprintf(path,
sizeof(path),
"%s/android-objects/generations/gen_11111111-1111-4111-8111-111111111111/payload.json",
root);
CHECK(access(path, R_OK) == 0);
snprintf(path, sizeof(path), "%s/out", root);
CHECK(mkdir(path, 0700) == 0);
char gen[512];
snprintf(gen, sizeof(gen), "%s/generation", path);
CHECK(mkdir(gen, 0700) == 0);
char artifact[512];
snprintf(artifact, sizeof(artifact), "%s/artifact.json", gen);
CHECK(write_file(artifact, "a") == 0);
char marker[512];
snprintf(marker, sizeof(marker), "%s/manifest.json", gen);
CHECK(write_file(marker, "m") == 0);
CHECK(trainlog_generation_mtp_push(
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
TRAINLOG_STATUS_OK);
manifest = strstr(send_order, "manifest.json\n");
CHECK(manifest != NULL);
CHECK(strstr(send_order, "artifact.json\n") < manifest);
return 0;
}
static int test_ambiguity_and_truncation(void) {
char root[] = "/tmp/trainlog-generation-mtp-error-XXXXXX", diag[256] = {0}, path[512];
TrainlogStatus status;
Object *peer;
reset();
CHECK(mkdtemp(root) != NULL);
device_count = 2U;
CHECK(trainlog_generation_mtp_pull(
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
TRAINLOG_STATUS_CONFLICT);
reset();
peer = find(3);
CHECK(peer != NULL);
snprintf(peer->data,
sizeof(peer->data),
"not-json containing peer_11111111-1111-4111-8111-111111111111 generation-manifest-v1 "
"generation-ack-v1");
peer->size = strlen(peer->data);
CHECK(trainlog_generation_mtp_pull(
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
TRAINLOG_STATUS_NOT_FOUND);
reset();
add(9, 2, false, "bad.bin", "abc");
find(9)->size = 99U;
status = trainlog_generation_mtp_pull(
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag));
CHECK(status != TRAINLOG_STATUS_OK);
snprintf(path, sizeof(path), "%s/bad.bin", root);
CHECK(access(path, F_OK) != 0);
return 0;
}
int main(void) {
CHECK(test_pull_and_manifest_last() == 0);
CHECK(test_ambiguity_and_truncation() == 0);
return 0;
}

View file

@ -20,24 +20,27 @@
#include "trainlog/web_server.h"
#include "web_assets.h"
#define CHECK(value) do { if (!(value)) { \
(void)fprintf(stderr, "CHECK failed at %s:%d: %s\n", \
__FILE__, __LINE__, #value); return false; } } while (0)
#define CHECK(value) \
do { \
if (!(value)) { \
(void)fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #value); \
return false; \
} \
} while (0)
static volatile sig_atomic_t child_stop;
static void stop_child(int signal_number)
{
static void stop_child(int signal_number) {
(void)signal_number;
child_stop = 1;
}
static int reserve_port(uint16_t *port, int keep_open)
{
static int reserve_port(uint16_t *port, int keep_open) {
struct sockaddr_in address;
socklen_t length = (socklen_t)sizeof(address);
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0) return -1;
if (socket_fd < 0)
return -1;
(void)memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
@ -56,8 +59,7 @@ static int reserve_port(uint16_t *port, int keep_open)
return socket_fd;
}
static int connect_with_retry(uint16_t port)
{
static int connect_with_retry(uint16_t port) {
struct sockaddr_in address;
struct timespec delay = {0, 10000000L};
size_t attempt;
@ -67,18 +69,17 @@ static int connect_with_retry(uint16_t port)
address.sin_port = htons(port);
for (attempt = 0U; attempt < 200U; ++attempt) {
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd >= 0 && connect(socket_fd,
(const struct sockaddr *)&address, sizeof(address)) == 0)
if (socket_fd >= 0 &&
connect(socket_fd, (const struct sockaddr *)&address, sizeof(address)) == 0)
return socket_fd;
if (socket_fd >= 0) (void)close(socket_fd);
if (socket_fd >= 0)
(void)close(socket_fd);
(void)nanosleep(&delay, NULL);
}
return -1;
}
static bool exchange(uint16_t port, const char *request, char *response,
size_t capacity)
{
static bool exchange(uint16_t port, const char *request, char *response, size_t capacity) {
size_t sent = 0U;
size_t used = 0U;
size_t length = strlen(request);
@ -92,7 +93,8 @@ static bool exchange(uint16_t port, const char *request, char *response,
(void)shutdown(socket_fd, SHUT_WR);
while (used + 1U < capacity) {
ssize_t count = recv(socket_fd, response + used, capacity - used - 1U, 0);
if (count == 0) break;
if (count == 0)
break;
CHECK(count > 0);
used += (size_t)count;
}
@ -101,19 +103,16 @@ static bool exchange(uint16_t port, const char *request, char *response,
return true;
}
static bool asset_path(const char *html, const char *suffix, char *output,
size_t capacity)
{
static bool asset_path(const char *html, const char *suffix, char *output, size_t capacity) {
const char *start = html;
size_t suffix_length = strlen(suffix);
while ((start = strstr(start, "/assets/")) != NULL) {
const char *end = start;
size_t length;
while (*end != '\0' && *end != '"' && *end != '\'' && *end != '<' &&
*end != '>') ++end;
while (*end != '\0' && *end != '"' && *end != '\'' && *end != '<' && *end != '>')
++end;
length = (size_t)(end - start);
if (length >= suffix_length &&
memcmp(end - suffix_length, suffix, suffix_length) == 0 &&
if (length >= suffix_length && memcmp(end - suffix_length, suffix, suffix_length) == 0 &&
length < capacity) {
(void)memcpy(output, start, length);
output[length] = '\0';
@ -124,18 +123,25 @@ static bool asset_path(const char *html, const char *suffix, char *output,
return false;
}
static bool response_header(const char *response, const char *name,
char *output, size_t capacity)
{
const char *start = strstr(response, name); const char *end; size_t length;
if (start == NULL) return false; start += strlen(name);
end = strstr(start, "\r\n"); if (end == NULL) return false;
length = (size_t)(end - start); if (length >= capacity) return false;
(void)memcpy(output, start, length); output[length] = '\0'; return true;
static bool response_header(const char *response, const char *name, char *output, size_t capacity) {
const char *start = strstr(response, name);
const char *end;
size_t length;
if (start == NULL)
return false;
start += strlen(name);
end = strstr(start, "\r\n");
if (end == NULL)
return false;
length = (size_t)(end - start);
if (length >= capacity)
return false;
(void)memcpy(output, start, length);
output[length] = '\0';
return true;
}
static bool test_http_contract(TrainlogDatabase *database)
{
static bool test_http_contract(TrainlogDatabase *database) {
char response[32768];
char large_request[12000];
char request[8192];
@ -156,44 +162,60 @@ static bool test_http_contract(TrainlogDatabase *database)
(void)sigemptyset(&action.sa_mask);
(void)sigaction(SIGTERM, &action, NULL);
child_stop = 0;
_exit(trainlog_web_server_run(database, "/tmp/trainlog-web-test.db", port, &child_stop,
diagnostic, sizeof(diagnostic)) == 0 ? 0 : 1);
_exit(trainlog_web_server_run(database,
"/tmp/trainlog-web-test.db",
port,
&child_stop,
diagnostic,
sizeof(diagnostic)) == 0
? 0
: 1);
}
CHECK(exchange(port,
"GET /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response, sizeof(response)));
"GET /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response,
sizeof(response)));
CHECK(strstr(response, "HTTP/1.1 200") != NULL);
CHECK(strstr(response, "Content-Type: application/json; charset=utf-8") != NULL);
CHECK(strstr(response, "X-Content-Type-Options: nosniff") != NULL);
CHECK(strstr(response, "Content-Security-Policy: default-src 'none'") != NULL);
CHECK(strstr(response, "Access-Control-Allow-Origin") == NULL);
CHECK(strstr(response,
"{\"api_version\":1,\"status\":\"ok\",\"product\":\"trainlog\","
"\"version\":\"0.1.2\"}") != NULL);
"{\"api_version\":1,\"status\":\"ok\",\"product\":\"trainlog\","
"\"version\":\"0.1.2\"}") != NULL);
{
char token[80];
CHECK(exchange(port,
"GET /api/v1/sync/status HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL);
"GET /api/v1/sync/status HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response,
sizeof(response)) &&
strstr(response, "HTTP/1.1 200") != NULL);
CHECK(strstr(response, "\"phase\":\"idle\"") != NULL);
CHECK(response_header(response, "X-Trainlog-CSRF-Token: ", token,
sizeof(token)) && strlen(token) == 64U);
(void)snprintf(request, sizeof(request),
CHECK(response_header(response, "X-Trainlog-CSRF-Token: ", token, sizeof(token)) &&
strlen(token) == 64U);
(void)snprintf(
request,
sizeof(request),
"POST /api/v1/sync HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Origin: http://127.0.0.1:%u\r\nContent-Type: application/json\r\n"
"X-Trainlog-CSRF-Token: %s\r\nContent-Length: 72\r\n\r\n"
"{\"request_id\":\"sy_11111111-1111-4111-8111-111111111111\",\"trigger\":\"web\"}",
(unsigned int)port, token);
(unsigned int)port,
token);
CHECK(exchange(port, request, response, sizeof(response)) &&
strstr(response, "HTTP/1.1 409") != NULL &&
strstr(response, "full_generation_disabled") != NULL);
strstr(response, "HTTP/1.1 409") != NULL &&
strstr(response, "full_generation_disabled") != NULL);
CHECK(exchange(port,
"GET /api/v1/sync HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response, sizeof(response)) && strstr(response, "HTTP/1.1 405") != NULL);
"GET /api/v1/sync HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response,
sizeof(response)) &&
strstr(response, "HTTP/1.1 405") != NULL);
}
CHECK(exchange(port,
"GET /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL);
"GET /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response,
sizeof(response)) &&
strstr(response, "HTTP/1.1 200") != NULL);
CHECK(strstr(response, "Content-Type: application/json; charset=utf-8") != NULL);
CHECK(strstr(response, "\"api_version\":1") != NULL);
CHECK(strstr(response, "\"no_persisted_executable_plan\"") != NULL);
@ -203,58 +225,94 @@ static bool test_http_contract(TrainlogDatabase *database)
{
TrainlogDashboardLayout layout;
char body[TRAINLOG_DASHBOARD_LAYOUT_JSON_CAPACITY];
char token[80]; char etag[32]; size_t body_size;
char token[80];
char etag[32];
size_t body_size;
CHECK(exchange(port,
"GET /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL);
"GET /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response,
sizeof(response)) &&
strstr(response, "HTTP/1.1 200") != NULL);
CHECK(strstr(response, "\"source\":\"default\"") != NULL);
CHECK(response_header(response, "ETag: ", etag, sizeof(etag)) && strcmp(etag, "\"0\"") == 0);
CHECK(response_header(response, "X-Trainlog-CSRF-Token: ", token, sizeof(token)) && strlen(token) == 64U);
trainlog_dashboard_layout_default(&layout); layout.tiles[3].y = 14U;
CHECK(trainlog_dashboard_layout_serialize(&layout,
TRAINLOG_DASHBOARD_LAYOUT_DEFAULT, false, body, sizeof(body), &body_size));
(void)snprintf(request, sizeof(request),
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Origin: http://127.0.0.1:%u\r\nContent-Type: application/json\r\n"
"If-Match: \"0\"\r\nX-Trainlog-CSRF-Token: wrong\r\n"
"Content-Length: %zu\r\n\r\n%s", (unsigned int)port, body_size, body);
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 403") != NULL);
(void)snprintf(request, sizeof(request),
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Origin: http://127.0.0.1:%u\r\nContent-Type: text/plain\r\n"
"If-Match: \"0\"\r\nX-Trainlog-CSRF-Token: %s\r\n"
"Content-Length: %zu\r\n\r\n%s", (unsigned int)port, token, body_size, body);
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 415") != NULL);
(void)snprintf(request, sizeof(request),
CHECK(response_header(response, "ETag: ", etag, sizeof(etag)) &&
strcmp(etag, "\"0\"") == 0);
CHECK(response_header(response, "X-Trainlog-CSRF-Token: ", token, sizeof(token)) &&
strlen(token) == 64U);
trainlog_dashboard_layout_default(&layout);
layout.tiles[3].y = 14U;
CHECK(trainlog_dashboard_layout_serialize(
&layout, TRAINLOG_DASHBOARD_LAYOUT_DEFAULT, false, body, sizeof(body), &body_size));
(void)snprintf(request,
sizeof(request),
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Origin: http://127.0.0.1:%u\r\nContent-Type: application/json\r\n"
"If-Match: \"0\"\r\nX-Trainlog-CSRF-Token: wrong\r\n"
"Content-Length: %zu\r\n\r\n%s",
(unsigned int)port,
body_size,
body);
CHECK(exchange(port, request, response, sizeof(response)) &&
strstr(response, "HTTP/1.1 403") != NULL);
(void)snprintf(request,
sizeof(request),
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Origin: http://127.0.0.1:%u\r\nContent-Type: text/plain\r\n"
"If-Match: \"0\"\r\nX-Trainlog-CSRF-Token: %s\r\n"
"Content-Length: %zu\r\n\r\n%s",
(unsigned int)port,
token,
body_size,
body);
CHECK(exchange(port, request, response, sizeof(response)) &&
strstr(response, "HTTP/1.1 415") != NULL);
(void)snprintf(
request,
sizeof(request),
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Origin: http://127.0.0.1:%u\r\nContent-Type: application/json\r\n"
"If-Match: \"0\"\r\nX-Trainlog-CSRF-Token: %s\r\nContent-Length: %zu\r\n\r\n%s",
(unsigned int)port, token, body_size, body);
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL);
CHECK(strstr(response, "\"revision\":1") != NULL && strstr(response, "\"source\":\"persisted\"") != NULL);
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 412") != NULL);
(void)snprintf(request, sizeof(request),
"DELETE /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Origin: http://trainlog.perf\r\nIf-Match: \"0\"\r\n"
"X-Trainlog-CSRF-Token: %s\r\nContent-Length: 0\r\n\r\n", token);
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 412") != NULL);
(void)snprintf(request, sizeof(request),
"DELETE /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Origin: http://trainlog.perf\r\nIf-Match: \"1\"\r\n"
"X-Trainlog-CSRF-Token: %s\r\nContent-Length: 0\r\n\r\n", token);
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL &&
strstr(response, "\"source\":\"default\"") != NULL);
(unsigned int)port,
token,
body_size,
body);
CHECK(exchange(port, request, response, sizeof(response)) &&
strstr(response, "HTTP/1.1 200") != NULL);
CHECK(strstr(response, "\"revision\":1") != NULL &&
strstr(response, "\"source\":\"persisted\"") != NULL);
CHECK(exchange(port, request, response, sizeof(response)) &&
strstr(response, "HTTP/1.1 412") != NULL);
(void)snprintf(request,
sizeof(request),
"DELETE /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Origin: http://trainlog.perf\r\nIf-Match: \"0\"\r\n"
"X-Trainlog-CSRF-Token: %s\r\nContent-Length: 0\r\n\r\n",
token);
CHECK(exchange(port, request, response, sizeof(response)) &&
strstr(response, "HTTP/1.1 412") != NULL);
(void)snprintf(request,
sizeof(request),
"DELETE /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Origin: http://trainlog.perf\r\nIf-Match: \"1\"\r\n"
"X-Trainlog-CSRF-Token: %s\r\nContent-Length: 0\r\n\r\n",
token);
CHECK(exchange(port, request, response, sizeof(response)) &&
strstr(response, "HTTP/1.1 200") != NULL &&
strstr(response, "\"source\":\"default\"") != NULL);
CHECK(exchange(port,
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Content-Type: application/json\r\nContent-Length: 2\r\n\r\n{}",
response, sizeof(response)) && strstr(response, "HTTP/1.1 403") != NULL);
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Content-Type: application/json\r\nContent-Length: 2\r\n\r\n{}",
response,
sizeof(response)) &&
strstr(response, "HTTP/1.1 403") != NULL);
}
CHECK(exchange(port,
"POST /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\n\r\n",
response, sizeof(response)) && strstr(response, "HTTP/1.1 405") != NULL &&
strstr(response, "Allow: GET") != NULL);
CHECK(exchange(port, "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL);
CHECK(
exchange(port,
"POST /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\n\r\n",
response,
sizeof(response)) &&
strstr(response, "HTTP/1.1 405") != NULL && strstr(response, "Allow: GET") != NULL);
CHECK(exchange(port, "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", response, sizeof(response)) &&
strstr(response, "HTTP/1.1 200") != NULL);
CHECK(strstr(response, "Content-Type: text/html; charset=utf-8") != NULL);
CHECK(strstr(response, "Cache-Control: no-cache") != NULL);
CHECK(strstr(response, "Content-Security-Policy: default-src 'self'") != NULL);
@ -263,81 +321,91 @@ static bool test_http_contract(TrainlogDatabase *database)
CHECK(strstr(response, "<title>Trainlog</title>") != NULL);
CHECK(asset_path(response, ".js", javascript_path, sizeof(javascript_path)));
CHECK(asset_path(response, ".css", stylesheet_path, sizeof(stylesheet_path)));
(void)snprintf(request, sizeof(request),
"GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", javascript_path);
(void)snprintf(
request, sizeof(request), "GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", javascript_path);
CHECK(exchange(port, request, response, sizeof(response)) &&
strstr(response, "HTTP/1.1 200") != NULL);
strstr(response, "HTTP/1.1 200") != NULL);
CHECK(strstr(response, "Content-Type: text/javascript; charset=utf-8") != NULL);
CHECK(strstr(response,
"Cache-Control: public, max-age=31536000, immutable") != NULL);
CHECK(strstr(response, "Cache-Control: public, max-age=31536000, immutable") != NULL);
CHECK(strstr(response, "ETag: \"sha256-") != NULL);
(void)snprintf(request, sizeof(request),
"GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", stylesheet_path);
(void)snprintf(
request, sizeof(request), "GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", stylesheet_path);
CHECK(exchange(port, request, response, sizeof(response)) &&
strstr(response, "Content-Type: text/css; charset=utf-8") != NULL);
strstr(response, "Content-Type: text/css; charset=utf-8") != NULL);
CHECK(
exchange(
port, "GET /analyse HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", response, sizeof(response)) &&
strstr(response, "<title>Trainlog</title>") != NULL);
CHECK(exchange(port,
"GET /analyse HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response, sizeof(response)) && strstr(response, "<title>Trainlog</title>") != NULL);
"GET /api/v1/unknown HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response,
sizeof(response)) &&
strstr(response, "HTTP/1.1 404") != NULL &&
strstr(response, "<title>Trainlog</title>") == NULL);
CHECK(exchange(port,
"GET /api/v1/unknown HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response, sizeof(response)) && strstr(response, "HTTP/1.1 404") != NULL &&
strstr(response, "<title>Trainlog</title>") == NULL);
"GET /assets/unknown.js HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response,
sizeof(response)) &&
strstr(response, "HTTP/1.1 404") != NULL);
CHECK(exchange(port,
"GET /assets/unknown.js HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response, sizeof(response)) && strstr(response, "HTTP/1.1 404") != NULL);
CHECK(exchange(port,
"GET /assets/../index.html HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response, sizeof(response)) && strstr(response, "HTTP/1.1 404") != NULL);
(void)snprintf(request, sizeof(request),
"HEAD %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", javascript_path);
"GET /assets/../index.html HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response,
sizeof(response)) &&
strstr(response, "HTTP/1.1 404") != NULL);
(void)snprintf(
request, sizeof(request), "HEAD %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", javascript_path);
CHECK(exchange(port, request, response, sizeof(response)) &&
strstr(response, "HTTP/1.1 200") != NULL &&
strstr(response, "Content-Type: text/javascript; charset=utf-8") != NULL);
strstr(response, "HTTP/1.1 200") != NULL &&
strstr(response, "Content-Type: text/javascript; charset=utf-8") != NULL);
CHECK(exchange(port,
"POST /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\n\r\n",
response, sizeof(response)) && strstr(response, "HTTP/1.1 405") != NULL);
"POST /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\n\r\n",
response,
sizeof(response)) &&
strstr(response, "HTTP/1.1 405") != NULL);
CHECK(strstr(response, "Allow: GET") != NULL);
CHECK(exchange(port,
"GET /api/v1/health HTTP/1.1\r\nHost: foreign.example\r\n\r\n",
response, sizeof(response)) && strstr(response, "HTTP/1.1 400") != NULL);
(void)snprintf(large_request, sizeof(large_request),
"POST /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Content-Length: 5000\r\n\r\n%05000d", 0);
"GET /api/v1/health HTTP/1.1\r\nHost: foreign.example\r\n\r\n",
response,
sizeof(response)) &&
strstr(response, "HTTP/1.1 400") != NULL);
(void)snprintf(large_request,
sizeof(large_request),
"POST /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\n"
"Content-Length: 5000\r\n\r\n%05000d",
0);
CHECK(exchange(port, large_request, response, sizeof(response)) &&
strstr(response, "HTTP/1.1 413") != NULL);
strstr(response, "HTTP/1.1 413") != NULL);
(void)memset(large_request, 'a', sizeof(large_request));
(void)snprintf(large_request, 128U,
"GET /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\nX-Large: ");
(void)snprintf(
large_request, 128U, "GET /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\nX-Large: ");
{
size_t prefix = strlen(large_request);
(void)memset(large_request + prefix, 'a', 8500U);
(void)memcpy(large_request + prefix + 8500U, "\r\n\r\n", 5U);
}
CHECK(exchange(port, large_request, response, sizeof(response)) &&
(strstr(response, "HTTP/1.1 431") != NULL ||
strstr(response, "HTTP/1.1 400") != NULL));
(strstr(response, "HTTP/1.1 431") != NULL || strstr(response, "HTTP/1.1 400") != NULL));
CHECK(kill(child, SIGTERM) == 0);
CHECK(waitpid(child, &status, 0) == child);
CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0);
return true;
}
static bool test_port_in_use(TrainlogDatabase *database)
{
static bool test_port_in_use(TrainlogDatabase *database) {
volatile sig_atomic_t stop = 0;
char diagnostic[256];
uint16_t port;
int occupied = reserve_port(&port, 1);
CHECK(occupied >= 0);
CHECK(trainlog_web_server_run(database, "/tmp/trainlog-web-test.db", port, &stop, diagnostic,
sizeof(diagnostic)) != 0);
CHECK(trainlog_web_server_run(
database, "/tmp/trainlog-web-test.db", port, &stop, diagnostic, sizeof(diagnostic)) !=
0);
CHECK(strstr(diagnostic, "127.0.0.1") != NULL);
(void)close(occupied);
return true;
}
static bool test_dashboard_core_error_translation(void)
{
static bool test_dashboard_core_error_translation(void) {
char path[] = "/tmp/trainlog-web-error-XXXXXX";
char response[4096];
TrainlogDatabase *database = NULL;
@ -361,12 +429,15 @@ static bool test_dashboard_core_error_translation(void)
if (child == 0) {
volatile sig_atomic_t stop = 0;
char diagnostic[256];
_exit(trainlog_web_server_run(database, path, port, &stop, diagnostic,
sizeof(diagnostic)) == 0 ? 0 : 1);
_exit(trainlog_web_server_run(
database, path, port, &stop, diagnostic, sizeof(diagnostic)) == 0
? 0
: 1);
}
CHECK(exchange(port,
"GET /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response, sizeof(response)));
"GET /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
response,
sizeof(response)));
CHECK(strstr(response, "HTTP/1.1 500") != NULL);
CHECK(strstr(response, "\"dashboard_unavailable\"") != NULL);
CHECK(kill(child, SIGTERM) == 0);
@ -376,8 +447,7 @@ static bool test_dashboard_core_error_translation(void)
return true;
}
int main(void)
{
int main(void) {
char config_root[] = "/tmp/trainlog-web-config-XXXXXX";
TrainlogDatabase *database = NULL;
bool passed;
@ -388,11 +458,16 @@ int main(void)
if (!trainlog_web_assets_available()) {
volatile sig_atomic_t stop = 0;
char diagnostic[256];
passed = trainlog_web_server_run(database, "/tmp/trainlog-web-test.db", 8080U, &stop, diagnostic,
sizeof(diagnostic)) != 0 && strstr(diagnostic, "frontend") != NULL;
passed = trainlog_web_server_run(database,
"/tmp/trainlog-web-test.db",
8080U,
&stop,
diagnostic,
sizeof(diagnostic)) != 0 &&
strstr(diagnostic, "frontend") != NULL;
} else {
passed = test_http_contract(database) && test_port_in_use(database) &&
test_dashboard_core_error_translation();
test_dashboard_core_error_translation();
}
trainlog_database_close(database);
return passed ? 0 : 1;

View file

@ -2,21 +2,32 @@
#include <string.h>
#include "trainlog/generation_mtp.h"
int main(int argc, char **argv)
{
char diagnostic[1024] = {0}; TrainlogStatus status;
int main(int argc, char **argv) {
char diagnostic[1024] = {0};
TrainlogStatus status;
if (argc == 2 && strcmp(argv[1], "--help") == 0) {
(void)puts("usage: trainlog-generation-mtp-adapter pull|push EXPECTED_PEER LOCAL_ROOT");
return 0;
}
if (argc != 4) return 64;
if (strcmp(argv[1], "pull") == 0) status = trainlog_generation_mtp_pull(
trainlog_generation_mtp_production_io(), argv[2], argv[3], diagnostic, sizeof(diagnostic));
else if (strcmp(argv[1], "push") == 0) status = trainlog_generation_mtp_push(
trainlog_generation_mtp_production_io(), argv[2], argv[3], diagnostic, sizeof(diagnostic));
else return 64;
if (argc != 4)
return 64;
if (strcmp(argv[1], "pull") == 0)
status = trainlog_generation_mtp_pull(trainlog_generation_mtp_production_io(),
argv[2],
argv[3],
diagnostic,
sizeof(diagnostic));
else if (strcmp(argv[1], "push") == 0)
status = trainlog_generation_mtp_push(trainlog_generation_mtp_production_io(),
argv[2],
argv[3],
diagnostic,
sizeof(diagnostic));
else
return 64;
if (status != TRAINLOG_STATUS_OK) {
(void)fprintf(stderr, "MTP adapter failed status=%d diagnostic=%s\n", (int)status, diagnostic);
(void)fprintf(
stderr, "MTP adapter failed status=%d diagnostic=%s\n", (int)status, diagnostic);
return 2;
}
return 0;