trainlog/tui/src/generation_mtp.c

649 lines
26 KiB
C

#include "trainlog/generation_mtp.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <yyjson.h>
typedef struct Selection {
unsigned int bus;
unsigned int device;
uint32_t storage;
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 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 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;
}
for (index = 0U; index < count; ++index) {
if (entries[index].folder == folder && strcmp(entries[index].name, name) == 0) {
*result = entries[index];
++matches;
}
}
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) {
int fd = open(path, O_RDONLY | O_CLOEXEC);
char buffer[16385];
ssize_t count;
yyjson_doc *document;
yyjson_val *root, *capabilities, *item;
size_t index, maximum;
bool manifest = false, acknowledgement = false, valid;
if (fd < 0) {
return false;
}
count = read(fd, buffer, sizeof(buffer));
(void)close(fd);
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;
}
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);
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;
}
}
}
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) {
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;
char peer_path[1024];
TrainlogStatus status;
if (snprintf(peer_path, sizeof(peer_path), "%s/.peer-candidate", local_root) < 0 ||
strlen(local_root) + strlen("/.peer-candidate") >= sizeof(peer_path)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
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;
}
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;
}
for (si = 0U; si < storage_count; ++si) {
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;
}
candidate.root = root.item_id;
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;
}
}
(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;
}
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) {
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;
}
*objects += count;
for (index = 0U; index < count; ++index) {
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 (entries[index].folder) {
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 += 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;
}
(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;
}
}
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus pull_named_file(const TrainlogGenerationMtpIo *io,
const Selection *selection,
uint32_t parent,
const char *name,
const char *local,
bool required) {
TrainlogMtpEntry entry;
TrainlogStatus status = one_named(io, selection, parent, name, false, &entry);
char path[1024], temporary[1060];
struct stat value;
int written;
if (status == TRAINLOG_STATUS_NOT_FOUND && !required) {
return TRAINLOG_STATUS_OK;
}
if (status != TRAINLOG_STATUS_OK || entry.size_bytes > 65536U) {
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_INVALID_ARGUMENT : status;
}
written = snprintf(path, sizeof(path), "%s/%s", local, name);
if (written < 0 || (size_t)written >= sizeof(path)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
written = snprintf(temporary, sizeof(temporary), "%s.part.%ld", path, (long)getpid());
if (written < 0 || (size_t)written >= sizeof(temporary)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
(void)unlink(temporary);
status = io->receive(selection->bus, selection->device, entry.item_id, temporary);
if (status == TRAINLOG_STATUS_OK &&
(stat(temporary, &value) != 0 || value.st_size < 0 ||
(uint64_t)value.st_size != entry.size_bytes || rename(temporary, path) != 0)) {
status = TRAINLOG_STATUS_SYSTEM_ERROR;
}
if (status != TRAINLOG_STATUS_OK) {
(void)unlink(temporary);
}
return status;
}
static bool referenced_android_generation(const char *local, char *generation, size_t size) {
char path[1024], buffer[65537];
int fd, written;
ssize_t count;
yyjson_doc *document;
yyjson_val *root, *relative;
const char *value, *prefix = "android-objects/generations/";
bool valid = false;
written = snprintf(path, sizeof(path), "%s/android-generation-v1.json", local);
if (written < 0 || (size_t)written >= sizeof(path) ||
(fd = open(path, O_RDONLY | O_CLOEXEC)) < 0) {
return false;
}
count = read(fd, buffer, sizeof(buffer));
(void)close(fd);
if (count <= 0 || (size_t)count >= sizeof(buffer)) {
return false;
}
document = yyjson_read(buffer, (size_t)count, YYJSON_READ_NOFLAG);
if (document == NULL) {
return false;
}
root = yyjson_doc_get_root(document);
relative = yyjson_obj_get(root, "relative_path");
value = yyjson_get_str(relative);
if (yyjson_is_obj(root) && value != NULL && strncmp(value, prefix, strlen(prefix)) == 0) {
const char *leaf = value + strlen(prefix);
if (safe_leaf(leaf) && strlen(leaf) < size) {
(void)memcpy(generation, leaf, strlen(leaf) + 1U);
valid = true;
}
}
yyjson_doc_free(document);
return valid;
}
static TrainlogStatus pull_referenced_generation(const TrainlogGenerationMtpIo *io,
const Selection *selection,
const char *local,
const char *generation) {
TrainlogMtpEntry android_objects, generations, selected;
char path[1024];
uint64_t total = 0U;
size_t objects = 0U;
TrainlogStatus status;
status = one_named(io, selection, selection->root, "android-objects", true, &android_objects);
if (status == TRAINLOG_STATUS_OK) {
status = one_named(io, selection, android_objects.item_id, "generations", true, &generations);
}
if (status == TRAINLOG_STATUS_OK) {
status = one_named(io, selection, generations.item_id, generation, true, &selected);
}
if (status != TRAINLOG_STATUS_OK ||
snprintf(path, sizeof(path), "%s/android-objects/generations/%s", local, generation) < 0 ||
strlen(local) + strlen("/android-objects/generations/") + strlen(generation) >= sizeof(path)) {
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_INVALID_ARGUMENT : status;
}
char parent[1024];
if (snprintf(parent, sizeof(parent), "%s/android-objects/generations", local) < 0 ||
strlen(local) + strlen("/android-objects/generations") >= sizeof(parent) ||
mkdir_private(local) != TRAINLOG_STATUS_OK) {
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
char objects_path[1024];
if (snprintf(objects_path, sizeof(objects_path), "%s/android-objects", local) < 0 ||
strlen(local) + strlen("/android-objects") >= sizeof(objects_path) ||
mkdir_private(objects_path) != TRAINLOG_STATUS_OK || mkdir_private(parent) != TRAINLOG_STATUS_OK) {
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
return pull_folder(io, selection, selected.item_id, path, 0U, &total, &objects);
}
static bool same_files(const char *left, const char *right) {
int left_fd = open(left, O_RDONLY | O_CLOEXEC);
int right_fd = open(right, O_RDONLY | O_CLOEXEC);
char left_buffer[8192];
char right_buffer[8192];
ssize_t left_count = 0;
ssize_t right_count;
bool same = true;
if (left_fd < 0 || right_fd < 0) {
same = false;
}
while (same && (left_count = read(left_fd, left_buffer, sizeof(left_buffer))) > 0) {
right_count = read(right_fd, right_buffer, (size_t)left_count);
if (right_count != left_count ||
memcmp(left_buffer, right_buffer, (size_t)left_count) != 0) {
same = false;
}
}
if (same && (left_count < 0 || read(right_fd, right_buffer, 1U) != 0)) {
same = false;
}
if (left_fd >= 0) {
(void)close(left_fd);
}
if (right_fd >= 0) {
(void)close(right_fd);
}
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;
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;
}
(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(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);
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);
}
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);
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;
}
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;
}
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 (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);
}
}
(void)closedir(directory);
return status;
}
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;
}
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;
}
++*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) {
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;
TrainlogStatus status;
char generation[256];
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) {
/* WHY: Documents/Trainlog retains prior generations and causal evidence.
* Recursively mirroring that durable history makes foreground sync cost
* grow with retention and can exhaust the run deadline before admission.
* CONTRACT: pull only current coordination objects and the one Android
* generation named by the current reference. Missing optional objects
* remain a normal polling state for the Python conversation.
* INVARIANT: no remote object, ACK, tombstone, or retained generation is
* removed or rewritten by this bounded read. */
status = pull_named_file(
io, &selection, selection.root, "android-peer-v1.json", local, true);
}
if (status == TRAINLOG_STATUS_OK) {
status = pull_named_file(
io, &selection, selection.root, "android-generation-v1.json", local, false);
}
if (status == TRAINLOG_STATUS_OK) {
status = pull_named_file(
io, &selection, selection.root, "android-consumption-ack-v1.json", local, false);
}
if (status == TRAINLOG_STATUS_OK) {
status = pull_named_file(
io, &selection, selection.root, "android-generation-error-v1.json", local, false);
}
if (status == TRAINLOG_STATUS_OK &&
referenced_android_generation(local, generation, sizeof(generation))) {
status = pull_referenced_generation(io, &selection, local, generation);
}
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;
}
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);
}
return status;
}
static const TrainlogGenerationMtpIo production = {
.devices = trainlog_usb_list_mtp_devices,
.storages = trainlog_mtp_list_storages,
.children = trainlog_mtp_list_folder,
.ensure_folder = trainlog_mtp_ensure_folder,
.receive = trainlog_mtp_receive_file,
.send = trainlog_mtp_send_text_file,
.remove = trainlog_mtp_delete_object,
.rename = trainlog_mtp_rename_object,
};
const TrainlogGenerationMtpIo *trainlog_generation_mtp_production_io(void) {
return &production;
}