gout

A static git page generator
git clone https://git.bracken.jp/gout.git
Log | Files | Refs | README | LICENSE

commit d3db909b464737863e9b54ae7c28393f0871aed4
parent c4edd894077a09547707f9ff76e4e188b361354b
Author: Chris Bracken <chris@bracken.jp>
Date:   Fri, 20 Feb 2026 18:08:46 +0900

utils: improve relpath_from_dir

Diffstat:
Msrc/utils.c | 33+++++++++++++++++++++++++--------
Msrc/utils_tests.c | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
2 files changed, 84 insertions(+), 27 deletions(-)

diff --git a/src/utils.c b/src/utils.c @@ -120,22 +120,39 @@ bool is_safe_mailto(const char* email) { } char* relpath_from_dir(const char* dir) { - size_t num_components = 1; - if (strcmp(dir, ".") != 0) { - for (const char* p = dir; *p != '\0'; p++) { - if (*p == '/') { - num_components++; - } + if (!dir || dir[0] == '\0' || strcmp(dir, ".") == 0) { + return estrdup(""); + } + + size_t depth = 0; + const char* p = dir; + /* Skip leading slashes. */ + while (*p == '/') { + p++; + } + + /* For each component, increment depth. Skip repeated slashes. */ + while (*p != '\0') { + depth++; + while (*p != '\0' && *p != '/') { + p++; + } + while (*p == '/') { + p++; } } + if (depth == 0) { + return estrdup(""); + } + /* Output the appropriate number of ../ for the depth. */ const char segment[] = "../"; const size_t segment_len = sizeof(segment) - 1; - size_t relpath_size = (num_components * segment_len) + 1; + size_t relpath_size = (depth * segment_len) + 1; char* relpath = ecalloc(relpath_size, 1); char* ptr = relpath; - for (size_t i = 0; i < num_components; i++) { + for (size_t i = 0; i < depth; i++) { memcpy(ptr, segment, segment_len); ptr += segment_len; } diff --git a/src/utils_tests.c b/src/utils_tests.c @@ -70,32 +70,72 @@ UTEST(is_safe_mailto, RejectsInvalidCharacters) { EXPECT_FALSE(is_safe_mailto("test@example.com\n")); } -UTEST(relpath_from_dir, SingleDirectory) { - char* result = relpath_from_dir("folder"); - ASSERT_STREQ("../", result); - free(result); +UTEST(relpath_from_dir, EmptyOrNull) { + char* r1 = relpath_from_dir(""); + EXPECT_STREQ("", r1); + free(r1); + + char* r2 = relpath_from_dir(NULL); + EXPECT_STREQ("", r2); + free(r2); + + char* r3 = relpath_from_dir("."); + EXPECT_STREQ("", r3); + free(r3); } -UTEST(relpath_from_dir, MultipleDirectories) { - char* result = relpath_from_dir("path/to/somewhere"); - ASSERT_STREQ("../../../", result); - free(result); +UTEST(relpath_from_dir, SingleDirectory) { + char* r1 = relpath_from_dir("foo"); + EXPECT_STREQ("../", r1); + free(r1); + + char* r2 = relpath_from_dir("foo/"); + EXPECT_STREQ("../", r2); + free(r2); } -UTEST(relpath_from_dir, CurrentDirectoryLiteral) { - char* result = relpath_from_dir("."); - ASSERT_STREQ("../", result); - free(result); +UTEST(relpath_from_dir, LeadingSlash) { + char* r2 = relpath_from_dir("/foo"); + EXPECT_STREQ("../", r2); + free(r2); } UTEST(relpath_from_dir, TrailingSlash) { - char* result = relpath_from_dir("folder/"); - ASSERT_STREQ("../../", result); - free(result); + char* r2 = relpath_from_dir("foo/"); + EXPECT_STREQ("../", r2); + free(r2); } -UTEST(relpath_from_dir, RootPath) { - char* result = relpath_from_dir("/"); - ASSERT_STREQ("../../", result); - free(result); +UTEST(relpath_from_dir, NestedDirectories) { + char* r1 = relpath_from_dir("foo/bar"); + EXPECT_STREQ("../../", r1); + free(r1); + + char* r2 = relpath_from_dir("foo/bar/baz"); + EXPECT_STREQ("../../../", r2); + free(r2); + + char* r3 = relpath_from_dir("a/b/c/d/e"); + EXPECT_STREQ("../../../../../", r3); + free(r3); +} + +UTEST(relpath_from_dir, MultipleSlashes) { + char* r1 = relpath_from_dir("foo//bar"); + EXPECT_STREQ("../../", r1); + free(r1); + + char* r2 = relpath_from_dir("//foo///bar//"); + EXPECT_STREQ("../../", r2); + free(r2); +} + +UTEST(relpath_from_dir, AbsolutePath) { + char* r1 = relpath_from_dir("/foo"); + EXPECT_STREQ("../", r1); + free(r1); + + char* r2 = relpath_from_dir("/"); + EXPECT_STREQ("", r2); + free(r2); }