common : normalize path to treat root separator coherently in validation

This commit is contained in:
Jan Boon 2026-02-10 05:46:41 +00:00
parent 09879b214e
commit 42dd51df99
4 changed files with 75 additions and 0 deletions

View File

@ -692,6 +692,49 @@ bool string_parse_kv_override(const char * data, std::vector<llama_model_kv_over
// Filesystem utils
//
// Normalizes a relative filepath
// - Replaces backslashes and forward slashes with the system path separator
// - Trims leading './' or '.\' segments
// - Trims leading '/' or '\' (treat 'root' as relative)
// - Trims duplicate directory separators
// - Does not resolve '..' segments
// - Does not ensure the path is valid or safe
// Use in conjunction with `fs_validate_filename`, calling `fs_validate_filename` after `fs_normalize_filepath`
std::string fs_normalize_filepath(const std::string & path) {
std::string result;
result.reserve(path.size());
bool leading = true;
char prev = 0;
for (size_t i = 0; i < path.size(); ++i) {
char c = path[i];
if (c == '/' || c == '\\') {
c = DIRECTORY_SEPARATOR;
}
if (leading) {
if (c == DIRECTORY_SEPARATOR) {
continue; // Skip leading separators
} else if (c == '.') {
if (i + 1 < path.size()) {
char next = path[i + 1];
if (next == '/' || next == '\\') {
++i;
continue; // Skip leading dot segments
}
}
}
leading = false;
}
if (prev == DIRECTORY_SEPARATOR && c == DIRECTORY_SEPARATOR) {
continue; // Skip duplicate separators
}
prev = c;
result += c;
}
return result;
}
// Validate if a filename or path is safe to use
bool fs_validate_filename(const std::string & filename, bool allow_subdirs) {
if (!filename.length()) {

View File

@ -707,6 +707,7 @@ std::string string_from(const struct llama_context * ctx, const struct llama_bat
// Filesystem utils
//
std::string fs_normalize_filepath(const std::string & path);
bool fs_validate_filename(const std::string & filename, bool allow_subdirs = false);
bool fs_create_directory_with_parents(const std::string & path);
bool fs_is_directory(const std::string & path);

View File

@ -9,6 +9,17 @@
static int n_tests = 0;
static int n_failed = 0;
static const char SEP = DIRECTORY_SEPARATOR;
static void test_normalize(const char * desc, const std::string & expected, const std::string & input) {
std::string result = fs_normalize_filepath(input);
n_tests++;
if (result != expected) {
n_failed++;
printf(" FAIL: %s (got \"%s\", expected \"%s\")\n", desc, result.c_str(), expected.c_str());
}
}
static void test(const char * desc, bool expected, const std::string & filename, bool allow_subdirs = false) {
bool result = fs_validate_filename(filename, allow_subdirs);
n_tests++;
@ -40,6 +51,7 @@ int main(void) {
test("leading space", false, " foo");
test("trailing space", false, "foo ");
test("trailing dot", false, "foo.");
test("dot path", false, "./././");
// --- Double dots ---
test("contains double dot", true, "foo..bar");
@ -104,6 +116,7 @@ int main(void) {
test("trailing slash", true, "foo/bar/", true);
test("colon in path", false, "foo/b:r/baz", true);
test("control char in path", false, "foo/b\nar/baz", true);
test("dot path", false, "./././", true);
// --- Leading separators ---
test("leading slash", false, "/foo/bar", true);
@ -125,6 +138,23 @@ int main(void) {
test("trailing space before slash", false, "bar /baz", true);
test("trailing dot before slash", false, "bar./baz", true);
// --- fs_normalize_filepath ---
test_normalize("passthrough simple", "foo.txt", "foo.txt");
test_normalize("passthrough subdir", std::string("foo") + SEP + "bar.txt", "foo/bar.txt");
test_normalize("backslash to sep", std::string("foo") + SEP + "bar.txt", "foo\\bar.txt");
test_normalize("mixed separators", std::string("a") + SEP + "b" + SEP + "c", "a/b\\c");
test_normalize("duplicate slashes", std::string("foo") + SEP + "bar", "foo//bar");
test_normalize("duplicate backslashes", std::string("foo") + SEP + "bar", "foo\\\\bar");
test_normalize("triple slashes", std::string("foo") + SEP + "bar", "foo///bar");
test_normalize("leading slash stripped", "foo", "/foo");
test_normalize("leading backslash stripped", "foo", "\\foo");
test_normalize("multiple leading slashes", "foo", "///foo");
test_normalize("leading dot-slash stripped", "foo", "./foo");
test_normalize("leading dot-backslash stripped", "foo", ".\\foo");
test_normalize("deep path normalized",
std::string("a") + SEP + "b" + SEP + "c" + SEP + "d.txt",
"/a//b\\c/d.txt");
if (n_failed) {
printf("\n%d/%d tests failed\n", n_failed, n_tests);
fflush(stdout);

View File

@ -798,6 +798,7 @@ static void handle_media(
}
// load local image file
std::string file_path = url.substr(7); // remove "file://"
file_path = fs_normalize_filepath(file_path); // remove any leading './' and normalize separators
raw_buffer data;
if (!fs_validate_filename(file_path, true)) {
throw std::invalid_argument("file path is not allowed: " + file_path);