receive_files() in src/server/server.c was a non-static, unprototyped, zero-caller duplicate of receiver_process()/receiver_receive_files() in src/server/receiver.c. Delete it along with the includes it uniquely pulled (chunk.h, metadata.h, sys/stat.h, duplicate quoted unistd.h); remaining code still uses file.h/config.h/protocol.h (via multiprocessing.h) directly. mkdir_r() in src/shared/utils.c had zero callers in src/ and tests/; remove the function and its declaration in utils.h, plus the unused libgen.h include.
369 lines
10 KiB
C
369 lines
10 KiB
C
#include "utils.h"
|
|
#include "array_list.h"
|
|
#include "log.h"
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
static int authorized_root_fd = -1;
|
|
static char* authorized_root_path;
|
|
|
|
bool utils_set_authorized_root(int fd, const char* canonical_path) {
|
|
char* path_copy = canonical_path ? str_dup(canonical_path) : NULL;
|
|
if (canonical_path && !path_copy) {
|
|
authorized_root_fd = -1;
|
|
free(authorized_root_path);
|
|
authorized_root_path = NULL;
|
|
return false;
|
|
}
|
|
authorized_root_fd = fd;
|
|
free(authorized_root_path);
|
|
authorized_root_path = path_copy;
|
|
return true;
|
|
}
|
|
|
|
void utils_set_authorized_root_fd(int fd) {
|
|
(void)utils_set_authorized_root(fd, NULL);
|
|
}
|
|
|
|
static bool path_is_within_root(const char* root, const char* path) {
|
|
size_t root_len = strlen(root);
|
|
return strncmp(root, path, root_len) == 0 && (path[root_len] == '\0' || path[root_len] == '/');
|
|
}
|
|
|
|
static int open_authorized_destination(const char* dest_root) {
|
|
if (authorized_root_fd < 0 || !authorized_root_path || !dest_root ||
|
|
!path_is_within_root(authorized_root_path, dest_root))
|
|
return -1;
|
|
|
|
int dirfd = dup(authorized_root_fd);
|
|
if (dirfd < 0)
|
|
return -1;
|
|
|
|
const char* relative_path = dest_root + strlen(authorized_root_path);
|
|
while (*relative_path == '/')
|
|
relative_path++;
|
|
char* relative = str_dup(*relative_path ? relative_path : ".");
|
|
if (!relative) {
|
|
close(dirfd);
|
|
return -1;
|
|
}
|
|
|
|
char* saveptr = NULL;
|
|
char* component = strtok_r(relative, "/", &saveptr);
|
|
while (component) {
|
|
if (strcmp(component, "..") == 0) {
|
|
free(relative);
|
|
close(dirfd);
|
|
return -1;
|
|
}
|
|
if (strcmp(component, ".") == 0) {
|
|
component = strtok_r(NULL, "/", &saveptr);
|
|
continue;
|
|
}
|
|
int next = openat(dirfd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
if (next < 0) {
|
|
free(relative);
|
|
close(dirfd);
|
|
return -1;
|
|
}
|
|
close(dirfd);
|
|
dirfd = next;
|
|
component = strtok_r(NULL, "/", &saveptr);
|
|
}
|
|
|
|
free(relative);
|
|
return dirfd;
|
|
}
|
|
|
|
char* str_dup(const char* string) {
|
|
if (string == NULL)
|
|
return NULL;
|
|
size_t str_len = strlen(string);
|
|
char* new_string = (char*)malloc(str_len + 1);
|
|
if (new_string == NULL)
|
|
return NULL;
|
|
memcpy(new_string, string, str_len + 1);
|
|
return new_string;
|
|
}
|
|
|
|
char* output_escape(const char* string, bool eight_bit_output) {
|
|
if (!string)
|
|
return NULL;
|
|
size_t length = strlen(string);
|
|
if (length > (SIZE_MAX - 1) / 5)
|
|
return NULL;
|
|
char* escaped = malloc(length * 5 + 1);
|
|
if (!escaped)
|
|
return NULL;
|
|
size_t out = 0;
|
|
for (size_t i = 0; i < length; i++) {
|
|
unsigned char byte = (unsigned char)string[i];
|
|
if ((byte >= 32 && byte <= 126) || (eight_bit_output && byte >= 128)) {
|
|
escaped[out++] = (char)byte;
|
|
} else {
|
|
escaped[out++] = '\\';
|
|
escaped[out++] = '#';
|
|
escaped[out++] = (char)('0' + ((byte >> 6) & 7));
|
|
escaped[out++] = (char)('0' + ((byte >> 3) & 7));
|
|
escaped[out++] = (char)('0' + (byte & 7));
|
|
}
|
|
}
|
|
escaped[out] = '\0';
|
|
return escaped;
|
|
}
|
|
|
|
/* Match a glob pattern against a string. Supported wildcards:
|
|
* ? matches any single character except '/'.
|
|
* * matches any sequence of characters within one path component (no '/').
|
|
* ** matches any sequence of characters, including '/' (cross-directory).
|
|
* slash-star-star-slash is treated as a cross-directory wildcard when it appears between
|
|
* literals.
|
|
*/
|
|
bool glob_match(const char* pattern, const char* str) {
|
|
while (*pattern) {
|
|
if (*pattern == '*') {
|
|
if (*(pattern + 1) == '*') {
|
|
/* globstar: match across directories */
|
|
pattern += 2;
|
|
if (*pattern == '\0')
|
|
return true;
|
|
if (*pattern == '/')
|
|
pattern++;
|
|
while (*str) {
|
|
if (glob_match(pattern, str))
|
|
return true;
|
|
str++;
|
|
}
|
|
return glob_match(pattern, str);
|
|
}
|
|
/* single *: match within one path component */
|
|
pattern++;
|
|
while (*str && *str != '/') {
|
|
if (glob_match(pattern, str))
|
|
return true;
|
|
str++;
|
|
}
|
|
return glob_match(pattern, str);
|
|
} else if (*pattern == '?') {
|
|
if (!*str || *str == '/')
|
|
return false;
|
|
pattern++;
|
|
str++;
|
|
} else {
|
|
if (*pattern != *str) {
|
|
/* allow literal / ** / rest to match any number of directories */
|
|
if (*pattern == '/' && *(pattern + 1) == '*' && *(pattern + 2) == '*') {
|
|
const char* rest = pattern + 3;
|
|
if (*rest == '/')
|
|
rest++;
|
|
return glob_match(rest, str);
|
|
}
|
|
return false;
|
|
}
|
|
pattern++;
|
|
str++;
|
|
}
|
|
}
|
|
return *str == '\0';
|
|
}
|
|
|
|
bool format_human_bytes(unsigned long long bytes, char* buffer, size_t buffer_size) {
|
|
static const char* const units[] = {"B", "KB", "MB", "GB", "TB", "PB", "EB"};
|
|
double value = (double)bytes;
|
|
size_t unit = 0;
|
|
int written;
|
|
|
|
if (!buffer || buffer_size == 0)
|
|
return false;
|
|
while (value >= 1024.0 && unit < sizeof(units) / sizeof(units[0]) - 1) {
|
|
value /= 1024.0;
|
|
unit++;
|
|
}
|
|
if (unit == 0)
|
|
written = snprintf(buffer, buffer_size, "%llu %s", bytes, units[unit]);
|
|
else
|
|
written = snprintf(buffer, buffer_size, "%.1f %s", value, units[unit]);
|
|
return written >= 0 && (size_t)written < buffer_size;
|
|
}
|
|
|
|
static bool is_dir_in_manifest(const char* rel_path, ArrayList* manifest) {
|
|
size_t len = strlen(rel_path);
|
|
for (int i = 0; i < manifest->size; i++) {
|
|
const char* entry = (const char*)manifest->items[i];
|
|
// Check if entry starts with rel_path + '/' or matches exactly
|
|
if (strncmp(entry, rel_path, len) == 0 && (entry[len] == '/' || entry[len] == '\0'))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifest,
|
|
size_t max_delete, size_t* deleted_count) {
|
|
int scanfd = dup(dirfd);
|
|
if (scanfd < 0)
|
|
return false;
|
|
DIR* dir = fdopendir(scanfd);
|
|
if (!dir) {
|
|
close(scanfd);
|
|
return false;
|
|
}
|
|
bool operation_ok = true;
|
|
const struct dirent* entry;
|
|
while ((entry = readdir(dir)) != NULL) {
|
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
|
continue;
|
|
char* child_rel = path_cat((char*)rel_path, entry->d_name);
|
|
if (!child_rel) {
|
|
operation_ok = false;
|
|
continue;
|
|
}
|
|
struct stat st;
|
|
if (fstatat(dirfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) {
|
|
if (errno != ENOENT)
|
|
operation_ok = false;
|
|
free(child_rel);
|
|
continue;
|
|
}
|
|
// Skip symlinks to prevent following them outside the destination tree
|
|
if (S_ISLNK(st.st_mode)) {
|
|
free(child_rel);
|
|
continue;
|
|
}
|
|
if (S_ISDIR(st.st_mode)) {
|
|
int childfd = openat(dirfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
bool child_removed = false;
|
|
if (childfd >= 0) {
|
|
child_removed = delete_extras_fd(childfd, child_rel, manifest, max_delete, deleted_count);
|
|
if (!child_removed)
|
|
operation_ok = false;
|
|
close(childfd);
|
|
} else if (errno != ENOENT) {
|
|
operation_ok = false;
|
|
}
|
|
if (child_removed && !is_dir_in_manifest(child_rel, manifest)) {
|
|
if (*deleted_count >= max_delete) {
|
|
operation_ok = false;
|
|
} else {
|
|
if (unlinkat(dirfd, entry->d_name, AT_REMOVEDIR) != 0) {
|
|
if (errno != ENOENT)
|
|
operation_ok = false;
|
|
} else {
|
|
(*deleted_count)++;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Check if relative path is in manifest
|
|
bool found = false;
|
|
for (int i = 0; i < manifest->size; i++) {
|
|
if (strcmp((char*)manifest->items[i], child_rel) == 0) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
if (*deleted_count >= max_delete) {
|
|
operation_ok = false;
|
|
free(child_rel);
|
|
continue;
|
|
}
|
|
if (unlinkat(dirfd, entry->d_name, 0) != 0) {
|
|
if (errno != ENOENT)
|
|
operation_ok = false;
|
|
} else {
|
|
(*deleted_count)++;
|
|
}
|
|
char* escaped_path = output_escape(child_rel, log_get_8_bit_output());
|
|
fprintf(stderr, " Deleted: %s\n", escaped_path ? escaped_path : "<allocation failed>");
|
|
free(escaped_path);
|
|
}
|
|
}
|
|
free(child_rel);
|
|
}
|
|
closedir(dir);
|
|
return operation_ok;
|
|
}
|
|
|
|
bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete) {
|
|
if (!manifest)
|
|
return false;
|
|
int rootfd;
|
|
if (authorized_root_fd >= 0) {
|
|
if (authorized_root_path)
|
|
rootfd = open_authorized_destination(dest_root);
|
|
else if (dest_root == NULL)
|
|
rootfd = dup(authorized_root_fd);
|
|
else
|
|
rootfd = -1;
|
|
} else {
|
|
rootfd = open(dest_root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
}
|
|
if (rootfd < 0)
|
|
return false;
|
|
size_t deleted_count = 0;
|
|
bool ok = delete_extras_fd(rootfd, "", manifest, max_delete, &deleted_count);
|
|
if (close(rootfd) != 0)
|
|
ok = false;
|
|
return ok;
|
|
}
|
|
|
|
bool delete_extras(const char* dest_root, ArrayList* manifest) {
|
|
return delete_extras_limited(dest_root, manifest, SIZE_MAX);
|
|
}
|
|
|
|
bool has_path_traversal(const char* path) {
|
|
if (!path)
|
|
return true;
|
|
char* dup = str_dup(path);
|
|
if (!dup)
|
|
return true;
|
|
char* saveptr;
|
|
const char* part = strtok_r(dup, "/", &saveptr);
|
|
while (part) {
|
|
if (strcmp(part, "..") == 0) {
|
|
free(dup);
|
|
return true;
|
|
}
|
|
part = strtok_r(NULL, "/", &saveptr);
|
|
}
|
|
free(dup);
|
|
return false;
|
|
}
|
|
|
|
bool utils_valid_batch_path(const char* path) {
|
|
return path && path[0] != '\0' && path[0] != '/' && !has_path_traversal(path);
|
|
}
|
|
|
|
char* path_cat(const char* path1, const char* path2) {
|
|
if (path1 == NULL || *path1 == '\0')
|
|
return str_dup(path2);
|
|
if (path2 == NULL || *path2 == '\0')
|
|
return str_dup(path1);
|
|
size_t path1_len = strlen(path1);
|
|
size_t path2_len = strlen(path2);
|
|
size_t offset = 0;
|
|
if (path1[path1_len - 1] == '/')
|
|
path1_len -= 1;
|
|
if (path2[0] == '/') {
|
|
offset = 1;
|
|
path2_len -= 1;
|
|
}
|
|
if (path1_len > SIZE_MAX - path2_len - 2)
|
|
return NULL;
|
|
char* new_path = malloc(path1_len + path2_len + 2);
|
|
if (new_path == NULL)
|
|
return NULL;
|
|
memcpy(new_path, path1, path1_len);
|
|
new_path[path1_len] = '/';
|
|
memcpy(new_path + path1_len + 1, path2 + offset, path2_len);
|
|
new_path[path1_len + path2_len + 1] = '\0';
|
|
return new_path;
|
|
}
|