diff --git a/tui/include/trainlog/generation_mtp.h b/tui/include/trainlog/generation_mtp.h index e435006..b1fbf8c 100644 --- a/tui/include/trainlog/generation_mtp.h +++ b/tui/include/trainlog/generation_mtp.h @@ -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 diff --git a/tui/src/dashboard_layout.c b/tui/src/dashboard_layout.c index 7d190c5..b5a4390 100644 --- a/tui/src/dashboard_layout.c +++ b/tui/src/dashboard_layout.c @@ -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(¤t, &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(¤t, &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; diff --git a/tui/src/generation_mtp.c b/tui/src/generation_mtp.c index c7f48a0..a397617 100644 --- a/tui/src/generation_mtp.c +++ b/tui/src/generation_mtp.c @@ -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; +} diff --git a/tui/src/json_writer.c b/tui/src/json_writer.c index 761c01e..766eb02 100644 --- a/tui/src/json_writer.c +++ b/tui/src/json_writer.c @@ -5,33 +5,87 @@ #include 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)n0&&(size_t)nfailed=true;return false;}n=snprintf(b,sizeof(b),"%.17g",v);return n>0&&(size_t)n=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,¤t)==NULL)return TRAINLOG_STATUS_DATABASE_ERROR; - out->activity_count=TRAINLOG_WEB_ACTIVITY_DAYS; - for(i=0U;iactivity[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;iactivity_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, ¤t) == 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_secondhas_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; } diff --git a/tui/src/web_dashboard_json.c b/tui/src/web_dashboard_json.c index fd6ac33..c571f18 100644 --- a/tui/src/web_dashboard_json.c +++ b/tui/src/web_dashboard_json.c @@ -3,26 +3,251 @@ #include -#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_kgweight_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;ilast_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;iactivity_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;iprogression.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;izone_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;imax_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;imuscle_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; } diff --git a/tui/tests/generation_mtp_adapter_double.c b/tui/tests/generation_mtp_adapter_double.c index 97d887c..f6ea3b8 100644 --- a/tui/tests/generation_mtp_adapter_double.c +++ b/tui/tests/generation_mtp_adapter_double.c @@ -6,19 +6,221 @@ #include #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;ibus_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=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;i0)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&©(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;ipath)==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; +} diff --git a/tui/tests/test_generation_mtp.c b/tui/tests/test_generation_mtp.c index c75cd53..e58eb31 100644 --- a/tui/tests/test_generation_mtp.c +++ b/tui/tests/test_generation_mtp.c @@ -5,22 +5,277 @@ #include #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(capstorage_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=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;isize;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")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; +} diff --git a/tui/tests/test_web_server.c b/tui/tests/test_web_server.c index 9ce7597..a970ca1 100644 --- a/tui/tests/test_web_server.c +++ b/tui/tests/test_web_server.c @@ -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, "Trainlog") != 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, "Trainlog") != NULL); CHECK(exchange(port, - "GET /analyse HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", - response, sizeof(response)) && strstr(response, "Trainlog") != 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, "Trainlog") == 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, "Trainlog") == 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; diff --git a/tui/tools/generation_mtp_adapter.c b/tui/tools/generation_mtp_adapter.c index 6c381e7..12774e2 100644 --- a/tui/tools/generation_mtp_adapter.c +++ b/tui/tools/generation_mtp_adapter.c @@ -2,21 +2,32 @@ #include #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;