style(c): expand compact synchronization code
This commit is contained in:
parent
34c6c0b369
commit
6cd6219ac5
10 changed files with 1893 additions and 564 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,33 +5,87 @@
|
|||
#include <utf8proc.h>
|
||||
|
||||
static bool append(TrainlogJsonWriter *w, const char *s, size_t n) {
|
||||
if (w == NULL || s == NULL || w->failed || n > w->capacity - (w->size < w->capacity ? w->size : w->capacity) - 1U) {
|
||||
if (w != NULL) w->failed = true; return false;
|
||||
if (w == NULL || s == NULL || w->failed ||
|
||||
n > w->capacity - (w->size < w->capacity ? w->size : w->capacity) - 1U) {
|
||||
if (w != NULL)
|
||||
w->failed = true;
|
||||
return false;
|
||||
}
|
||||
(void)memcpy(w->data + w->size, s, n); w->size += n; w->data[w->size] = '\0'; return true;
|
||||
(void)memcpy(w->data + w->size, s, n);
|
||||
w->size += n;
|
||||
w->data[w->size] = '\0';
|
||||
return true;
|
||||
}
|
||||
void trainlog_json_init(TrainlogJsonWriter *w, char *data, size_t capacity) {
|
||||
if (w == NULL) return; w->data=data; w->capacity=capacity; w->size=0U; w->failed=data==NULL||capacity==0U; if (!w->failed) data[0]='\0';
|
||||
if (w == NULL)
|
||||
return;
|
||||
w->data = data;
|
||||
w->capacity = capacity;
|
||||
w->size = 0U;
|
||||
w->failed = data == NULL || capacity == 0U;
|
||||
if (!w->failed)
|
||||
data[0] = '\0';
|
||||
}
|
||||
bool trainlog_json_raw(TrainlogJsonWriter *w,const char *v){return v!=NULL&&append(w,v,strlen(v));}
|
||||
bool trainlog_json_string(TrainlogJsonWriter *w,const char *v){
|
||||
const unsigned char *p=(const unsigned char *)v; char escape[7];
|
||||
if(v==NULL||!append(w,"\"",1U))return false;
|
||||
while(*p!='\0'){
|
||||
bool trainlog_json_raw(TrainlogJsonWriter *w, const char *v) {
|
||||
return v != NULL && append(w, v, strlen(v));
|
||||
}
|
||||
bool trainlog_json_string(TrainlogJsonWriter *w, const char *v) {
|
||||
const unsigned char *p = (const unsigned char *)v;
|
||||
char escape[7];
|
||||
if (v == NULL || !append(w, "\"", 1U))
|
||||
return false;
|
||||
while (*p != '\0') {
|
||||
utf8proc_int32_t codepoint;
|
||||
utf8proc_ssize_t sequence=utf8proc_iterate(p,-1,&codepoint);
|
||||
utf8proc_ssize_t sequence = utf8proc_iterate(p, -1, &codepoint);
|
||||
/* CONTRACT: valid UTF-8 is preserved byte-for-byte; corrupt storage
|
||||
* must fail serialization instead of producing invalid JSON. */
|
||||
(void)codepoint;
|
||||
if(sequence<=0){w->failed=true;return false;}
|
||||
if(*p=='\"'||*p=='\\'){escape[0]='\\';escape[1]=(char)*p;if(!append(w,escape,2U))return false;}
|
||||
else if(*p=='\b'||*p=='\f'||*p=='\n'||*p=='\r'||*p=='\t'){const char codes[]="bfnrt";const char chars[]="\b\f\n\r\t";size_t i;escape[0]='\\';for(i=0U;i<5U&&chars[i]!=(char)*p;++i){}escape[1]=codes[i];if(!append(w,escape,2U))return false;}
|
||||
else if(*p<0x20U){int n=snprintf(escape,sizeof(escape),"\\u%04x",(unsigned int)*p);if(n!=6||!append(w,escape,6U))return false;}
|
||||
else if(!append(w,(const char *)p,(size_t)sequence))return false;
|
||||
p+=(size_t)sequence;
|
||||
if (sequence <= 0) {
|
||||
w->failed = true;
|
||||
return false;
|
||||
}
|
||||
if (*p == '\"' || *p == '\\') {
|
||||
escape[0] = '\\';
|
||||
escape[1] = (char)*p;
|
||||
if (!append(w, escape, 2U))
|
||||
return false;
|
||||
} else if (*p == '\b' || *p == '\f' || *p == '\n' || *p == '\r' || *p == '\t') {
|
||||
const char codes[] = "bfnrt";
|
||||
const char chars[] = "\b\f\n\r\t";
|
||||
size_t i;
|
||||
escape[0] = '\\';
|
||||
for (i = 0U; i < 5U && chars[i] != (char)*p; ++i) {
|
||||
}
|
||||
escape[1] = codes[i];
|
||||
if (!append(w, escape, 2U))
|
||||
return false;
|
||||
} else if (*p < 0x20U) {
|
||||
int n = snprintf(escape, sizeof(escape), "\\u%04x", (unsigned int)*p);
|
||||
if (n != 6 || !append(w, escape, 6U))
|
||||
return false;
|
||||
} else if (!append(w, (const char *)p, (size_t)sequence))
|
||||
return false;
|
||||
p += (size_t)sequence;
|
||||
}
|
||||
return append(w,"\"",1U);
|
||||
return append(w, "\"", 1U);
|
||||
}
|
||||
bool trainlog_json_size(TrainlogJsonWriter *w, size_t v) {
|
||||
char b[32];
|
||||
int n = snprintf(b, sizeof(b), "%zu", v);
|
||||
return n > 0 && (size_t)n < sizeof(b) && append(w, b, (size_t)n);
|
||||
}
|
||||
bool trainlog_json_i64(TrainlogJsonWriter *w, long long v) {
|
||||
char b[32];
|
||||
int n = snprintf(b, sizeof(b), "%lld", v);
|
||||
return n > 0 && (size_t)n < sizeof(b) && append(w, b, (size_t)n);
|
||||
}
|
||||
bool trainlog_json_double(TrainlogJsonWriter *w, double v) {
|
||||
char b[64];
|
||||
int n;
|
||||
if (!isfinite(v)) {
|
||||
w->failed = true;
|
||||
return false;
|
||||
}
|
||||
n = snprintf(b, sizeof(b), "%.17g", v);
|
||||
return n > 0 && (size_t)n < sizeof(b) && append(w, b, (size_t)n);
|
||||
}
|
||||
bool trainlog_json_size(TrainlogJsonWriter*w,size_t v){char b[32];int n=snprintf(b,sizeof(b),"%zu",v);return n>0&&(size_t)n<sizeof(b)&&append(w,b,(size_t)n);}
|
||||
bool trainlog_json_i64(TrainlogJsonWriter*w,long long v){char b[32];int n=snprintf(b,sizeof(b),"%lld",v);return n>0&&(size_t)n<sizeof(b)&&append(w,b,(size_t)n);}
|
||||
bool trainlog_json_double(TrainlogJsonWriter*w,double v){char b[64];int n;if(!isfinite(v)){w->failed=true;return false;}n=snprintf(b,sizeof(b),"%.17g",v);return n>0&&(size_t)n<sizeof(b)&&append(w,b,(size_t)n);}
|
||||
|
|
|
|||
|
|
@ -16,92 +16,368 @@
|
|||
#define SECONDS_PER_DAY INT64_C(86400)
|
||||
|
||||
static bool copy_column(sqlite3_stmt *s, int column, char *out, size_t cap) {
|
||||
const unsigned char *text; int bytes;
|
||||
if (sqlite3_column_type(s,column)!=SQLITE_TEXT) return false;
|
||||
text=sqlite3_column_text(s,column); bytes=sqlite3_column_bytes(s,column);
|
||||
if(text==NULL||bytes<0||(size_t)bytes>=cap)return false;
|
||||
(void)memcpy(out,text,(size_t)bytes);out[bytes]='\0';return true;
|
||||
const unsigned char *text;
|
||||
int bytes;
|
||||
if (sqlite3_column_type(s, column) != SQLITE_TEXT)
|
||||
return false;
|
||||
text = sqlite3_column_text(s, column);
|
||||
bytes = sqlite3_column_bytes(s, column);
|
||||
if (text == NULL || bytes < 0 || (size_t)bytes >= cap)
|
||||
return false;
|
||||
(void)memcpy(out, text, (size_t)bytes);
|
||||
out[bytes] = '\0';
|
||||
return true;
|
||||
}
|
||||
static bool read_size(sqlite3_stmt*s,int c,size_t*out){sqlite3_int64 v;if(sqlite3_column_type(s,c)!=SQLITE_INTEGER)return false;v=sqlite3_column_int64(s,c);if(v<0||(uint64_t)v>(uint64_t)SIZE_MAX)return false;*out=(size_t)v;return true;}
|
||||
static bool prepare(TrainlogDatabase*d,const char*sql,sqlite3_stmt**s){return sqlite3_prepare_v2(d->connection,sql,-1,s,NULL)==SQLITE_OK;}
|
||||
|
||||
static bool format_generated(int64_t now,char out[TRAINLOG_TIMESTAMP_MAX+1U]){
|
||||
time_t value=(time_t)now;struct tm utc;if((int64_t)value!=now||gmtime_r(&value,&utc)==NULL)return false;
|
||||
return strftime(out,TRAINLOG_TIMESTAMP_MAX+1U,"%Y-%m-%dT%H:%M:%SZ",&utc)>0U;
|
||||
static bool read_size(sqlite3_stmt *s, int c, size_t *out) {
|
||||
sqlite3_int64 v;
|
||||
if (sqlite3_column_type(s, c) != SQLITE_INTEGER)
|
||||
return false;
|
||||
v = sqlite3_column_int64(s, c);
|
||||
if (v < 0 || (uint64_t)v > (uint64_t)SIZE_MAX)
|
||||
return false;
|
||||
*out = (size_t)v;
|
||||
return true;
|
||||
}
|
||||
static bool format_local_date(time_t value,char out[11]){struct tm local;return localtime_r(&value,&local)!=NULL&&strftime(out,11U,"%Y-%m-%d",&local)==10U;}
|
||||
static bool load_user(char out[TRAINLOG_NAME_MAX+1U]){
|
||||
struct passwd value;struct passwd *result=NULL;char buffer[4096];int rc=getpwuid_r(geteuid(),&value,buffer,sizeof(buffer),&result);
|
||||
if(rc!=0||result==NULL||result->pw_name==NULL||result->pw_name[0]=='\0'||strlen(result->pw_name)>TRAINLOG_NAME_MAX)return false;
|
||||
(void)snprintf(out,TRAINLOG_NAME_MAX+1U,"%s",result->pw_name);return true;
|
||||
static bool prepare(TrainlogDatabase *d, const char *sql, sqlite3_stmt **s) {
|
||||
return sqlite3_prepare_v2(d->connection, sql, -1, s, NULL) == SQLITE_OK;
|
||||
}
|
||||
|
||||
static TrainlogStatus load_activity(TrainlogDatabase*d,int64_t now,TrainlogWebDashboardSnapshot*out){
|
||||
static const char SQL[]=
|
||||
static bool format_generated(int64_t now, char out[TRAINLOG_TIMESTAMP_MAX + 1U]) {
|
||||
time_t value = (time_t)now;
|
||||
struct tm utc;
|
||||
if ((int64_t)value != now || gmtime_r(&value, &utc) == NULL)
|
||||
return false;
|
||||
return strftime(out, TRAINLOG_TIMESTAMP_MAX + 1U, "%Y-%m-%dT%H:%M:%SZ", &utc) > 0U;
|
||||
}
|
||||
static bool format_local_date(time_t value, char out[11]) {
|
||||
struct tm local;
|
||||
return localtime_r(&value, &local) != NULL && strftime(out, 11U, "%Y-%m-%d", &local) == 10U;
|
||||
}
|
||||
static bool load_user(char out[TRAINLOG_NAME_MAX + 1U]) {
|
||||
struct passwd value;
|
||||
struct passwd *result = NULL;
|
||||
char buffer[4096];
|
||||
int rc = getpwuid_r(geteuid(), &value, buffer, sizeof(buffer), &result);
|
||||
if (rc != 0 || result == NULL || result->pw_name == NULL || result->pw_name[0] == '\0' ||
|
||||
strlen(result->pw_name) > TRAINLOG_NAME_MAX)
|
||||
return false;
|
||||
(void)snprintf(out, TRAINLOG_NAME_MAX + 1U, "%s", result->pw_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
static TrainlogStatus
|
||||
load_activity(TrainlogDatabase *d, int64_t now, TrainlogWebDashboardSnapshot *out) {
|
||||
static const char SQL[] =
|
||||
"SELECT substr(s.started_at,1,10),COUNT(DISTINCT s.id),COUNT(ps.id) FROM sessions s "
|
||||
"LEFT JOIN session_exercises se ON se.session_row_id=s.id LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id "
|
||||
"WHERE unixepoch(s.started_at)<=?1 AND substr(s.started_at,1,10)>=?2 AND (EXISTS(SELECT 1 FROM session_exercises x JOIN performed_sets p ON p.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) OR EXISTS(SELECT 1 FROM session_exercises x JOIN continuous_activity c ON c.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) OR EXISTS(SELECT 1 FROM session_exercises x JOIN max_results m ON m.session_exercise_row_id=x.id WHERE x.session_row_id=s.id)) GROUP BY substr(s.started_at,1,10) ORDER BY 1;";
|
||||
sqlite3_stmt*s=NULL;int step;size_t i;time_t reference=(time_t)now;struct tm current;
|
||||
if((int64_t)reference!=now)return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
if(localtime_r(&reference,¤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;
|
||||
"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_second<start.utc_second)goto invalid;out->has_duration=true;out->duration_seconds=(uint64_t)(end.utc_second-start.utc_second);}
|
||||
if(sqlite3_step(s)!=SQLITE_DONE||sqlite3_finalize(s)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR;s=NULL;
|
||||
if(!prepare(d,ZONE_SQL,&s)||sqlite3_bind_text(s,1,out->session_id,-1,SQLITE_TRANSIENT)!=SQLITE_OK)goto fail;
|
||||
while((step=sqlite3_step(s))==SQLITE_ROW){TrainlogWebWorkedZone*z;const TrainlogBodyZone*catalog;if(out->zone_count==TRAINLOG_WEB_ZONE_CAPACITY){return TRAINLOG_STATUS_DATABASE_ERROR;}z=&out->zones[out->zone_count];if(!copy_column(s,0,z->zone_id,sizeof(z->zone_id))||!read_size(s,1,&z->occurrence_count)||!read_size(s,2,&z->set_count))goto fail;z->session_count=1U;catalog=trainlog_body_zone_catalog_lookup(z->zone_id);if(catalog==NULL||strlen(catalog->display_name)>TRAINLOG_NAME_MAX)goto invalid;(void)snprintf(z->label,sizeof(z->label),"%s",catalog->display_name);++out->zone_count;}
|
||||
if(step!=SQLITE_DONE||sqlite3_finalize(s)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR;return TRAINLOG_STATUS_OK;
|
||||
invalid:if(s!=NULL)(void)sqlite3_finalize(s);return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
fail:if(s!=NULL)(void)sqlite3_finalize(s);return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
static TrainlogStatus load_last(TrainlogDatabase *d, int64_t now, TrainlogWebLastSession *out) {
|
||||
static const char SESSION_SQL[] =
|
||||
"SELECT s.session_id,s.started_at,COALESCE(s.ended_at,''),COUNT(DISTINCT CASE WHEN ps.id "
|
||||
"IS NOT NULL OR ca.id IS NOT NULL OR mr.session_exercise_row_id IS NOT NULL THEN se.id "
|
||||
"END),COUNT(DISTINCT ps.id),COUNT(DISTINCT ca.id),COUNT(DISTINCT "
|
||||
"mr.session_exercise_row_id) FROM sessions s JOIN session_exercises se ON "
|
||||
"se.session_row_id=s.id LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id "
|
||||
"LEFT JOIN continuous_activity ca ON ca.session_exercise_row_id=se.id LEFT JOIN "
|
||||
"max_results mr ON mr.session_exercise_row_id=se.id WHERE unixepoch(s.started_at)<=?1 AND "
|
||||
"(ps.id IS NOT NULL OR ca.id IS NOT NULL OR mr.session_exercise_row_id IS NOT NULL) GROUP "
|
||||
"BY s.id ORDER BY unixepoch(s.started_at) DESC,s.session_id DESC LIMIT 1;";
|
||||
static const char ZONE_SQL[] =
|
||||
"SELECT z.zone_id,COUNT(DISTINCT se.id),COUNT(DISTINCT ps.id) FROM sessions s JOIN "
|
||||
"session_exercises se ON se.session_row_id=s.id JOIN exercises e ON "
|
||||
"e.id=se.exercise_row_id JOIN exercise_body_zones z ON z.exercise_row_id=e.id AND "
|
||||
"z.role='primary' LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id WHERE "
|
||||
"s.session_id=?1 AND (EXISTS(SELECT 1 FROM performed_sets p WHERE "
|
||||
"p.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM continuous_activity c WHERE "
|
||||
"c.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM max_results m WHERE "
|
||||
"m.session_exercise_row_id=se.id)) GROUP BY z.zone_id ORDER BY z.zone_id;";
|
||||
sqlite3_stmt *s = NULL;
|
||||
int step;
|
||||
TrainlogTimestampKey start, end;
|
||||
if (!prepare(d, SESSION_SQL, &s) || sqlite3_bind_int64(s, 1, now) != SQLITE_OK)
|
||||
goto fail;
|
||||
step = sqlite3_step(s);
|
||||
if (step == SQLITE_DONE) {
|
||||
(void)sqlite3_finalize(s);
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
if (step != SQLITE_ROW || !copy_column(s, 0, out->session_id, sizeof(out->session_id)) ||
|
||||
!copy_column(s, 1, out->started_at, sizeof(out->started_at)) ||
|
||||
!copy_column(s, 2, out->ended_at, sizeof(out->ended_at)) ||
|
||||
!read_size(s, 3, &out->exercise_count) || !read_size(s, 4, &out->set_count) ||
|
||||
!read_size(s, 5, &out->continuous_count) || !read_size(s, 6, &out->max_count))
|
||||
goto fail;
|
||||
out->available = true;
|
||||
out->has_ended_at = out->ended_at[0] != '\0';
|
||||
if (!trainlog_timestamp_parse(out->started_at, strlen(out->started_at), &start)) {
|
||||
out->available = false;
|
||||
goto invalid;
|
||||
}
|
||||
if (out->has_ended_at) {
|
||||
if (!trainlog_timestamp_parse(out->ended_at, strlen(out->ended_at), &end) ||
|
||||
end.utc_second < start.utc_second)
|
||||
goto invalid;
|
||||
out->has_duration = true;
|
||||
out->duration_seconds = (uint64_t)(end.utc_second - start.utc_second);
|
||||
}
|
||||
if (sqlite3_step(s) != SQLITE_DONE || sqlite3_finalize(s) != SQLITE_OK)
|
||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
s = NULL;
|
||||
if (!prepare(d, ZONE_SQL, &s) ||
|
||||
sqlite3_bind_text(s, 1, out->session_id, -1, SQLITE_TRANSIENT) != SQLITE_OK)
|
||||
goto fail;
|
||||
while ((step = sqlite3_step(s)) == SQLITE_ROW) {
|
||||
TrainlogWebWorkedZone *z;
|
||||
const TrainlogBodyZone *catalog;
|
||||
if (out->zone_count == TRAINLOG_WEB_ZONE_CAPACITY) {
|
||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
}
|
||||
z = &out->zones[out->zone_count];
|
||||
if (!copy_column(s, 0, z->zone_id, sizeof(z->zone_id)) ||
|
||||
!read_size(s, 1, &z->occurrence_count) || !read_size(s, 2, &z->set_count))
|
||||
goto fail;
|
||||
z->session_count = 1U;
|
||||
catalog = trainlog_body_zone_catalog_lookup(z->zone_id);
|
||||
if (catalog == NULL || strlen(catalog->display_name) > TRAINLOG_NAME_MAX)
|
||||
goto invalid;
|
||||
(void)snprintf(z->label, sizeof(z->label), "%s", catalog->display_name);
|
||||
++out->zone_count;
|
||||
}
|
||||
if (step != SQLITE_DONE || sqlite3_finalize(s) != SQLITE_OK)
|
||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
invalid:
|
||||
if (s != NULL)
|
||||
(void)sqlite3_finalize(s);
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
fail:
|
||||
if (s != NULL)
|
||||
(void)sqlite3_finalize(s);
|
||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
}
|
||||
|
||||
static TrainlogStatus load_maxima(TrainlogDatabase*d,int64_t now,TrainlogWebDashboardSnapshot*out){
|
||||
static const char SQL[]="SELECT s.session_id,se.entry_id,e.exercise_id,e.name,COALESCE(se.equipment_id,''),m.max_weight_kg,s.started_at FROM max_results m JOIN session_exercises se ON se.id=m.session_exercise_row_id JOIN sessions s ON s.id=se.session_row_id JOIN exercises e ON e.id=se.exercise_row_id WHERE unixepoch(s.started_at)<=?1 ORDER BY unixepoch(s.started_at) DESC,s.session_id DESC,se.entry_id DESC LIMIT ?2;";
|
||||
sqlite3_stmt*s=NULL;int step;if(!prepare(d,SQL,&s)||sqlite3_bind_int64(s,1,now)!=SQLITE_OK||sqlite3_bind_int64(s,2,(sqlite3_int64)(TRAINLOG_WEB_MAX_RECORDS+1U))!=SQLITE_OK)goto fail;
|
||||
while((step=sqlite3_step(s))==SQLITE_ROW){TrainlogWebMaxRecord*r;if(out->max_record_count==TRAINLOG_WEB_MAX_RECORDS){out->partial=true;continue;}r=&out->max_records[out->max_record_count];if(!copy_column(s,0,r->session_id,sizeof(r->session_id))||!copy_column(s,1,r->entry_id,sizeof(r->entry_id))||!copy_column(s,2,r->exercise_id,sizeof(r->exercise_id))||!copy_column(s,3,r->exercise_name,sizeof(r->exercise_name))||!copy_column(s,4,r->equipment_id,sizeof(r->equipment_id))||!copy_column(s,6,r->timestamp,sizeof(r->timestamp))||(sqlite3_column_type(s,5)!=SQLITE_FLOAT&&sqlite3_column_type(s,5)!=SQLITE_INTEGER)||!isfinite(sqlite3_column_double(s,5))||sqlite3_column_double(s,5)<=0.0){out->invalid_data=true;continue;}r->weight_kg=sqlite3_column_double(s,5);++out->max_record_count;}
|
||||
if(step!=SQLITE_DONE||sqlite3_finalize(s)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR;return TRAINLOG_STATUS_OK;
|
||||
fail:if(s!=NULL)(void)sqlite3_finalize(s);return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
static TrainlogStatus
|
||||
load_maxima(TrainlogDatabase *d, int64_t now, TrainlogWebDashboardSnapshot *out) {
|
||||
static const char SQL[] =
|
||||
"SELECT "
|
||||
"s.session_id,se.entry_id,e.exercise_id,e.name,COALESCE(se.equipment_id,''),m.max_weight_"
|
||||
"kg,s.started_at FROM max_results m JOIN session_exercises se ON "
|
||||
"se.id=m.session_exercise_row_id JOIN sessions s ON s.id=se.session_row_id JOIN exercises "
|
||||
"e ON e.id=se.exercise_row_id WHERE unixepoch(s.started_at)<=?1 ORDER BY "
|
||||
"unixepoch(s.started_at) DESC,s.session_id DESC,se.entry_id DESC LIMIT ?2;";
|
||||
sqlite3_stmt *s = NULL;
|
||||
int step;
|
||||
if (!prepare(d, SQL, &s) || sqlite3_bind_int64(s, 1, now) != SQLITE_OK ||
|
||||
sqlite3_bind_int64(s, 2, (sqlite3_int64)(TRAINLOG_WEB_MAX_RECORDS + 1U)) != SQLITE_OK)
|
||||
goto fail;
|
||||
while ((step = sqlite3_step(s)) == SQLITE_ROW) {
|
||||
TrainlogWebMaxRecord *r;
|
||||
if (out->max_record_count == TRAINLOG_WEB_MAX_RECORDS) {
|
||||
out->partial = true;
|
||||
continue;
|
||||
}
|
||||
r = &out->max_records[out->max_record_count];
|
||||
if (!copy_column(s, 0, r->session_id, sizeof(r->session_id)) ||
|
||||
!copy_column(s, 1, r->entry_id, sizeof(r->entry_id)) ||
|
||||
!copy_column(s, 2, r->exercise_id, sizeof(r->exercise_id)) ||
|
||||
!copy_column(s, 3, r->exercise_name, sizeof(r->exercise_name)) ||
|
||||
!copy_column(s, 4, r->equipment_id, sizeof(r->equipment_id)) ||
|
||||
!copy_column(s, 6, r->timestamp, sizeof(r->timestamp)) ||
|
||||
(sqlite3_column_type(s, 5) != SQLITE_FLOAT &&
|
||||
sqlite3_column_type(s, 5) != SQLITE_INTEGER) ||
|
||||
!isfinite(sqlite3_column_double(s, 5)) || sqlite3_column_double(s, 5) <= 0.0) {
|
||||
out->invalid_data = true;
|
||||
continue;
|
||||
}
|
||||
r->weight_kg = sqlite3_column_double(s, 5);
|
||||
++out->max_record_count;
|
||||
}
|
||||
if (step != SQLITE_DONE || sqlite3_finalize(s) != SQLITE_OK)
|
||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
fail:
|
||||
if (s != NULL)
|
||||
(void)sqlite3_finalize(s);
|
||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
}
|
||||
|
||||
static TrainlogStatus load_zones(TrainlogDatabase*d,int64_t now,TrainlogWebDashboardSnapshot*out){
|
||||
static const char SQL[]="SELECT z.zone_id,COUNT(DISTINCT s.id),COUNT(DISTINCT se.id),COUNT(DISTINCT ps.id) FROM sessions s JOIN session_exercises se ON se.session_row_id=s.id JOIN exercises e ON e.id=se.exercise_row_id JOIN exercise_body_zones z ON z.exercise_row_id=e.id AND z.role='primary' LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id WHERE substr(s.started_at,1,10)>=?1 AND unixepoch(s.started_at)<=?2 AND (EXISTS(SELECT 1 FROM performed_sets p WHERE p.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM continuous_activity c WHERE c.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM max_results m WHERE m.session_exercise_row_id=se.id)) GROUP BY z.zone_id ORDER BY z.zone_id;";
|
||||
sqlite3_stmt*s=NULL;int step;if(!prepare(d,SQL,&s)||sqlite3_bind_text(s,1,out->activity[TRAINLOG_WEB_ACTIVITY_DAYS-30U].date,-1,SQLITE_TRANSIENT)!=SQLITE_OK||sqlite3_bind_int64(s,2,now)!=SQLITE_OK)goto fail;
|
||||
while((step=sqlite3_step(s))==SQLITE_ROW){TrainlogWebWorkedZone*z;const TrainlogBodyZone*catalog;if(out->muscle_zone_count==TRAINLOG_WEB_ZONE_CAPACITY){out->partial=true;continue;}z=&out->muscle_zones[out->muscle_zone_count];if(!copy_column(s,0,z->zone_id,sizeof(z->zone_id))||!read_size(s,1,&z->session_count)||!read_size(s,2,&z->occurrence_count)||!read_size(s,3,&z->set_count))goto fail;catalog=trainlog_body_zone_catalog_lookup(z->zone_id);if(catalog==NULL||strlen(catalog->display_name)>TRAINLOG_NAME_MAX){out->invalid_data=true;continue;}(void)snprintf(z->label,sizeof(z->label),"%s",catalog->display_name);++out->muscle_zone_count;}
|
||||
if(step!=SQLITE_DONE||sqlite3_finalize(s)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR;return TRAINLOG_STATUS_OK;
|
||||
fail:if(s!=NULL)(void)sqlite3_finalize(s);return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
static TrainlogStatus
|
||||
load_zones(TrainlogDatabase *d, int64_t now, TrainlogWebDashboardSnapshot *out) {
|
||||
static const char SQL[] =
|
||||
"SELECT z.zone_id,COUNT(DISTINCT s.id),COUNT(DISTINCT se.id),COUNT(DISTINCT ps.id) FROM "
|
||||
"sessions s JOIN session_exercises se ON se.session_row_id=s.id JOIN exercises e ON "
|
||||
"e.id=se.exercise_row_id JOIN exercise_body_zones z ON z.exercise_row_id=e.id AND "
|
||||
"z.role='primary' LEFT JOIN performed_sets ps ON ps.session_exercise_row_id=se.id WHERE "
|
||||
"substr(s.started_at,1,10)>=?1 AND unixepoch(s.started_at)<=?2 AND (EXISTS(SELECT 1 FROM "
|
||||
"performed_sets p WHERE p.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM "
|
||||
"continuous_activity c WHERE c.session_exercise_row_id=se.id) OR EXISTS(SELECT 1 FROM "
|
||||
"max_results m WHERE m.session_exercise_row_id=se.id)) GROUP BY z.zone_id ORDER BY "
|
||||
"z.zone_id;";
|
||||
sqlite3_stmt *s = NULL;
|
||||
int step;
|
||||
if (!prepare(d, SQL, &s) ||
|
||||
sqlite3_bind_text(
|
||||
s, 1, out->activity[TRAINLOG_WEB_ACTIVITY_DAYS - 30U].date, -1, SQLITE_TRANSIENT) !=
|
||||
SQLITE_OK ||
|
||||
sqlite3_bind_int64(s, 2, now) != SQLITE_OK)
|
||||
goto fail;
|
||||
while ((step = sqlite3_step(s)) == SQLITE_ROW) {
|
||||
TrainlogWebWorkedZone *z;
|
||||
const TrainlogBodyZone *catalog;
|
||||
if (out->muscle_zone_count == TRAINLOG_WEB_ZONE_CAPACITY) {
|
||||
out->partial = true;
|
||||
continue;
|
||||
}
|
||||
z = &out->muscle_zones[out->muscle_zone_count];
|
||||
if (!copy_column(s, 0, z->zone_id, sizeof(z->zone_id)) ||
|
||||
!read_size(s, 1, &z->session_count) || !read_size(s, 2, &z->occurrence_count) ||
|
||||
!read_size(s, 3, &z->set_count))
|
||||
goto fail;
|
||||
catalog = trainlog_body_zone_catalog_lookup(z->zone_id);
|
||||
if (catalog == NULL || strlen(catalog->display_name) > TRAINLOG_NAME_MAX) {
|
||||
out->invalid_data = true;
|
||||
continue;
|
||||
}
|
||||
(void)snprintf(z->label, sizeof(z->label), "%s", catalog->display_name);
|
||||
++out->muscle_zone_count;
|
||||
}
|
||||
if (step != SQLITE_DONE || sqlite3_finalize(s) != SQLITE_OK)
|
||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
fail:
|
||||
if (s != NULL)
|
||||
(void)sqlite3_finalize(s);
|
||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
}
|
||||
|
||||
static TrainlogStatus scan_quality(TrainlogDatabase*d,TrainlogWebDashboardSnapshot*out){
|
||||
static const char SQL[]="SELECT s.started_at,COALESCE(s.ended_at,'') FROM sessions s WHERE EXISTS(SELECT 1 FROM session_exercises x JOIN performed_sets p ON p.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) OR EXISTS(SELECT 1 FROM session_exercises x JOIN continuous_activity c ON c.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) OR EXISTS(SELECT 1 FROM session_exercises x JOIN max_results m ON m.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) ORDER BY s.id LIMIT 4097;";
|
||||
sqlite3_stmt*s=NULL;int step;size_t count=0U;if(!prepare(d,SQL,&s))goto fail;while((step=sqlite3_step(s))==SQLITE_ROW){char started[TRAINLOG_TIMESTAMP_MAX+1U],ended[TRAINLOG_TIMESTAMP_MAX+1U];TrainlogTimestampKey start_key,end_key;++count;if(count>4096U){out->partial=true;continue;}if(!copy_column(s,0,started,sizeof(started))||!copy_column(s,1,ended,sizeof(ended))||!trainlog_timestamp_parse(started,strlen(started),&start_key)||(ended[0]!='\0'&&(!trainlog_timestamp_parse(ended,strlen(ended),&end_key)||trainlog_timestamp_compare(&end_key,&start_key)<0)))out->invalid_data=true;}if(step!=SQLITE_DONE||sqlite3_finalize(s)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR;return TRAINLOG_STATUS_OK;fail:if(s!=NULL)(void)sqlite3_finalize(s);return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
static TrainlogStatus scan_quality(TrainlogDatabase *d, TrainlogWebDashboardSnapshot *out) {
|
||||
static const char SQL[] =
|
||||
"SELECT s.started_at,COALESCE(s.ended_at,'') FROM sessions s WHERE EXISTS(SELECT 1 FROM "
|
||||
"session_exercises x JOIN performed_sets p ON p.session_exercise_row_id=x.id WHERE "
|
||||
"x.session_row_id=s.id) OR EXISTS(SELECT 1 FROM session_exercises x JOIN "
|
||||
"continuous_activity c ON c.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) OR "
|
||||
"EXISTS(SELECT 1 FROM session_exercises x JOIN max_results m ON "
|
||||
"m.session_exercise_row_id=x.id WHERE x.session_row_id=s.id) ORDER BY s.id LIMIT 4097;";
|
||||
sqlite3_stmt *s = NULL;
|
||||
int step;
|
||||
size_t count = 0U;
|
||||
if (!prepare(d, SQL, &s))
|
||||
goto fail;
|
||||
while ((step = sqlite3_step(s)) == SQLITE_ROW) {
|
||||
char started[TRAINLOG_TIMESTAMP_MAX + 1U], ended[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||||
TrainlogTimestampKey start_key, end_key;
|
||||
++count;
|
||||
if (count > 4096U) {
|
||||
out->partial = true;
|
||||
continue;
|
||||
}
|
||||
if (!copy_column(s, 0, started, sizeof(started)) ||
|
||||
!copy_column(s, 1, ended, sizeof(ended)) ||
|
||||
!trainlog_timestamp_parse(started, strlen(started), &start_key) ||
|
||||
(ended[0] != '\0' && (!trainlog_timestamp_parse(ended, strlen(ended), &end_key) ||
|
||||
trainlog_timestamp_compare(&end_key, &start_key) < 0)))
|
||||
out->invalid_data = true;
|
||||
}
|
||||
if (step != SQLITE_DONE || sqlite3_finalize(s) != SQLITE_OK)
|
||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
fail:
|
||||
if (s != NULL)
|
||||
(void)sqlite3_finalize(s);
|
||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
}
|
||||
|
||||
TrainlogStatus trainlog_web_dashboard_load(TrainlogDatabase*d,const TrainlogWebDashboardQuery*q,TrainlogWebDashboardSnapshot*out){
|
||||
TrainlogStatus status,end_status;TrainlogDashboardQuery progression_query;
|
||||
if(d==NULL||q==NULL||out==NULL)return TRAINLOG_STATUS_INVALID_ARGUMENT;(void)memset(out,0,sizeof(*out));
|
||||
out->next_session_reason="no_persisted_executable_plan";out->cardio_reason="no_cardio_data_source";
|
||||
if(!format_generated(q->reference_unix_second,out->generated_at)||!load_user(out->user_display_name))return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
status=trainlog_database_read_snapshot_begin(d);if(status!=TRAINLOG_STATUS_OK)return status;
|
||||
status=load_activity(d,q->reference_unix_second,out);
|
||||
if(status==TRAINLOG_STATUS_OK)status=scan_quality(d,out);
|
||||
if(status==TRAINLOG_STATUS_OK){progression_query.period=TRAINLOG_DASHBOARD_90_DAYS;progression_query.reference_unix_second=q->reference_unix_second;status=trainlog_dashboard_load(d,&progression_query,&out->progression);out->partial=out->partial||out->progression.partial;out->invalid_data=out->invalid_data||out->progression.invalid_data;}
|
||||
if(status==TRAINLOG_STATUS_OK){status=load_last(d,q->reference_unix_second,&out->last_session);if(status==TRAINLOG_STATUS_INVALID_ARGUMENT){out->invalid_data=true;status=TRAINLOG_STATUS_OK;}}
|
||||
if(status==TRAINLOG_STATUS_OK)status=load_maxima(d,q->reference_unix_second,out);
|
||||
if(status==TRAINLOG_STATUS_OK)status=load_zones(d,q->reference_unix_second,out);
|
||||
end_status=trainlog_database_read_snapshot_end(d,status==TRAINLOG_STATUS_OK);if(end_status!=TRAINLOG_STATUS_OK)status=end_status;return status;
|
||||
TrainlogStatus trainlog_web_dashboard_load(TrainlogDatabase *d,
|
||||
const TrainlogWebDashboardQuery *q,
|
||||
TrainlogWebDashboardSnapshot *out) {
|
||||
TrainlogStatus status, end_status;
|
||||
TrainlogDashboardQuery progression_query;
|
||||
if (d == NULL || q == NULL || out == NULL)
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
(void)memset(out, 0, sizeof(*out));
|
||||
out->next_session_reason = "no_persisted_executable_plan";
|
||||
out->cardio_reason = "no_cardio_data_source";
|
||||
if (!format_generated(q->reference_unix_second, out->generated_at) ||
|
||||
!load_user(out->user_display_name))
|
||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||
status = trainlog_database_read_snapshot_begin(d);
|
||||
if (status != TRAINLOG_STATUS_OK)
|
||||
return status;
|
||||
status = load_activity(d, q->reference_unix_second, out);
|
||||
if (status == TRAINLOG_STATUS_OK)
|
||||
status = scan_quality(d, out);
|
||||
if (status == TRAINLOG_STATUS_OK) {
|
||||
progression_query.period = TRAINLOG_DASHBOARD_90_DAYS;
|
||||
progression_query.reference_unix_second = q->reference_unix_second;
|
||||
status = trainlog_dashboard_load(d, &progression_query, &out->progression);
|
||||
out->partial = out->partial || out->progression.partial;
|
||||
out->invalid_data = out->invalid_data || out->progression.invalid_data;
|
||||
}
|
||||
if (status == TRAINLOG_STATUS_OK) {
|
||||
status = load_last(d, q->reference_unix_second, &out->last_session);
|
||||
if (status == TRAINLOG_STATUS_INVALID_ARGUMENT) {
|
||||
out->invalid_data = true;
|
||||
status = TRAINLOG_STATUS_OK;
|
||||
}
|
||||
}
|
||||
if (status == TRAINLOG_STATUS_OK)
|
||||
status = load_maxima(d, q->reference_unix_second, out);
|
||||
if (status == TRAINLOG_STATUS_OK)
|
||||
status = load_zones(d, q->reference_unix_second, out);
|
||||
end_status = trainlog_database_read_snapshot_end(d, status == TRAINLOG_STATUS_OK);
|
||||
if (end_status != TRAINLOG_STATUS_OK)
|
||||
status = end_status;
|
||||
return status;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,26 +3,251 @@
|
|||
|
||||
#include <string.h>
|
||||
|
||||
#define RAW(value) do { if (!trainlog_json_raw(&w,(value))) goto overflow; } while (0)
|
||||
#define STR(value) do { if (!trainlog_json_string(&w,(value))) goto overflow; } while (0)
|
||||
#define NUM(value) do { if (!trainlog_json_size(&w,(value))) goto overflow; } while (0)
|
||||
#define RAW(value) \
|
||||
do { \
|
||||
if (!trainlog_json_raw(&w, (value))) \
|
||||
goto overflow; \
|
||||
} while (0)
|
||||
#define STR(value) \
|
||||
do { \
|
||||
if (!trainlog_json_string(&w, (value))) \
|
||||
goto overflow; \
|
||||
} while (0)
|
||||
#define NUM(value) \
|
||||
do { \
|
||||
if (!trainlog_json_size(&w, (value))) \
|
||||
goto overflow; \
|
||||
} while (0)
|
||||
|
||||
static const char *tracking_name(TrainlogTrackingMode value){return value==TRAINLOG_TRACKING_DURATION?"duration":"reps";}
|
||||
static const char *load_name(TrainlogLoadMode value){if(value==TRAINLOG_LOAD_EXTERNAL)return "external";if(value==TRAINLOG_LOAD_ASSISTANCE)return "assistance";return "none";}
|
||||
static bool point_improved(const TrainlogDashboardSnapshot*p,size_t i){const TrainlogExercisePerformancePoint*a,*b;if(i==0U)return false;a=&p->performance[i-1U];b=&p->performance[i];if(!a->has_weight&&!b->has_weight)return b->metric_value>a->metric_value;if(a->has_weight!=b->has_weight)return false;if(p->performance_load_mode==TRAINLOG_LOAD_ASSISTANCE)return b->weight_kg<a->weight_kg||(b->weight_kg==a->weight_kg&&b->metric_value>a->metric_value);return b->weight_kg>a->weight_kg||(b->weight_kg==a->weight_kg&&b->metric_value>a->metric_value);}
|
||||
|
||||
static bool write_zone(TrainlogJsonWriter*w0,const TrainlogWebWorkedZone*z){TrainlogJsonWriter w=*w0;RAW("{\"zone_id\":");STR(z->zone_id);RAW(",\"label\":");STR(z->label);RAW(",\"session_count\":");NUM(z->session_count);RAW(",\"occurrence_count\":");NUM(z->occurrence_count);RAW(",\"set_count\":");NUM(z->set_count);RAW("}");*w0=w;return true;overflow:*w0=w;return false;}
|
||||
|
||||
TrainlogStatus trainlog_web_dashboard_serialize(const TrainlogWebDashboardSnapshot*s,char*out,size_t cap,size_t*out_size){
|
||||
TrainlogJsonWriter w;size_t i;if(s==NULL||out==NULL||out_size==NULL)return TRAINLOG_STATUS_INVALID_ARGUMENT;*out_size=0U;trainlog_json_init(&w,out,cap);
|
||||
RAW("{\"api_version\":1,\"data\":{\"footer\":{\"user\":");STR(s->user_display_name);RAW(",\"last_session_date\":");if(s->last_session.available){char date[11];(void)memcpy(date,s->last_session.started_at,10U);date[10]='\0';STR(date);}else RAW("null");RAW(",\"last_zones\":[");for(i=0U;i<s->last_session.zone_count;++i){if(i>0U)RAW(",");STR(s->last_session.zones[i].label);}RAW("]},");
|
||||
RAW("\"next_session\":{\"available\":false,\"reason\":");STR(s->next_session_reason);RAW("},");
|
||||
RAW("\"activity\":{\"available\":true,\"window_days\":90,\"days\":[");for(i=0U;i<s->activity_count;++i){const TrainlogWebActivityDay*d=&s->activity[i];if(i>0U)RAW(",");RAW("{\"date\":");STR(d->date);RAW(",\"active\":");RAW(d->active?"true":"false");RAW(",\"session_count\":");NUM(d->session_count);RAW(",\"set_count\":");NUM(d->set_count);RAW("}");}RAW("]},");
|
||||
RAW("\"progression\":{");if(!s->progression.has_performance){RAW("\"available\":false,\"reason\":\"no_comparable_performance\"");}else{RAW("\"available\":true,\"identity\":{\"exercise_id\":");STR(s->progression.exercise.exercise_id);RAW(",\"exercise_name\":");STR(s->progression.exercise.name);RAW(",\"equipment_id\":");STR(s->progression.performance_equipment_id);RAW(",\"equipment_label\":");STR(s->progression.performance_equipment_label);RAW(",\"tracking_mode\":");STR(tracking_name(s->progression.performance_tracking_mode));RAW(",\"load_mode\":");STR(load_name(s->progression.performance_load_mode));RAW(",\"dose\":");if(!trainlog_json_i64(&w,(long long)s->progression.performance_dose))goto overflow;RAW("},\"points\":[");for(i=0U;i<s->progression.performance_count;++i){const TrainlogExercisePerformancePoint*p=&s->progression.performance[i];if(i>0U)RAW(",");RAW("{\"session_id\":");STR(p->session_id);RAW(",\"timestamp\":");STR(p->started_at);RAW(",\"metric_value\":");if(!trainlog_json_i64(&w,(long long)p->metric_value))goto overflow;RAW(",\"weight_kg\":");if(p->has_weight){if(!trainlog_json_double(&w,p->weight_kg))goto overflow;}else RAW("null");RAW(",\"improved\":");RAW(point_improved(&s->progression,i)?"true":"false");RAW("}");}RAW("]");}RAW("},");
|
||||
RAW("\"last_session\":{");if(!s->last_session.available){RAW("\"available\":false,\"reason\":\"no_observable_session\"");}else{const TrainlogWebLastSession*l=&s->last_session;RAW("\"available\":true,\"session_id\":");STR(l->session_id);RAW(",\"started_at\":");STR(l->started_at);RAW(",\"ended_at\":");if(l->has_ended_at)STR(l->ended_at);else RAW("null");RAW(",\"duration_seconds\":");if(l->has_duration){if(!trainlog_json_i64(&w,(long long)l->duration_seconds))goto overflow;}else RAW("null");RAW(",\"exercise_count\":");NUM(l->exercise_count);RAW(",\"set_count\":");NUM(l->set_count);RAW(",\"continuous_count\":");NUM(l->continuous_count);RAW(",\"max_count\":");NUM(l->max_count);RAW(",\"primary_zones\":[");for(i=0U;i<l->zone_count;++i){if(i>0U)RAW(",");if(!write_zone(&w,&l->zones[i]))goto overflow;}RAW("]");}RAW("},");
|
||||
RAW("\"max_records\":{\"available\":");RAW(s->max_record_count>0U?"true":"false");if(s->max_record_count==0U)RAW(",\"reason\":\"no_explicit_max_results\"");RAW(",\"records\":[");for(i=0U;i<s->max_record_count;++i){const TrainlogWebMaxRecord*r=&s->max_records[i];if(i>0U)RAW(",");RAW("{\"session_id\":");STR(r->session_id);RAW(",\"entry_id\":");STR(r->entry_id);RAW(",\"exercise_id\":");STR(r->exercise_id);RAW(",\"exercise_name\":");STR(r->exercise_name);RAW(",\"equipment_id\":");STR(r->equipment_id);RAW(",\"weight_kg\":");if(!trainlog_json_double(&w,r->weight_kg))goto overflow;RAW(",\"timestamp\":");STR(r->timestamp);RAW("}");}RAW("]},");
|
||||
RAW("\"muscle_distribution\":{\"available\":");RAW(s->muscle_zone_count>0U?"true":"false");if(s->muscle_zone_count==0U)RAW(",\"reason\":\"no_worked_primary_zones\"");RAW(",\"window_days\":30,\"primary_zones\":[");for(i=0U;i<s->muscle_zone_count;++i){if(i>0U)RAW(",");if(!write_zone(&w,&s->muscle_zones[i]))goto overflow;}RAW("]},");
|
||||
RAW("\"cardio\":{\"available\":false,\"reason\":");STR(s->cardio_reason);RAW("}},\"meta\":{\"partial\":");RAW(s->partial?"true":"false");RAW(",\"invalid_data\":");RAW(s->invalid_data?"true":"false");RAW(",\"generated_at\":");STR(s->generated_at);RAW("}}\n");
|
||||
if(w.failed)goto overflow;*out_size=w.size;return TRAINLOG_STATUS_OK;
|
||||
overflow:if(cap>0U)out[0]='\0';return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
static const char *tracking_name(TrainlogTrackingMode value) {
|
||||
return value == TRAINLOG_TRACKING_DURATION ? "duration" : "reps";
|
||||
}
|
||||
static const char *load_name(TrainlogLoadMode value) {
|
||||
if (value == TRAINLOG_LOAD_EXTERNAL)
|
||||
return "external";
|
||||
if (value == TRAINLOG_LOAD_ASSISTANCE)
|
||||
return "assistance";
|
||||
return "none";
|
||||
}
|
||||
static bool point_improved(const TrainlogDashboardSnapshot *p, size_t i) {
|
||||
const TrainlogExercisePerformancePoint *a, *b;
|
||||
if (i == 0U)
|
||||
return false;
|
||||
a = &p->performance[i - 1U];
|
||||
b = &p->performance[i];
|
||||
if (!a->has_weight && !b->has_weight)
|
||||
return b->metric_value > a->metric_value;
|
||||
if (a->has_weight != b->has_weight)
|
||||
return false;
|
||||
if (p->performance_load_mode == TRAINLOG_LOAD_ASSISTANCE)
|
||||
return b->weight_kg < a->weight_kg ||
|
||||
(b->weight_kg == a->weight_kg && b->metric_value > a->metric_value);
|
||||
return b->weight_kg > a->weight_kg ||
|
||||
(b->weight_kg == a->weight_kg && b->metric_value > a->metric_value);
|
||||
}
|
||||
|
||||
static bool write_zone(TrainlogJsonWriter *w0, const TrainlogWebWorkedZone *z) {
|
||||
TrainlogJsonWriter w = *w0;
|
||||
RAW("{\"zone_id\":");
|
||||
STR(z->zone_id);
|
||||
RAW(",\"label\":");
|
||||
STR(z->label);
|
||||
RAW(",\"session_count\":");
|
||||
NUM(z->session_count);
|
||||
RAW(",\"occurrence_count\":");
|
||||
NUM(z->occurrence_count);
|
||||
RAW(",\"set_count\":");
|
||||
NUM(z->set_count);
|
||||
RAW("}");
|
||||
*w0 = w;
|
||||
return true;
|
||||
overflow:
|
||||
*w0 = w;
|
||||
return false;
|
||||
}
|
||||
|
||||
TrainlogStatus trainlog_web_dashboard_serialize(const TrainlogWebDashboardSnapshot *s,
|
||||
char *out,
|
||||
size_t cap,
|
||||
size_t *out_size) {
|
||||
TrainlogJsonWriter w;
|
||||
size_t i;
|
||||
if (s == NULL || out == NULL || out_size == NULL)
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
*out_size = 0U;
|
||||
trainlog_json_init(&w, out, cap);
|
||||
RAW("{\"api_version\":1,\"data\":{\"footer\":{\"user\":");
|
||||
STR(s->user_display_name);
|
||||
RAW(",\"last_session_date\":");
|
||||
if (s->last_session.available) {
|
||||
char date[11];
|
||||
(void)memcpy(date, s->last_session.started_at, 10U);
|
||||
date[10] = '\0';
|
||||
STR(date);
|
||||
} else
|
||||
RAW("null");
|
||||
RAW(",\"last_zones\":[");
|
||||
for (i = 0U; i < s->last_session.zone_count; ++i) {
|
||||
if (i > 0U)
|
||||
RAW(",");
|
||||
STR(s->last_session.zones[i].label);
|
||||
}
|
||||
RAW("]},");
|
||||
RAW("\"next_session\":{\"available\":false,\"reason\":");
|
||||
STR(s->next_session_reason);
|
||||
RAW("},");
|
||||
RAW("\"activity\":{\"available\":true,\"window_days\":90,\"days\":[");
|
||||
for (i = 0U; i < s->activity_count; ++i) {
|
||||
const TrainlogWebActivityDay *d = &s->activity[i];
|
||||
if (i > 0U)
|
||||
RAW(",");
|
||||
RAW("{\"date\":");
|
||||
STR(d->date);
|
||||
RAW(",\"active\":");
|
||||
RAW(d->active ? "true" : "false");
|
||||
RAW(",\"session_count\":");
|
||||
NUM(d->session_count);
|
||||
RAW(",\"set_count\":");
|
||||
NUM(d->set_count);
|
||||
RAW("}");
|
||||
}
|
||||
RAW("]},");
|
||||
RAW("\"progression\":{");
|
||||
if (!s->progression.has_performance) {
|
||||
RAW("\"available\":false,\"reason\":\"no_comparable_performance\"");
|
||||
} else {
|
||||
RAW("\"available\":true,\"identity\":{\"exercise_id\":");
|
||||
STR(s->progression.exercise.exercise_id);
|
||||
RAW(",\"exercise_name\":");
|
||||
STR(s->progression.exercise.name);
|
||||
RAW(",\"equipment_id\":");
|
||||
STR(s->progression.performance_equipment_id);
|
||||
RAW(",\"equipment_label\":");
|
||||
STR(s->progression.performance_equipment_label);
|
||||
RAW(",\"tracking_mode\":");
|
||||
STR(tracking_name(s->progression.performance_tracking_mode));
|
||||
RAW(",\"load_mode\":");
|
||||
STR(load_name(s->progression.performance_load_mode));
|
||||
RAW(",\"dose\":");
|
||||
if (!trainlog_json_i64(&w, (long long)s->progression.performance_dose))
|
||||
goto overflow;
|
||||
RAW("},\"points\":[");
|
||||
for (i = 0U; i < s->progression.performance_count; ++i) {
|
||||
const TrainlogExercisePerformancePoint *p = &s->progression.performance[i];
|
||||
if (i > 0U)
|
||||
RAW(",");
|
||||
RAW("{\"session_id\":");
|
||||
STR(p->session_id);
|
||||
RAW(",\"timestamp\":");
|
||||
STR(p->started_at);
|
||||
RAW(",\"metric_value\":");
|
||||
if (!trainlog_json_i64(&w, (long long)p->metric_value))
|
||||
goto overflow;
|
||||
RAW(",\"weight_kg\":");
|
||||
if (p->has_weight) {
|
||||
if (!trainlog_json_double(&w, p->weight_kg))
|
||||
goto overflow;
|
||||
} else
|
||||
RAW("null");
|
||||
RAW(",\"improved\":");
|
||||
RAW(point_improved(&s->progression, i) ? "true" : "false");
|
||||
RAW("}");
|
||||
}
|
||||
RAW("]");
|
||||
}
|
||||
RAW("},");
|
||||
RAW("\"last_session\":{");
|
||||
if (!s->last_session.available) {
|
||||
RAW("\"available\":false,\"reason\":\"no_observable_session\"");
|
||||
} else {
|
||||
const TrainlogWebLastSession *l = &s->last_session;
|
||||
RAW("\"available\":true,\"session_id\":");
|
||||
STR(l->session_id);
|
||||
RAW(",\"started_at\":");
|
||||
STR(l->started_at);
|
||||
RAW(",\"ended_at\":");
|
||||
if (l->has_ended_at)
|
||||
STR(l->ended_at);
|
||||
else
|
||||
RAW("null");
|
||||
RAW(",\"duration_seconds\":");
|
||||
if (l->has_duration) {
|
||||
if (!trainlog_json_i64(&w, (long long)l->duration_seconds))
|
||||
goto overflow;
|
||||
} else
|
||||
RAW("null");
|
||||
RAW(",\"exercise_count\":");
|
||||
NUM(l->exercise_count);
|
||||
RAW(",\"set_count\":");
|
||||
NUM(l->set_count);
|
||||
RAW(",\"continuous_count\":");
|
||||
NUM(l->continuous_count);
|
||||
RAW(",\"max_count\":");
|
||||
NUM(l->max_count);
|
||||
RAW(",\"primary_zones\":[");
|
||||
for (i = 0U; i < l->zone_count; ++i) {
|
||||
if (i > 0U)
|
||||
RAW(",");
|
||||
if (!write_zone(&w, &l->zones[i]))
|
||||
goto overflow;
|
||||
}
|
||||
RAW("]");
|
||||
}
|
||||
RAW("},");
|
||||
RAW("\"max_records\":{\"available\":");
|
||||
RAW(s->max_record_count > 0U ? "true" : "false");
|
||||
if (s->max_record_count == 0U)
|
||||
RAW(",\"reason\":\"no_explicit_max_results\"");
|
||||
RAW(",\"records\":[");
|
||||
for (i = 0U; i < s->max_record_count; ++i) {
|
||||
const TrainlogWebMaxRecord *r = &s->max_records[i];
|
||||
if (i > 0U)
|
||||
RAW(",");
|
||||
RAW("{\"session_id\":");
|
||||
STR(r->session_id);
|
||||
RAW(",\"entry_id\":");
|
||||
STR(r->entry_id);
|
||||
RAW(",\"exercise_id\":");
|
||||
STR(r->exercise_id);
|
||||
RAW(",\"exercise_name\":");
|
||||
STR(r->exercise_name);
|
||||
RAW(",\"equipment_id\":");
|
||||
STR(r->equipment_id);
|
||||
RAW(",\"weight_kg\":");
|
||||
if (!trainlog_json_double(&w, r->weight_kg))
|
||||
goto overflow;
|
||||
RAW(",\"timestamp\":");
|
||||
STR(r->timestamp);
|
||||
RAW("}");
|
||||
}
|
||||
RAW("]},");
|
||||
RAW("\"muscle_distribution\":{\"available\":");
|
||||
RAW(s->muscle_zone_count > 0U ? "true" : "false");
|
||||
if (s->muscle_zone_count == 0U)
|
||||
RAW(",\"reason\":\"no_worked_primary_zones\"");
|
||||
RAW(",\"window_days\":30,\"primary_zones\":[");
|
||||
for (i = 0U; i < s->muscle_zone_count; ++i) {
|
||||
if (i > 0U)
|
||||
RAW(",");
|
||||
if (!write_zone(&w, &s->muscle_zones[i]))
|
||||
goto overflow;
|
||||
}
|
||||
RAW("]},");
|
||||
RAW("\"cardio\":{\"available\":false,\"reason\":");
|
||||
STR(s->cardio_reason);
|
||||
RAW("}},\"meta\":{\"partial\":");
|
||||
RAW(s->partial ? "true" : "false");
|
||||
RAW(",\"invalid_data\":");
|
||||
RAW(s->invalid_data ? "true" : "false");
|
||||
RAW(",\"generated_at\":");
|
||||
STR(s->generated_at);
|
||||
RAW("}}\n");
|
||||
if (w.failed)
|
||||
goto overflow;
|
||||
*out_size = w.size;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
overflow:
|
||||
if (cap > 0U)
|
||||
out[0] = '\0';
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,19 +6,221 @@
|
|||
#include <unistd.h>
|
||||
#include "trainlog/generation_mtp.h"
|
||||
|
||||
typedef struct Node { uint32_t id,parent; bool folder; char path[1024],name[256]; uint64_t size; } Node;
|
||||
static Node nodes[2048]; static size_t node_count; static const char *remote;
|
||||
static uint32_t scan(const char *path,uint32_t parent){DIR *d;struct dirent *e;uint32_t self=(uint32_t)(node_count+1U);Node *n=&nodes[node_count++];struct stat st;lstat(path,&st);n->id=self;n->parent=parent;n->folder=S_ISDIR(st.st_mode);n->size=(uint64_t)st.st_size;snprintf(n->path,sizeof(n->path),"%s",path);const char *slash=strrchr(path,'/');snprintf(n->name,sizeof(n->name),"%s",slash?slash+1:path);if(n->folder&&(d=opendir(path))!=NULL){while((e=readdir(d)))if(strcmp(e->d_name,".")&&strcmp(e->d_name,"..")){char child[1024];snprintf(child,sizeof(child),"%s/%s",path,e->d_name);scan(child,self);}closedir(d);}return self;}
|
||||
static void rebuild(void){node_count=0U;scan(remote,UINT32_MAX);}
|
||||
static Node *node(uint32_t id){size_t i;for(i=0;i<node_count;i++)if(nodes[i].id==id)return &nodes[i];return NULL;}
|
||||
static TrainlogStatus devices(TrainlogUsbDevice *o,size_t c,size_t *n){if(c<1U)return TRAINLOG_STATUS_INVALID_ARGUMENT;memset(o,0,sizeof(*o));o->bus_number=1;o->device_number=1;*n=1;return TRAINLOG_STATUS_OK;}
|
||||
static TrainlogStatus storages(unsigned int b,unsigned int d,TrainlogMtpStorage *o,size_t c,size_t *n){(void)b;(void)d;if(c<1U)return TRAINLOG_STATUS_INVALID_ARGUMENT;memset(o,0,sizeof(*o));o->storage_id=1;*n=1;return TRAINLOG_STATUS_OK;}
|
||||
static TrainlogStatus children(unsigned int b,unsigned int d,uint32_t s,uint32_t p,TrainlogMtpEntry *o,size_t c,size_t *n){size_t i,k=0;(void)b;(void)d;(void)s;rebuild();if(p==UINT32_MAX)p=nodes[0].id;for(i=0;i<node_count;i++)if(nodes[i].parent==p){if(k>=c)return TRAINLOG_STATUS_INVALID_ARGUMENT;memset(&o[k],0,sizeof(o[k]));o[k].item_id=nodes[i].id;o[k].parent_id=p;o[k].storage_id=1;o[k].folder=nodes[i].folder;o[k].size_bytes=nodes[i].size;snprintf(o[k].name,sizeof(o[k].name),"%s",nodes[i].name);k++;}*n=k;return TRAINLOG_STATUS_OK;}
|
||||
static TrainlogStatus ensure(unsigned int b,unsigned int d,uint32_t s,uint32_t p,const char *name,uint32_t *id,bool *created){Node *parent;char path[1024];(void)b;(void)d;(void)s;rebuild();parent=node(p);if(!parent)return TRAINLOG_STATUS_NOT_FOUND;snprintf(path,sizeof(path),"%s/%s",parent->path,name);if(mkdir(path,0700)==0)*created=true;else if(access(path,F_OK)==0)*created=false;else return TRAINLOG_STATUS_SYSTEM_ERROR;rebuild();size_t i;for(i=0;i<node_count;i++)if(strcmp(nodes[i].path,path)==0){*id=nodes[i].id;return TRAINLOG_STATUS_OK;}return TRAINLOG_STATUS_SYSTEM_ERROR;}
|
||||
static int copy(const char *a,const char *b){FILE *in=fopen(a,"rb"),*out=fopen(b,"wb");char buf[8192];size_t n;if(!in||!out)return -1;while((n=fread(buf,1,sizeof(buf),in))>0)if(fwrite(buf,1,n,out)!=n)return -1;return fclose(in)|fclose(out);}
|
||||
static TrainlogStatus receive(unsigned int b,unsigned int d,uint32_t id,const char *path){Node *n;(void)b;(void)d;rebuild();n=node(id);return n&©(n->path,path)==0?TRAINLOG_STATUS_OK:TRAINLOG_STATUS_SYSTEM_ERROR;}
|
||||
static TrainlogStatus send_file(unsigned int b,unsigned int d,uint32_t s,uint32_t p,const char *local,const char *name,uint32_t *id){Node *parent;char path[1024];(void)b;(void)d;(void)s;rebuild();parent=node(p);if(!parent)return TRAINLOG_STATUS_NOT_FOUND;snprintf(path,sizeof(path),"%s/%s",parent->path,name);if(copy(local,path)!=0)return TRAINLOG_STATUS_SYSTEM_ERROR;rebuild();size_t i;for(i=0;i<node_count;i++)if(strcmp(nodes[i].path,path)==0){*id=nodes[i].id;return TRAINLOG_STATUS_OK;}return TRAINLOG_STATUS_SYSTEM_ERROR;}
|
||||
static TrainlogStatus remove_object(unsigned int b,unsigned int d,uint32_t id){Node *n;(void)b;(void)d;rebuild();n=node(id);return n&&unlink(n->path)==0?TRAINLOG_STATUS_OK:TRAINLOG_STATUS_SYSTEM_ERROR;}
|
||||
static TrainlogStatus rename_object(unsigned int b,unsigned int d,uint32_t id,const char *name){Node *n;char path[1024],*slash;(void)b;(void)d;rebuild();n=node(id);if(!n)return TRAINLOG_STATUS_NOT_FOUND;snprintf(path,sizeof(path),"%s",n->path);slash=strrchr(path,'/');if(!slash)return TRAINLOG_STATUS_SYSTEM_ERROR;snprintf(slash+1,(size_t)(path+sizeof(path)-(slash+1)),"%s",name);return rename(n->path,path)==0?TRAINLOG_STATUS_OK:TRAINLOG_STATUS_SYSTEM_ERROR;}
|
||||
static const TrainlogGenerationMtpIo io={devices,storages,children,ensure,receive,send_file,remove_object,rename_object};
|
||||
int main(int argc,char **argv){char diag[1024]={0};TrainlogStatus status;remote=getenv("TRAINLOG_MTP_DOUBLE_ROOT");if(argc!=4||!remote)return 64;if(strcmp(argv[1],"pull")==0)status=trainlog_generation_mtp_pull(&io,argv[2],argv[3],diag,sizeof(diag));else if(strcmp(argv[1],"push")==0)status=trainlog_generation_mtp_push(&io,argv[2],argv[3],diag,sizeof(diag));else return 64;if(status!=TRAINLOG_STATUS_OK){fprintf(stderr,"double failed %d %s\n",status,diag);return 2;}return 0;}
|
||||
typedef struct Node {
|
||||
uint32_t id, parent;
|
||||
bool folder;
|
||||
char path[1024], name[256];
|
||||
uint64_t size;
|
||||
} Node;
|
||||
static Node nodes[2048];
|
||||
static size_t node_count;
|
||||
static const char *remote;
|
||||
static uint32_t scan(const char *path, uint32_t parent) {
|
||||
DIR *d;
|
||||
struct dirent *e;
|
||||
uint32_t self = (uint32_t)(node_count + 1U);
|
||||
Node *n = &nodes[node_count++];
|
||||
struct stat st;
|
||||
lstat(path, &st);
|
||||
n->id = self;
|
||||
n->parent = parent;
|
||||
n->folder = S_ISDIR(st.st_mode);
|
||||
n->size = (uint64_t)st.st_size;
|
||||
snprintf(n->path, sizeof(n->path), "%s", path);
|
||||
const char *slash = strrchr(path, '/');
|
||||
snprintf(n->name, sizeof(n->name), "%s", slash ? slash + 1 : path);
|
||||
if (n->folder && (d = opendir(path)) != NULL) {
|
||||
while ((e = readdir(d)))
|
||||
if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..")) {
|
||||
char child[1024];
|
||||
snprintf(child, sizeof(child), "%s/%s", path, e->d_name);
|
||||
scan(child, self);
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
static void rebuild(void) {
|
||||
node_count = 0U;
|
||||
scan(remote, UINT32_MAX);
|
||||
}
|
||||
static Node *node(uint32_t id) {
|
||||
size_t i;
|
||||
for (i = 0; i < node_count; i++)
|
||||
if (nodes[i].id == id)
|
||||
return &nodes[i];
|
||||
return NULL;
|
||||
}
|
||||
static TrainlogStatus devices(TrainlogUsbDevice *o, size_t c, size_t *n) {
|
||||
if (c < 1U)
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
memset(o, 0, sizeof(*o));
|
||||
o->bus_number = 1;
|
||||
o->device_number = 1;
|
||||
*n = 1;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
static TrainlogStatus
|
||||
storages(unsigned int b, unsigned int d, TrainlogMtpStorage *o, size_t c, size_t *n) {
|
||||
(void)b;
|
||||
(void)d;
|
||||
if (c < 1U)
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
memset(o, 0, sizeof(*o));
|
||||
o->storage_id = 1;
|
||||
*n = 1;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
static TrainlogStatus children(unsigned int b,
|
||||
unsigned int d,
|
||||
uint32_t s,
|
||||
uint32_t p,
|
||||
TrainlogMtpEntry *o,
|
||||
size_t c,
|
||||
size_t *n) {
|
||||
size_t i, k = 0;
|
||||
(void)b;
|
||||
(void)d;
|
||||
(void)s;
|
||||
rebuild();
|
||||
if (p == UINT32_MAX)
|
||||
p = nodes[0].id;
|
||||
for (i = 0; i < node_count; i++)
|
||||
if (nodes[i].parent == p) {
|
||||
if (k >= c)
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
memset(&o[k], 0, sizeof(o[k]));
|
||||
o[k].item_id = nodes[i].id;
|
||||
o[k].parent_id = p;
|
||||
o[k].storage_id = 1;
|
||||
o[k].folder = nodes[i].folder;
|
||||
o[k].size_bytes = nodes[i].size;
|
||||
snprintf(o[k].name, sizeof(o[k].name), "%s", nodes[i].name);
|
||||
k++;
|
||||
}
|
||||
*n = k;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
static TrainlogStatus ensure(unsigned int b,
|
||||
unsigned int d,
|
||||
uint32_t s,
|
||||
uint32_t p,
|
||||
const char *name,
|
||||
uint32_t *id,
|
||||
bool *created) {
|
||||
Node *parent;
|
||||
char path[1024];
|
||||
(void)b;
|
||||
(void)d;
|
||||
(void)s;
|
||||
rebuild();
|
||||
parent = node(p);
|
||||
if (!parent)
|
||||
return TRAINLOG_STATUS_NOT_FOUND;
|
||||
snprintf(path, sizeof(path), "%s/%s", parent->path, name);
|
||||
if (mkdir(path, 0700) == 0)
|
||||
*created = true;
|
||||
else if (access(path, F_OK) == 0)
|
||||
*created = false;
|
||||
else
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
rebuild();
|
||||
size_t i;
|
||||
for (i = 0; i < node_count; i++)
|
||||
if (strcmp(nodes[i].path, path) == 0) {
|
||||
*id = nodes[i].id;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
static int copy(const char *a, const char *b) {
|
||||
FILE *in = fopen(a, "rb"), *out = fopen(b, "wb");
|
||||
char buf[8192];
|
||||
size_t n;
|
||||
if (!in || !out)
|
||||
return -1;
|
||||
while ((n = fread(buf, 1, sizeof(buf), in)) > 0)
|
||||
if (fwrite(buf, 1, n, out) != n)
|
||||
return -1;
|
||||
return fclose(in) | fclose(out);
|
||||
}
|
||||
static TrainlogStatus receive(unsigned int b, unsigned int d, uint32_t id, const char *path) {
|
||||
Node *n;
|
||||
(void)b;
|
||||
(void)d;
|
||||
rebuild();
|
||||
n = node(id);
|
||||
return n && copy(n->path, path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
static TrainlogStatus send_file(unsigned int b,
|
||||
unsigned int d,
|
||||
uint32_t s,
|
||||
uint32_t p,
|
||||
const char *local,
|
||||
const char *name,
|
||||
uint32_t *id) {
|
||||
Node *parent;
|
||||
char path[1024];
|
||||
(void)b;
|
||||
(void)d;
|
||||
(void)s;
|
||||
rebuild();
|
||||
parent = node(p);
|
||||
if (!parent)
|
||||
return TRAINLOG_STATUS_NOT_FOUND;
|
||||
snprintf(path, sizeof(path), "%s/%s", parent->path, name);
|
||||
if (copy(local, path) != 0)
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
rebuild();
|
||||
size_t i;
|
||||
for (i = 0; i < node_count; i++)
|
||||
if (strcmp(nodes[i].path, path) == 0) {
|
||||
*id = nodes[i].id;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
static TrainlogStatus remove_object(unsigned int b, unsigned int d, uint32_t id) {
|
||||
Node *n;
|
||||
(void)b;
|
||||
(void)d;
|
||||
rebuild();
|
||||
n = node(id);
|
||||
return n && unlink(n->path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
static TrainlogStatus rename_object(unsigned int b, unsigned int d, uint32_t id, const char *name) {
|
||||
Node *n;
|
||||
char path[1024], *slash;
|
||||
(void)b;
|
||||
(void)d;
|
||||
rebuild();
|
||||
n = node(id);
|
||||
if (!n)
|
||||
return TRAINLOG_STATUS_NOT_FOUND;
|
||||
snprintf(path, sizeof(path), "%s", n->path);
|
||||
slash = strrchr(path, '/');
|
||||
if (!slash)
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
snprintf(slash + 1, (size_t)(path + sizeof(path) - (slash + 1)), "%s", name);
|
||||
return rename(n->path, path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
static const TrainlogGenerationMtpIo io = {
|
||||
devices, storages, children, ensure, receive, send_file, remove_object, rename_object};
|
||||
int main(int argc, char **argv) {
|
||||
char diag[1024] = {0};
|
||||
TrainlogStatus status;
|
||||
remote = getenv("TRAINLOG_MTP_DOUBLE_ROOT");
|
||||
if (argc != 4 || !remote)
|
||||
return 64;
|
||||
if (strcmp(argv[1], "pull") == 0)
|
||||
status = trainlog_generation_mtp_pull(&io, argv[2], argv[3], diag, sizeof(diag));
|
||||
else if (strcmp(argv[1], "push") == 0)
|
||||
status = trainlog_generation_mtp_push(&io, argv[2], argv[3], diag, sizeof(diag));
|
||||
else
|
||||
return 64;
|
||||
if (status != TRAINLOG_STATUS_OK) {
|
||||
fprintf(stderr, "double failed %d %s\n", status, diag);
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,22 +5,277 @@
|
|||
#include <unistd.h>
|
||||
#include "trainlog/generation_mtp.h"
|
||||
|
||||
#define CHECK(x) do { if (!(x)) { fprintf(stderr,"CHECK %s:%d: %s\n",__FILE__,__LINE__,#x); return 1; } } while (0)
|
||||
typedef struct Object { uint32_t id,parent; bool folder; char name[256]; char data[4096]; size_t size; } Object;
|
||||
static Object objects[64]; static size_t object_count; static unsigned int device_count=1U; static char send_order[1024];
|
||||
static void add(uint32_t id,uint32_t parent,bool folder,const char *name,const char *data){Object *o=&objects[object_count++];memset(o,0,sizeof(*o));o->id=id;o->parent=parent;o->folder=folder;snprintf(o->name,sizeof(o->name),"%s",name);if(data){o->size=strlen(data);memcpy(o->data,data,o->size);}}
|
||||
static void reset(void){object_count=0U;device_count=1U;send_order[0]='\0';add(1,UINT32_MAX,true,"Documents",NULL);add(2,1,true,"Trainlog",NULL);add(3,2,false,"android-peer-v1.json","{\"format\":\"trainlog-sync-peer\",\"version\":1,\"peer_id\":\"peer_11111111-1111-4111-8111-111111111111\",\"capabilities\":[\"generation-manifest-v1\",\"generation-ack-v1\"]}");}
|
||||
static TrainlogStatus devices(TrainlogUsbDevice *out,size_t cap,size_t *count){size_t i;if(cap<device_count)return TRAINLOG_STATUS_INVALID_ARGUMENT;*count=device_count;for(i=0;i<device_count;i++){memset(&out[i],0,sizeof(out[i]));out[i].bus_number=1U;out[i].device_number=(unsigned int)i+1U;}return TRAINLOG_STATUS_OK;}
|
||||
static TrainlogStatus storages(unsigned int bus,unsigned int dev,TrainlogMtpStorage *out,size_t cap,size_t *count){(void)bus;if(cap<1U)return TRAINLOG_STATUS_INVALID_ARGUMENT;memset(out,0,sizeof(*out));out->storage_id=dev;*count=1U;return TRAINLOG_STATUS_OK;}
|
||||
static TrainlogStatus children(unsigned int bus,unsigned int dev,uint32_t storage,uint32_t parent,TrainlogMtpEntry *out,size_t cap,size_t *count){size_t i,n=0;(void)bus;(void)storage;for(i=0;i<object_count;i++)if(objects[i].parent==parent && (dev==1U || objects[i].id<=3U)){if(n>=cap)return TRAINLOG_STATUS_INVALID_ARGUMENT;memset(&out[n],0,sizeof(out[n]));out[n].item_id=objects[i].id;out[n].parent_id=parent;out[n].storage_id=dev;out[n].folder=objects[i].folder;out[n].size_bytes=objects[i].size;snprintf(out[n].name,sizeof(out[n].name),"%s",objects[i].name);n++;}*count=n;return TRAINLOG_STATUS_OK;}
|
||||
static TrainlogStatus ensure_folder(unsigned int b,unsigned int d,uint32_t s,uint32_t p,const char *name,uint32_t *id,bool *created){size_t i;(void)b;(void)d;(void)s;for(i=0;i<object_count;i++)if(objects[i].parent==p&&objects[i].folder&&strcmp(objects[i].name,name)==0){*id=objects[i].id;*created=false;return TRAINLOG_STATUS_OK;}*id=(uint32_t)(100U+object_count);*created=true;add(*id,p,true,name,NULL);return TRAINLOG_STATUS_OK;}
|
||||
static Object *find(uint32_t id){size_t i;for(i=0;i<object_count;i++)if(objects[i].id==id)return &objects[i];return NULL;}
|
||||
static TrainlogStatus receive(unsigned int b,unsigned int d,uint32_t id,const char *path){FILE *f;Object *o=find(id);size_t bytes;(void)b;(void)d;if(!o)return TRAINLOG_STATUS_NOT_FOUND;f=fopen(path,"wb");if(!f)return TRAINLOG_STATUS_SYSTEM_ERROR;bytes=id==9U?3U:o->size;if(fwrite(o->data,1,bytes,f)!=bytes){fclose(f);return TRAINLOG_STATUS_SYSTEM_ERROR;}return fclose(f)==0?TRAINLOG_STATUS_OK:TRAINLOG_STATUS_SYSTEM_ERROR;}
|
||||
static TrainlogStatus send_file(unsigned int b,unsigned int d,uint32_t s,uint32_t p,const char *path,const char *name,uint32_t *id){FILE *f;Object *o;long size;(void)b;(void)d;(void)s;f=fopen(path,"rb");if(!f)return TRAINLOG_STATUS_SYSTEM_ERROR;fseek(f,0,SEEK_END);size=ftell(f);rewind(f);if(size<0||size>(long)sizeof(objects[0].data)){fclose(f);return TRAINLOG_STATUS_INVALID_ARGUMENT;}*id=(uint32_t)(100U+object_count);add(*id,p,false,name,NULL);o=&objects[object_count-1];o->size=(size_t)size;if(fread(o->data,1,o->size,f)!=o->size){fclose(f);return TRAINLOG_STATUS_SYSTEM_ERROR;}fclose(f);strncat(send_order,name,sizeof(send_order)-strlen(send_order)-2U);strncat(send_order,"\n",sizeof(send_order)-strlen(send_order)-1U);return TRAINLOG_STATUS_OK;}
|
||||
static TrainlogStatus remove_object(unsigned int b,unsigned int d,uint32_t id){Object *o=find(id);(void)b;(void)d;if(!o)return TRAINLOG_STATUS_NOT_FOUND;o->name[0]='\0';return TRAINLOG_STATUS_OK;}
|
||||
static TrainlogStatus rename_object(unsigned int b,unsigned int d,uint32_t id,const char *name){Object *o=find(id);(void)b;(void)d;if(!o)return TRAINLOG_STATUS_NOT_FOUND;snprintf(o->name,sizeof(o->name),"%s",name);return TRAINLOG_STATUS_OK;}
|
||||
static const TrainlogGenerationMtpIo io={devices,storages,children,ensure_folder,receive,send_file,remove_object,rename_object};
|
||||
static int write_file(const char *path,const char *value){FILE *f=fopen(path,"wb");CHECK(f!=NULL);CHECK(fwrite(value,1,strlen(value),f)==strlen(value));CHECK(fclose(f)==0);return 0;}
|
||||
static int test_pull_and_manifest_last(void){char root[]="/tmp/trainlog-generation-mtp-XXXXXX",path[512],diag[256]={0};char *manifest;reset();CHECK(mkdtemp(root)!=NULL);add(4,2,true,"android-objects",NULL);add(5,4,true,"generations",NULL);add(6,5,true,"gen_11111111-1111-4111-8111-111111111111",NULL);add(7,6,false,"payload.json","payload");add(8,6,false,"manifest.json","manifest");CHECK(trainlog_generation_mtp_pull(&io,"peer_11111111-1111-4111-8111-111111111111",root,diag,sizeof(diag))==TRAINLOG_STATUS_OK);snprintf(path,sizeof(path),"%s/android-objects/generations/gen_11111111-1111-4111-8111-111111111111/payload.json",root);CHECK(access(path,R_OK)==0);snprintf(path,sizeof(path),"%s/out",root);CHECK(mkdir(path,0700)==0);char gen[512];snprintf(gen,sizeof(gen),"%s/generation",path);CHECK(mkdir(gen,0700)==0);char artifact[512];snprintf(artifact,sizeof(artifact),"%s/artifact.json",gen);CHECK(write_file(artifact,"a")==0);char marker[512];snprintf(marker,sizeof(marker),"%s/manifest.json",gen);CHECK(write_file(marker,"m")==0);CHECK(trainlog_generation_mtp_push(&io,"peer_11111111-1111-4111-8111-111111111111",root,diag,sizeof(diag))==TRAINLOG_STATUS_OK);manifest=strstr(send_order,"manifest.json\n");CHECK(manifest!=NULL);CHECK(strstr(send_order,"artifact.json\n")<manifest);return 0;}
|
||||
static int test_ambiguity_and_truncation(void){char root[]="/tmp/trainlog-generation-mtp-error-XXXXXX",diag[256]={0},path[512];TrainlogStatus status;Object *peer;reset();CHECK(mkdtemp(root)!=NULL);device_count=2U;CHECK(trainlog_generation_mtp_pull(&io,"peer_11111111-1111-4111-8111-111111111111",root,diag,sizeof(diag))==TRAINLOG_STATUS_CONFLICT);reset();peer=find(3);CHECK(peer!=NULL);snprintf(peer->data,sizeof(peer->data),"not-json containing peer_11111111-1111-4111-8111-111111111111 generation-manifest-v1 generation-ack-v1");peer->size=strlen(peer->data);CHECK(trainlog_generation_mtp_pull(&io,"peer_11111111-1111-4111-8111-111111111111",root,diag,sizeof(diag))==TRAINLOG_STATUS_NOT_FOUND);reset();add(9,2,false,"bad.bin","abc");find(9)->size=99U;status=trainlog_generation_mtp_pull(&io,"peer_11111111-1111-4111-8111-111111111111",root,diag,sizeof(diag));CHECK(status!=TRAINLOG_STATUS_OK);snprintf(path,sizeof(path),"%s/bad.bin",root);CHECK(access(path,F_OK)!=0);return 0;}
|
||||
int main(void){CHECK(test_pull_and_manifest_last()==0);CHECK(test_ambiguity_and_truncation()==0);return 0;}
|
||||
#define CHECK(x) \
|
||||
do { \
|
||||
if (!(x)) { \
|
||||
fprintf(stderr, "CHECK %s:%d: %s\n", __FILE__, __LINE__, #x); \
|
||||
return 1; \
|
||||
} \
|
||||
} while (0)
|
||||
typedef struct Object {
|
||||
uint32_t id, parent;
|
||||
bool folder;
|
||||
char name[256];
|
||||
char data[4096];
|
||||
size_t size;
|
||||
} Object;
|
||||
static Object objects[64];
|
||||
static size_t object_count;
|
||||
static unsigned int device_count = 1U;
|
||||
static char send_order[1024];
|
||||
static void add(uint32_t id, uint32_t parent, bool folder, const char *name, const char *data) {
|
||||
Object *o = &objects[object_count++];
|
||||
memset(o, 0, sizeof(*o));
|
||||
o->id = id;
|
||||
o->parent = parent;
|
||||
o->folder = folder;
|
||||
snprintf(o->name, sizeof(o->name), "%s", name);
|
||||
if (data) {
|
||||
o->size = strlen(data);
|
||||
memcpy(o->data, data, o->size);
|
||||
}
|
||||
}
|
||||
static void reset(void) {
|
||||
object_count = 0U;
|
||||
device_count = 1U;
|
||||
send_order[0] = '\0';
|
||||
add(1, UINT32_MAX, true, "Documents", NULL);
|
||||
add(2, 1, true, "Trainlog", NULL);
|
||||
add(3,
|
||||
2,
|
||||
false,
|
||||
"android-peer-v1.json",
|
||||
"{\"format\":\"trainlog-sync-peer\",\"version\":1,\"peer_id\":\"peer_11111111-1111-4111-"
|
||||
"8111-111111111111\",\"capabilities\":[\"generation-manifest-v1\",\"generation-ack-v1\"]}");
|
||||
}
|
||||
static TrainlogStatus devices(TrainlogUsbDevice *out, size_t cap, size_t *count) {
|
||||
size_t i;
|
||||
if (cap < device_count)
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
*count = device_count;
|
||||
for (i = 0; i < device_count; i++) {
|
||||
memset(&out[i], 0, sizeof(out[i]));
|
||||
out[i].bus_number = 1U;
|
||||
out[i].device_number = (unsigned int)i + 1U;
|
||||
}
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
static TrainlogStatus
|
||||
storages(unsigned int bus, unsigned int dev, TrainlogMtpStorage *out, size_t cap, size_t *count) {
|
||||
(void)bus;
|
||||
if (cap < 1U)
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->storage_id = dev;
|
||||
*count = 1U;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
static TrainlogStatus children(unsigned int bus,
|
||||
unsigned int dev,
|
||||
uint32_t storage,
|
||||
uint32_t parent,
|
||||
TrainlogMtpEntry *out,
|
||||
size_t cap,
|
||||
size_t *count) {
|
||||
size_t i, n = 0;
|
||||
(void)bus;
|
||||
(void)storage;
|
||||
for (i = 0; i < object_count; i++)
|
||||
if (objects[i].parent == parent && (dev == 1U || objects[i].id <= 3U)) {
|
||||
if (n >= cap)
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
memset(&out[n], 0, sizeof(out[n]));
|
||||
out[n].item_id = objects[i].id;
|
||||
out[n].parent_id = parent;
|
||||
out[n].storage_id = dev;
|
||||
out[n].folder = objects[i].folder;
|
||||
out[n].size_bytes = objects[i].size;
|
||||
snprintf(out[n].name, sizeof(out[n].name), "%s", objects[i].name);
|
||||
n++;
|
||||
}
|
||||
*count = n;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
static TrainlogStatus ensure_folder(unsigned int b,
|
||||
unsigned int d,
|
||||
uint32_t s,
|
||||
uint32_t p,
|
||||
const char *name,
|
||||
uint32_t *id,
|
||||
bool *created) {
|
||||
size_t i;
|
||||
(void)b;
|
||||
(void)d;
|
||||
(void)s;
|
||||
for (i = 0; i < object_count; i++)
|
||||
if (objects[i].parent == p && objects[i].folder && strcmp(objects[i].name, name) == 0) {
|
||||
*id = objects[i].id;
|
||||
*created = false;
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
*id = (uint32_t)(100U + object_count);
|
||||
*created = true;
|
||||
add(*id, p, true, name, NULL);
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
static Object *find(uint32_t id) {
|
||||
size_t i;
|
||||
for (i = 0; i < object_count; i++)
|
||||
if (objects[i].id == id)
|
||||
return &objects[i];
|
||||
return NULL;
|
||||
}
|
||||
static TrainlogStatus receive(unsigned int b, unsigned int d, uint32_t id, const char *path) {
|
||||
FILE *f;
|
||||
Object *o = find(id);
|
||||
size_t bytes;
|
||||
(void)b;
|
||||
(void)d;
|
||||
if (!o)
|
||||
return TRAINLOG_STATUS_NOT_FOUND;
|
||||
f = fopen(path, "wb");
|
||||
if (!f)
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
bytes = id == 9U ? 3U : o->size;
|
||||
if (fwrite(o->data, 1, bytes, f) != bytes) {
|
||||
fclose(f);
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
return fclose(f) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
static TrainlogStatus send_file(unsigned int b,
|
||||
unsigned int d,
|
||||
uint32_t s,
|
||||
uint32_t p,
|
||||
const char *path,
|
||||
const char *name,
|
||||
uint32_t *id) {
|
||||
FILE *f;
|
||||
Object *o;
|
||||
long size;
|
||||
(void)b;
|
||||
(void)d;
|
||||
(void)s;
|
||||
f = fopen(path, "rb");
|
||||
if (!f)
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
fseek(f, 0, SEEK_END);
|
||||
size = ftell(f);
|
||||
rewind(f);
|
||||
if (size < 0 || size > (long)sizeof(objects[0].data)) {
|
||||
fclose(f);
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
}
|
||||
*id = (uint32_t)(100U + object_count);
|
||||
add(*id, p, false, name, NULL);
|
||||
o = &objects[object_count - 1];
|
||||
o->size = (size_t)size;
|
||||
if (fread(o->data, 1, o->size, f) != o->size) {
|
||||
fclose(f);
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
fclose(f);
|
||||
strncat(send_order, name, sizeof(send_order) - strlen(send_order) - 2U);
|
||||
strncat(send_order, "\n", sizeof(send_order) - strlen(send_order) - 1U);
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
static TrainlogStatus remove_object(unsigned int b, unsigned int d, uint32_t id) {
|
||||
Object *o = find(id);
|
||||
(void)b;
|
||||
(void)d;
|
||||
if (!o)
|
||||
return TRAINLOG_STATUS_NOT_FOUND;
|
||||
o->name[0] = '\0';
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
static TrainlogStatus rename_object(unsigned int b, unsigned int d, uint32_t id, const char *name) {
|
||||
Object *o = find(id);
|
||||
(void)b;
|
||||
(void)d;
|
||||
if (!o)
|
||||
return TRAINLOG_STATUS_NOT_FOUND;
|
||||
snprintf(o->name, sizeof(o->name), "%s", name);
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
static const TrainlogGenerationMtpIo io = {
|
||||
devices, storages, children, ensure_folder, receive, send_file, remove_object, rename_object};
|
||||
static int write_file(const char *path, const char *value) {
|
||||
FILE *f = fopen(path, "wb");
|
||||
CHECK(f != NULL);
|
||||
CHECK(fwrite(value, 1, strlen(value), f) == strlen(value));
|
||||
CHECK(fclose(f) == 0);
|
||||
return 0;
|
||||
}
|
||||
static int test_pull_and_manifest_last(void) {
|
||||
char root[] = "/tmp/trainlog-generation-mtp-XXXXXX", path[512], diag[256] = {0};
|
||||
char *manifest;
|
||||
reset();
|
||||
CHECK(mkdtemp(root) != NULL);
|
||||
add(4, 2, true, "android-objects", NULL);
|
||||
add(5, 4, true, "generations", NULL);
|
||||
add(6, 5, true, "gen_11111111-1111-4111-8111-111111111111", NULL);
|
||||
add(7, 6, false, "payload.json", "payload");
|
||||
add(8, 6, false, "manifest.json", "manifest");
|
||||
CHECK(trainlog_generation_mtp_pull(
|
||||
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
|
||||
TRAINLOG_STATUS_OK);
|
||||
snprintf(path,
|
||||
sizeof(path),
|
||||
"%s/android-objects/generations/gen_11111111-1111-4111-8111-111111111111/payload.json",
|
||||
root);
|
||||
CHECK(access(path, R_OK) == 0);
|
||||
snprintf(path, sizeof(path), "%s/out", root);
|
||||
CHECK(mkdir(path, 0700) == 0);
|
||||
char gen[512];
|
||||
snprintf(gen, sizeof(gen), "%s/generation", path);
|
||||
CHECK(mkdir(gen, 0700) == 0);
|
||||
char artifact[512];
|
||||
snprintf(artifact, sizeof(artifact), "%s/artifact.json", gen);
|
||||
CHECK(write_file(artifact, "a") == 0);
|
||||
char marker[512];
|
||||
snprintf(marker, sizeof(marker), "%s/manifest.json", gen);
|
||||
CHECK(write_file(marker, "m") == 0);
|
||||
CHECK(trainlog_generation_mtp_push(
|
||||
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
|
||||
TRAINLOG_STATUS_OK);
|
||||
manifest = strstr(send_order, "manifest.json\n");
|
||||
CHECK(manifest != NULL);
|
||||
CHECK(strstr(send_order, "artifact.json\n") < manifest);
|
||||
return 0;
|
||||
}
|
||||
static int test_ambiguity_and_truncation(void) {
|
||||
char root[] = "/tmp/trainlog-generation-mtp-error-XXXXXX", diag[256] = {0}, path[512];
|
||||
TrainlogStatus status;
|
||||
Object *peer;
|
||||
reset();
|
||||
CHECK(mkdtemp(root) != NULL);
|
||||
device_count = 2U;
|
||||
CHECK(trainlog_generation_mtp_pull(
|
||||
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
|
||||
TRAINLOG_STATUS_CONFLICT);
|
||||
reset();
|
||||
peer = find(3);
|
||||
CHECK(peer != NULL);
|
||||
snprintf(peer->data,
|
||||
sizeof(peer->data),
|
||||
"not-json containing peer_11111111-1111-4111-8111-111111111111 generation-manifest-v1 "
|
||||
"generation-ack-v1");
|
||||
peer->size = strlen(peer->data);
|
||||
CHECK(trainlog_generation_mtp_pull(
|
||||
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
|
||||
TRAINLOG_STATUS_NOT_FOUND);
|
||||
reset();
|
||||
add(9, 2, false, "bad.bin", "abc");
|
||||
find(9)->size = 99U;
|
||||
status = trainlog_generation_mtp_pull(
|
||||
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag));
|
||||
CHECK(status != TRAINLOG_STATUS_OK);
|
||||
snprintf(path, sizeof(path), "%s/bad.bin", root);
|
||||
CHECK(access(path, F_OK) != 0);
|
||||
return 0;
|
||||
}
|
||||
int main(void) {
|
||||
CHECK(test_pull_and_manifest_last() == 0);
|
||||
CHECK(test_ambiguity_and_truncation() == 0);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,24 +20,27 @@
|
|||
#include "trainlog/web_server.h"
|
||||
#include "web_assets.h"
|
||||
|
||||
#define CHECK(value) do { if (!(value)) { \
|
||||
(void)fprintf(stderr, "CHECK failed at %s:%d: %s\n", \
|
||||
__FILE__, __LINE__, #value); return false; } } while (0)
|
||||
#define CHECK(value) \
|
||||
do { \
|
||||
if (!(value)) { \
|
||||
(void)fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #value); \
|
||||
return false; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static volatile sig_atomic_t child_stop;
|
||||
|
||||
static void stop_child(int signal_number)
|
||||
{
|
||||
static void stop_child(int signal_number) {
|
||||
(void)signal_number;
|
||||
child_stop = 1;
|
||||
}
|
||||
|
||||
static int reserve_port(uint16_t *port, int keep_open)
|
||||
{
|
||||
static int reserve_port(uint16_t *port, int keep_open) {
|
||||
struct sockaddr_in address;
|
||||
socklen_t length = (socklen_t)sizeof(address);
|
||||
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (socket_fd < 0) return -1;
|
||||
if (socket_fd < 0)
|
||||
return -1;
|
||||
(void)memset(&address, 0, sizeof(address));
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
|
|
@ -56,8 +59,7 @@ static int reserve_port(uint16_t *port, int keep_open)
|
|||
return socket_fd;
|
||||
}
|
||||
|
||||
static int connect_with_retry(uint16_t port)
|
||||
{
|
||||
static int connect_with_retry(uint16_t port) {
|
||||
struct sockaddr_in address;
|
||||
struct timespec delay = {0, 10000000L};
|
||||
size_t attempt;
|
||||
|
|
@ -67,18 +69,17 @@ static int connect_with_retry(uint16_t port)
|
|||
address.sin_port = htons(port);
|
||||
for (attempt = 0U; attempt < 200U; ++attempt) {
|
||||
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (socket_fd >= 0 && connect(socket_fd,
|
||||
(const struct sockaddr *)&address, sizeof(address)) == 0)
|
||||
if (socket_fd >= 0 &&
|
||||
connect(socket_fd, (const struct sockaddr *)&address, sizeof(address)) == 0)
|
||||
return socket_fd;
|
||||
if (socket_fd >= 0) (void)close(socket_fd);
|
||||
if (socket_fd >= 0)
|
||||
(void)close(socket_fd);
|
||||
(void)nanosleep(&delay, NULL);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool exchange(uint16_t port, const char *request, char *response,
|
||||
size_t capacity)
|
||||
{
|
||||
static bool exchange(uint16_t port, const char *request, char *response, size_t capacity) {
|
||||
size_t sent = 0U;
|
||||
size_t used = 0U;
|
||||
size_t length = strlen(request);
|
||||
|
|
@ -92,7 +93,8 @@ static bool exchange(uint16_t port, const char *request, char *response,
|
|||
(void)shutdown(socket_fd, SHUT_WR);
|
||||
while (used + 1U < capacity) {
|
||||
ssize_t count = recv(socket_fd, response + used, capacity - used - 1U, 0);
|
||||
if (count == 0) break;
|
||||
if (count == 0)
|
||||
break;
|
||||
CHECK(count > 0);
|
||||
used += (size_t)count;
|
||||
}
|
||||
|
|
@ -101,19 +103,16 @@ static bool exchange(uint16_t port, const char *request, char *response,
|
|||
return true;
|
||||
}
|
||||
|
||||
static bool asset_path(const char *html, const char *suffix, char *output,
|
||||
size_t capacity)
|
||||
{
|
||||
static bool asset_path(const char *html, const char *suffix, char *output, size_t capacity) {
|
||||
const char *start = html;
|
||||
size_t suffix_length = strlen(suffix);
|
||||
while ((start = strstr(start, "/assets/")) != NULL) {
|
||||
const char *end = start;
|
||||
size_t length;
|
||||
while (*end != '\0' && *end != '"' && *end != '\'' && *end != '<' &&
|
||||
*end != '>') ++end;
|
||||
while (*end != '\0' && *end != '"' && *end != '\'' && *end != '<' && *end != '>')
|
||||
++end;
|
||||
length = (size_t)(end - start);
|
||||
if (length >= suffix_length &&
|
||||
memcmp(end - suffix_length, suffix, suffix_length) == 0 &&
|
||||
if (length >= suffix_length && memcmp(end - suffix_length, suffix, suffix_length) == 0 &&
|
||||
length < capacity) {
|
||||
(void)memcpy(output, start, length);
|
||||
output[length] = '\0';
|
||||
|
|
@ -124,18 +123,25 @@ static bool asset_path(const char *html, const char *suffix, char *output,
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool response_header(const char *response, const char *name,
|
||||
char *output, size_t capacity)
|
||||
{
|
||||
const char *start = strstr(response, name); const char *end; size_t length;
|
||||
if (start == NULL) return false; start += strlen(name);
|
||||
end = strstr(start, "\r\n"); if (end == NULL) return false;
|
||||
length = (size_t)(end - start); if (length >= capacity) return false;
|
||||
(void)memcpy(output, start, length); output[length] = '\0'; return true;
|
||||
static bool response_header(const char *response, const char *name, char *output, size_t capacity) {
|
||||
const char *start = strstr(response, name);
|
||||
const char *end;
|
||||
size_t length;
|
||||
if (start == NULL)
|
||||
return false;
|
||||
start += strlen(name);
|
||||
end = strstr(start, "\r\n");
|
||||
if (end == NULL)
|
||||
return false;
|
||||
length = (size_t)(end - start);
|
||||
if (length >= capacity)
|
||||
return false;
|
||||
(void)memcpy(output, start, length);
|
||||
output[length] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool test_http_contract(TrainlogDatabase *database)
|
||||
{
|
||||
static bool test_http_contract(TrainlogDatabase *database) {
|
||||
char response[32768];
|
||||
char large_request[12000];
|
||||
char request[8192];
|
||||
|
|
@ -156,44 +162,60 @@ static bool test_http_contract(TrainlogDatabase *database)
|
|||
(void)sigemptyset(&action.sa_mask);
|
||||
(void)sigaction(SIGTERM, &action, NULL);
|
||||
child_stop = 0;
|
||||
_exit(trainlog_web_server_run(database, "/tmp/trainlog-web-test.db", port, &child_stop,
|
||||
diagnostic, sizeof(diagnostic)) == 0 ? 0 : 1);
|
||||
_exit(trainlog_web_server_run(database,
|
||||
"/tmp/trainlog-web-test.db",
|
||||
port,
|
||||
&child_stop,
|
||||
diagnostic,
|
||||
sizeof(diagnostic)) == 0
|
||||
? 0
|
||||
: 1);
|
||||
}
|
||||
CHECK(exchange(port,
|
||||
"GET /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response, sizeof(response)));
|
||||
"GET /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)));
|
||||
CHECK(strstr(response, "HTTP/1.1 200") != NULL);
|
||||
CHECK(strstr(response, "Content-Type: application/json; charset=utf-8") != NULL);
|
||||
CHECK(strstr(response, "X-Content-Type-Options: nosniff") != NULL);
|
||||
CHECK(strstr(response, "Content-Security-Policy: default-src 'none'") != NULL);
|
||||
CHECK(strstr(response, "Access-Control-Allow-Origin") == NULL);
|
||||
CHECK(strstr(response,
|
||||
"{\"api_version\":1,\"status\":\"ok\",\"product\":\"trainlog\","
|
||||
"\"version\":\"0.1.2\"}") != NULL);
|
||||
"{\"api_version\":1,\"status\":\"ok\",\"product\":\"trainlog\","
|
||||
"\"version\":\"0.1.2\"}") != NULL);
|
||||
{
|
||||
char token[80];
|
||||
CHECK(exchange(port,
|
||||
"GET /api/v1/sync/status HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL);
|
||||
"GET /api/v1/sync/status HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 200") != NULL);
|
||||
CHECK(strstr(response, "\"phase\":\"idle\"") != NULL);
|
||||
CHECK(response_header(response, "X-Trainlog-CSRF-Token: ", token,
|
||||
sizeof(token)) && strlen(token) == 64U);
|
||||
(void)snprintf(request, sizeof(request),
|
||||
CHECK(response_header(response, "X-Trainlog-CSRF-Token: ", token, sizeof(token)) &&
|
||||
strlen(token) == 64U);
|
||||
(void)snprintf(
|
||||
request,
|
||||
sizeof(request),
|
||||
"POST /api/v1/sync HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Origin: http://127.0.0.1:%u\r\nContent-Type: application/json\r\n"
|
||||
"X-Trainlog-CSRF-Token: %s\r\nContent-Length: 72\r\n\r\n"
|
||||
"{\"request_id\":\"sy_11111111-1111-4111-8111-111111111111\",\"trigger\":\"web\"}",
|
||||
(unsigned int)port, token);
|
||||
(unsigned int)port,
|
||||
token);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 409") != NULL &&
|
||||
strstr(response, "full_generation_disabled") != NULL);
|
||||
strstr(response, "HTTP/1.1 409") != NULL &&
|
||||
strstr(response, "full_generation_disabled") != NULL);
|
||||
CHECK(exchange(port,
|
||||
"GET /api/v1/sync HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 405") != NULL);
|
||||
"GET /api/v1/sync HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 405") != NULL);
|
||||
}
|
||||
CHECK(exchange(port,
|
||||
"GET /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL);
|
||||
"GET /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 200") != NULL);
|
||||
CHECK(strstr(response, "Content-Type: application/json; charset=utf-8") != NULL);
|
||||
CHECK(strstr(response, "\"api_version\":1") != NULL);
|
||||
CHECK(strstr(response, "\"no_persisted_executable_plan\"") != NULL);
|
||||
|
|
@ -203,58 +225,94 @@ static bool test_http_contract(TrainlogDatabase *database)
|
|||
{
|
||||
TrainlogDashboardLayout layout;
|
||||
char body[TRAINLOG_DASHBOARD_LAYOUT_JSON_CAPACITY];
|
||||
char token[80]; char etag[32]; size_t body_size;
|
||||
char token[80];
|
||||
char etag[32];
|
||||
size_t body_size;
|
||||
CHECK(exchange(port,
|
||||
"GET /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL);
|
||||
"GET /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 200") != NULL);
|
||||
CHECK(strstr(response, "\"source\":\"default\"") != NULL);
|
||||
CHECK(response_header(response, "ETag: ", etag, sizeof(etag)) && strcmp(etag, "\"0\"") == 0);
|
||||
CHECK(response_header(response, "X-Trainlog-CSRF-Token: ", token, sizeof(token)) && strlen(token) == 64U);
|
||||
trainlog_dashboard_layout_default(&layout); layout.tiles[3].y = 14U;
|
||||
CHECK(trainlog_dashboard_layout_serialize(&layout,
|
||||
TRAINLOG_DASHBOARD_LAYOUT_DEFAULT, false, body, sizeof(body), &body_size));
|
||||
(void)snprintf(request, sizeof(request),
|
||||
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Origin: http://127.0.0.1:%u\r\nContent-Type: application/json\r\n"
|
||||
"If-Match: \"0\"\r\nX-Trainlog-CSRF-Token: wrong\r\n"
|
||||
"Content-Length: %zu\r\n\r\n%s", (unsigned int)port, body_size, body);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 403") != NULL);
|
||||
(void)snprintf(request, sizeof(request),
|
||||
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Origin: http://127.0.0.1:%u\r\nContent-Type: text/plain\r\n"
|
||||
"If-Match: \"0\"\r\nX-Trainlog-CSRF-Token: %s\r\n"
|
||||
"Content-Length: %zu\r\n\r\n%s", (unsigned int)port, token, body_size, body);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 415") != NULL);
|
||||
(void)snprintf(request, sizeof(request),
|
||||
CHECK(response_header(response, "ETag: ", etag, sizeof(etag)) &&
|
||||
strcmp(etag, "\"0\"") == 0);
|
||||
CHECK(response_header(response, "X-Trainlog-CSRF-Token: ", token, sizeof(token)) &&
|
||||
strlen(token) == 64U);
|
||||
trainlog_dashboard_layout_default(&layout);
|
||||
layout.tiles[3].y = 14U;
|
||||
CHECK(trainlog_dashboard_layout_serialize(
|
||||
&layout, TRAINLOG_DASHBOARD_LAYOUT_DEFAULT, false, body, sizeof(body), &body_size));
|
||||
(void)snprintf(request,
|
||||
sizeof(request),
|
||||
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Origin: http://127.0.0.1:%u\r\nContent-Type: application/json\r\n"
|
||||
"If-Match: \"0\"\r\nX-Trainlog-CSRF-Token: wrong\r\n"
|
||||
"Content-Length: %zu\r\n\r\n%s",
|
||||
(unsigned int)port,
|
||||
body_size,
|
||||
body);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 403") != NULL);
|
||||
(void)snprintf(request,
|
||||
sizeof(request),
|
||||
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Origin: http://127.0.0.1:%u\r\nContent-Type: text/plain\r\n"
|
||||
"If-Match: \"0\"\r\nX-Trainlog-CSRF-Token: %s\r\n"
|
||||
"Content-Length: %zu\r\n\r\n%s",
|
||||
(unsigned int)port,
|
||||
token,
|
||||
body_size,
|
||||
body);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 415") != NULL);
|
||||
(void)snprintf(
|
||||
request,
|
||||
sizeof(request),
|
||||
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Origin: http://127.0.0.1:%u\r\nContent-Type: application/json\r\n"
|
||||
"If-Match: \"0\"\r\nX-Trainlog-CSRF-Token: %s\r\nContent-Length: %zu\r\n\r\n%s",
|
||||
(unsigned int)port, token, body_size, body);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL);
|
||||
CHECK(strstr(response, "\"revision\":1") != NULL && strstr(response, "\"source\":\"persisted\"") != NULL);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 412") != NULL);
|
||||
(void)snprintf(request, sizeof(request),
|
||||
"DELETE /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Origin: http://trainlog.perf\r\nIf-Match: \"0\"\r\n"
|
||||
"X-Trainlog-CSRF-Token: %s\r\nContent-Length: 0\r\n\r\n", token);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 412") != NULL);
|
||||
(void)snprintf(request, sizeof(request),
|
||||
"DELETE /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Origin: http://trainlog.perf\r\nIf-Match: \"1\"\r\n"
|
||||
"X-Trainlog-CSRF-Token: %s\r\nContent-Length: 0\r\n\r\n", token);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL &&
|
||||
strstr(response, "\"source\":\"default\"") != NULL);
|
||||
(unsigned int)port,
|
||||
token,
|
||||
body_size,
|
||||
body);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 200") != NULL);
|
||||
CHECK(strstr(response, "\"revision\":1") != NULL &&
|
||||
strstr(response, "\"source\":\"persisted\"") != NULL);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 412") != NULL);
|
||||
(void)snprintf(request,
|
||||
sizeof(request),
|
||||
"DELETE /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Origin: http://trainlog.perf\r\nIf-Match: \"0\"\r\n"
|
||||
"X-Trainlog-CSRF-Token: %s\r\nContent-Length: 0\r\n\r\n",
|
||||
token);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 412") != NULL);
|
||||
(void)snprintf(request,
|
||||
sizeof(request),
|
||||
"DELETE /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Origin: http://trainlog.perf\r\nIf-Match: \"1\"\r\n"
|
||||
"X-Trainlog-CSRF-Token: %s\r\nContent-Length: 0\r\n\r\n",
|
||||
token);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 200") != NULL &&
|
||||
strstr(response, "\"source\":\"default\"") != NULL);
|
||||
CHECK(exchange(port,
|
||||
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Content-Type: application/json\r\nContent-Length: 2\r\n\r\n{}",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 403") != NULL);
|
||||
"PUT /api/v1/dashboard-layout HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Content-Type: application/json\r\nContent-Length: 2\r\n\r\n{}",
|
||||
response,
|
||||
sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 403") != NULL);
|
||||
}
|
||||
CHECK(exchange(port,
|
||||
"POST /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 405") != NULL &&
|
||||
strstr(response, "Allow: GET") != NULL);
|
||||
CHECK(exchange(port, "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 200") != NULL);
|
||||
CHECK(
|
||||
exchange(port,
|
||||
"POST /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 405") != NULL && strstr(response, "Allow: GET") != NULL);
|
||||
CHECK(exchange(port, "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", response, sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 200") != NULL);
|
||||
CHECK(strstr(response, "Content-Type: text/html; charset=utf-8") != NULL);
|
||||
CHECK(strstr(response, "Cache-Control: no-cache") != NULL);
|
||||
CHECK(strstr(response, "Content-Security-Policy: default-src 'self'") != NULL);
|
||||
|
|
@ -263,81 +321,91 @@ static bool test_http_contract(TrainlogDatabase *database)
|
|||
CHECK(strstr(response, "<title>Trainlog</title>") != NULL);
|
||||
CHECK(asset_path(response, ".js", javascript_path, sizeof(javascript_path)));
|
||||
CHECK(asset_path(response, ".css", stylesheet_path, sizeof(stylesheet_path)));
|
||||
(void)snprintf(request, sizeof(request),
|
||||
"GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", javascript_path);
|
||||
(void)snprintf(
|
||||
request, sizeof(request), "GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", javascript_path);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 200") != NULL);
|
||||
strstr(response, "HTTP/1.1 200") != NULL);
|
||||
CHECK(strstr(response, "Content-Type: text/javascript; charset=utf-8") != NULL);
|
||||
CHECK(strstr(response,
|
||||
"Cache-Control: public, max-age=31536000, immutable") != NULL);
|
||||
CHECK(strstr(response, "Cache-Control: public, max-age=31536000, immutable") != NULL);
|
||||
CHECK(strstr(response, "ETag: \"sha256-") != NULL);
|
||||
(void)snprintf(request, sizeof(request),
|
||||
"GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", stylesheet_path);
|
||||
(void)snprintf(
|
||||
request, sizeof(request), "GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", stylesheet_path);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) &&
|
||||
strstr(response, "Content-Type: text/css; charset=utf-8") != NULL);
|
||||
strstr(response, "Content-Type: text/css; charset=utf-8") != NULL);
|
||||
CHECK(
|
||||
exchange(
|
||||
port, "GET /analyse HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", response, sizeof(response)) &&
|
||||
strstr(response, "<title>Trainlog</title>") != NULL);
|
||||
CHECK(exchange(port,
|
||||
"GET /analyse HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "<title>Trainlog</title>") != NULL);
|
||||
"GET /api/v1/unknown HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 404") != NULL &&
|
||||
strstr(response, "<title>Trainlog</title>") == NULL);
|
||||
CHECK(exchange(port,
|
||||
"GET /api/v1/unknown HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 404") != NULL &&
|
||||
strstr(response, "<title>Trainlog</title>") == NULL);
|
||||
"GET /assets/unknown.js HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 404") != NULL);
|
||||
CHECK(exchange(port,
|
||||
"GET /assets/unknown.js HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 404") != NULL);
|
||||
CHECK(exchange(port,
|
||||
"GET /assets/../index.html HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 404") != NULL);
|
||||
(void)snprintf(request, sizeof(request),
|
||||
"HEAD %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", javascript_path);
|
||||
"GET /assets/../index.html HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 404") != NULL);
|
||||
(void)snprintf(
|
||||
request, sizeof(request), "HEAD %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", javascript_path);
|
||||
CHECK(exchange(port, request, response, sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 200") != NULL &&
|
||||
strstr(response, "Content-Type: text/javascript; charset=utf-8") != NULL);
|
||||
strstr(response, "HTTP/1.1 200") != NULL &&
|
||||
strstr(response, "Content-Type: text/javascript; charset=utf-8") != NULL);
|
||||
CHECK(exchange(port,
|
||||
"POST /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 405") != NULL);
|
||||
"POST /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 405") != NULL);
|
||||
CHECK(strstr(response, "Allow: GET") != NULL);
|
||||
CHECK(exchange(port,
|
||||
"GET /api/v1/health HTTP/1.1\r\nHost: foreign.example\r\n\r\n",
|
||||
response, sizeof(response)) && strstr(response, "HTTP/1.1 400") != NULL);
|
||||
(void)snprintf(large_request, sizeof(large_request),
|
||||
"POST /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Content-Length: 5000\r\n\r\n%05000d", 0);
|
||||
"GET /api/v1/health HTTP/1.1\r\nHost: foreign.example\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 400") != NULL);
|
||||
(void)snprintf(large_request,
|
||||
sizeof(large_request),
|
||||
"POST /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\n"
|
||||
"Content-Length: 5000\r\n\r\n%05000d",
|
||||
0);
|
||||
CHECK(exchange(port, large_request, response, sizeof(response)) &&
|
||||
strstr(response, "HTTP/1.1 413") != NULL);
|
||||
strstr(response, "HTTP/1.1 413") != NULL);
|
||||
(void)memset(large_request, 'a', sizeof(large_request));
|
||||
(void)snprintf(large_request, 128U,
|
||||
"GET /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\nX-Large: ");
|
||||
(void)snprintf(
|
||||
large_request, 128U, "GET /api/v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\nX-Large: ");
|
||||
{
|
||||
size_t prefix = strlen(large_request);
|
||||
(void)memset(large_request + prefix, 'a', 8500U);
|
||||
(void)memcpy(large_request + prefix + 8500U, "\r\n\r\n", 5U);
|
||||
}
|
||||
CHECK(exchange(port, large_request, response, sizeof(response)) &&
|
||||
(strstr(response, "HTTP/1.1 431") != NULL ||
|
||||
strstr(response, "HTTP/1.1 400") != NULL));
|
||||
(strstr(response, "HTTP/1.1 431") != NULL || strstr(response, "HTTP/1.1 400") != NULL));
|
||||
CHECK(kill(child, SIGTERM) == 0);
|
||||
CHECK(waitpid(child, &status, 0) == child);
|
||||
CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool test_port_in_use(TrainlogDatabase *database)
|
||||
{
|
||||
static bool test_port_in_use(TrainlogDatabase *database) {
|
||||
volatile sig_atomic_t stop = 0;
|
||||
char diagnostic[256];
|
||||
uint16_t port;
|
||||
int occupied = reserve_port(&port, 1);
|
||||
CHECK(occupied >= 0);
|
||||
CHECK(trainlog_web_server_run(database, "/tmp/trainlog-web-test.db", port, &stop, diagnostic,
|
||||
sizeof(diagnostic)) != 0);
|
||||
CHECK(trainlog_web_server_run(
|
||||
database, "/tmp/trainlog-web-test.db", port, &stop, diagnostic, sizeof(diagnostic)) !=
|
||||
0);
|
||||
CHECK(strstr(diagnostic, "127.0.0.1") != NULL);
|
||||
(void)close(occupied);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool test_dashboard_core_error_translation(void)
|
||||
{
|
||||
static bool test_dashboard_core_error_translation(void) {
|
||||
char path[] = "/tmp/trainlog-web-error-XXXXXX";
|
||||
char response[4096];
|
||||
TrainlogDatabase *database = NULL;
|
||||
|
|
@ -361,12 +429,15 @@ static bool test_dashboard_core_error_translation(void)
|
|||
if (child == 0) {
|
||||
volatile sig_atomic_t stop = 0;
|
||||
char diagnostic[256];
|
||||
_exit(trainlog_web_server_run(database, path, port, &stop, diagnostic,
|
||||
sizeof(diagnostic)) == 0 ? 0 : 1);
|
||||
_exit(trainlog_web_server_run(
|
||||
database, path, port, &stop, diagnostic, sizeof(diagnostic)) == 0
|
||||
? 0
|
||||
: 1);
|
||||
}
|
||||
CHECK(exchange(port,
|
||||
"GET /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response, sizeof(response)));
|
||||
"GET /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||
response,
|
||||
sizeof(response)));
|
||||
CHECK(strstr(response, "HTTP/1.1 500") != NULL);
|
||||
CHECK(strstr(response, "\"dashboard_unavailable\"") != NULL);
|
||||
CHECK(kill(child, SIGTERM) == 0);
|
||||
|
|
@ -376,8 +447,7 @@ static bool test_dashboard_core_error_translation(void)
|
|||
return true;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int main(void) {
|
||||
char config_root[] = "/tmp/trainlog-web-config-XXXXXX";
|
||||
TrainlogDatabase *database = NULL;
|
||||
bool passed;
|
||||
|
|
@ -388,11 +458,16 @@ int main(void)
|
|||
if (!trainlog_web_assets_available()) {
|
||||
volatile sig_atomic_t stop = 0;
|
||||
char diagnostic[256];
|
||||
passed = trainlog_web_server_run(database, "/tmp/trainlog-web-test.db", 8080U, &stop, diagnostic,
|
||||
sizeof(diagnostic)) != 0 && strstr(diagnostic, "frontend") != NULL;
|
||||
passed = trainlog_web_server_run(database,
|
||||
"/tmp/trainlog-web-test.db",
|
||||
8080U,
|
||||
&stop,
|
||||
diagnostic,
|
||||
sizeof(diagnostic)) != 0 &&
|
||||
strstr(diagnostic, "frontend") != NULL;
|
||||
} else {
|
||||
passed = test_http_contract(database) && test_port_in_use(database) &&
|
||||
test_dashboard_core_error_translation();
|
||||
test_dashboard_core_error_translation();
|
||||
}
|
||||
trainlog_database_close(database);
|
||||
return passed ? 0 : 1;
|
||||
|
|
|
|||
|
|
@ -2,21 +2,32 @@
|
|||
#include <string.h>
|
||||
#include "trainlog/generation_mtp.h"
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
char diagnostic[1024] = {0}; TrainlogStatus status;
|
||||
int main(int argc, char **argv) {
|
||||
char diagnostic[1024] = {0};
|
||||
TrainlogStatus status;
|
||||
if (argc == 2 && strcmp(argv[1], "--help") == 0) {
|
||||
(void)puts("usage: trainlog-generation-mtp-adapter pull|push EXPECTED_PEER LOCAL_ROOT");
|
||||
return 0;
|
||||
}
|
||||
if (argc != 4) return 64;
|
||||
if (strcmp(argv[1], "pull") == 0) status = trainlog_generation_mtp_pull(
|
||||
trainlog_generation_mtp_production_io(), argv[2], argv[3], diagnostic, sizeof(diagnostic));
|
||||
else if (strcmp(argv[1], "push") == 0) status = trainlog_generation_mtp_push(
|
||||
trainlog_generation_mtp_production_io(), argv[2], argv[3], diagnostic, sizeof(diagnostic));
|
||||
else return 64;
|
||||
if (argc != 4)
|
||||
return 64;
|
||||
if (strcmp(argv[1], "pull") == 0)
|
||||
status = trainlog_generation_mtp_pull(trainlog_generation_mtp_production_io(),
|
||||
argv[2],
|
||||
argv[3],
|
||||
diagnostic,
|
||||
sizeof(diagnostic));
|
||||
else if (strcmp(argv[1], "push") == 0)
|
||||
status = trainlog_generation_mtp_push(trainlog_generation_mtp_production_io(),
|
||||
argv[2],
|
||||
argv[3],
|
||||
diagnostic,
|
||||
sizeof(diagnostic));
|
||||
else
|
||||
return 64;
|
||||
if (status != TRAINLOG_STATUS_OK) {
|
||||
(void)fprintf(stderr, "MTP adapter failed status=%d diagnostic=%s\n", (int)status, diagnostic);
|
||||
(void)fprintf(
|
||||
stderr, "MTP adapter failed status=%d diagnostic=%s\n", (int)status, diagnostic);
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue