673 lines
27 KiB
C
673 lines
27 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) {
|
|
/* WHY: the Android request remains the explicit user/foreground
|
|
* admission signal even when the conversation uses full generations.
|
|
* CONTRACT: copy the bounded signal only; the orchestrator, not this
|
|
* transport adapter, owns correlation and all domain mutations.
|
|
* INVARIANT: reading the request never acknowledges or removes it. */
|
|
status = pull_named_file(
|
|
io, &selection, selection.root, "trainlog-sync-request-v1.json", local, false);
|
|
}
|
|
if (status == TRAINLOG_STATUS_OK) {
|
|
/* WHY: strict legacy V3 may be unavailable while a complete causal
|
|
* generation remains valid. CONTRACT: pull the dedicated bounded
|
|
* coordination object without scanning the exchange tree. INVARIANT:
|
|
* this read never acknowledges, deletes, or rewrites remote state. */
|
|
status = pull_named_file(io,
|
|
&selection,
|
|
selection.root,
|
|
"trainlog-sync-full-generation-request-v1.json",
|
|
local,
|
|
false);
|
|
}
|
|
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;
|
|
}
|