From: "Omar Polo" Subject: got_path_strip_trailing_slashes: change to preserve the first "/" To: gameoftrees@openbsd.org Date: Sat, 18 Jul 2026 22:25:57 +0200 otherwise a path made of only "/" becomes an empty string, which is not desiderable since we're managing paths. Adjust the callers to cope with the behaviour change. ok? diff 554d01aac8a3901d8a7f0ac67b88392d1fd2a71d c3f50630385142d9b8e1f024294037c21ea4ac5c commit - 554d01aac8a3901d8a7f0ac67b88392d1fd2a71d commit + c3f50630385142d9b8e1f024294037c21ea4ac5c blob - f367e5a43d5c219b2524ae00cf0508b70c89693a blob + 86375a5ba981252f7373c914687eb34e0fe9822e --- gotd/parse.y +++ gotd/parse.y @@ -1370,11 +1370,17 @@ conf_protect_ref_namespace(char **new, struct got_path *new = NULL; got_path_strip_trailing_slashes(namespace); if (!refname_is_valid(namespace)) return -1; - if (asprintf(&s, "%s/", namespace) == -1) { + if (got_path_is_root_dir(namespace)) { + s = strdup(namespace); + if (s == NULL) { + yyerror("strdup: %s", strerror(errno)); + return -1; + } + } else if (asprintf(&s, "%s/", namespace) == -1) { yyerror("asprintf: %s", strerror(errno)); return -1; } error = got_pathlist_insert(&pe, refs, s, NULL); @@ -1522,11 +1528,17 @@ conf_notify_ref_namespace(struct gotd_repo *repo, char got_path_strip_trailing_slashes(namespace); if (!refname_is_valid(namespace)) return -1; - if (asprintf(&s, "%s/", namespace) == -1) { + if (got_path_is_root_dir(namespace)) { + s = strdup(namespace); + if (s == NULL) { + yyerror("strdup: %s", strerror(errno)); + return -1; + } + } else if (asprintf(&s, "%s/", namespace) == -1) { yyerror("asprintf: %s", strerror(errno)); return -1; } error = got_pathlist_insert(&pe, &repo->notification_ref_namespaces, blob - a93897378b3c9c123c2da0f01702fb86d2481274 blob + eaed62bb5e478983164ee2a6b5cf893e4e258435 --- gotsys/parse.y +++ gotsys/parse.y @@ -1063,13 +1063,11 @@ webopts1 : DISABLE AUTHENTICATION { "an empty string"); free($4); YYERROR; } - if (!got_path_is_root_dir($4)) - got_path_strip_trailing_slashes($4); - + got_path_strip_trailing_slashes($4); if (!got_path_is_absolute($4)) { int ret; ret = snprintf(srv->repos_url_path, sizeof(srv->repos_url_path), @@ -1828,11 +1826,17 @@ conf_protect_ref_namespace(char **new, struct got_path *new = NULL; got_path_strip_trailing_slashes(namespace); if (!refname_is_valid(namespace)) return -1; - if (asprintf(&s, "%s/", namespace) == -1) { + if (got_path_is_root_dir(namespace)) { + s = strdup(namespace); + if (s == NULL) { + yyerror("strdup: %s", strerror(errno)); + return -1; + } + } else if (asprintf(&s, "%s/", namespace) == -1) { yyerror("asprintf: %s", strerror(errno)); return -1; } error = got_pathlist_insert(&pe, refs, s, NULL); @@ -1979,12 +1983,17 @@ conf_notify_ref_namespace(struct gotsys_repo *repo, ch char *s; got_path_strip_trailing_slashes(namespace); if (!refname_is_valid(namespace)) return -1; - - if (asprintf(&s, "%s/", namespace) == -1) { + if (got_path_is_root_dir(namespace)) { + s = strdup(namespace); + if (s == NULL) { + yyerror("strdup: %s", strerror(errno)); + return -1; + } + } else if (asprintf(&s, "%s/", namespace) == -1) { yyerror("asprintf: %s", strerror(errno)); return -1; } error = got_pathlist_insert(&pe, &repo->notification_ref_namespaces, blob - 361d77c7427fc4a3d7cf21474edb09a17c84f5ed blob + dc9d8ca2826bc77b85db2cff43d7a50c8dfdbc3f --- gotwebd/gotweb.c +++ gotwebd/gotweb.c @@ -539,10 +539,11 @@ gotweb_serve_website(struct request *c, struct website for (i = 0; i < nentries; i++) { struct gotweb_url url = { .index_page = -1, .action = -1, }; + const char *index = "/index.html"; te = got_object_tree_get_entry(tree, i); if (te == NULL) { char *id_str; @@ -562,11 +563,14 @@ gotweb_serve_website(struct request *c, struct website strcasecmp(name, "index.html") != 0) continue; /* XXX gotweb_reply uses request struct field */ got_path_strip_trailing_slashes(p->document_uri); - if (strlcat(p->document_uri, "/index.html", + if (got_path_is_root_dir(p->document_uri)) + index++; /* skip initial "/" */ + + if (strlcat(p->document_uri, index, sizeof(p->document_uri)) >= sizeof(p->document_uri)) { error = got_error(GOT_ERR_NO_SPACE); goto done; } @@ -696,13 +700,11 @@ gotweb_route_request(int *is_repository_request, struc error = got_error_from_errno("strdup"); goto done; } } - if (!got_path_is_root_dir(*request_path)) - got_path_strip_trailing_slashes(*request_path); - + got_path_strip_trailing_slashes(*request_path); *site = gotweb_get_website(srv, *request_path); /* * If the target of the request is ambiguous, the longer path wins. */ blob - ab1b6288020128df255679667f47261e6eed76a0 blob + b618203af4224f676c92168f6e40fd0e17977773 --- gotwebd/pages.tmpl +++ gotwebd/pages.tmpl @@ -186,11 +186,12 @@ nextsep(char *s, char **t) while (url_path[0] == '/') url_path++; if (url_path[0] != '\0') { if (url_root[0] != '\0') { got_path_strip_trailing_slashes(prefix); - strlcat(prefix, "/", sizeof(prefix)); + if (!got_path_is_root_dir(prefix)) + strlcat(prefix, "/", sizeof(prefix)); } strlcat(prefix, url_path, sizeof(prefix)); } got_path_strip_trailing_slashes(prefix); @@ -1490,11 +1491,12 @@ date: {{ datebuf }} {{ " UTC" }} {{ "\n" }} while (url_path[0] == '/') url_path++; if (url_path[0] != '\0') { if (url_root[0] != '\0') { got_path_strip_trailing_slashes(prefix); - strlcat(prefix, "/", sizeof(prefix)); + if (!got_path_is_root_dir(prefix)) + strlcat(prefix, "/", sizeof(prefix)); } strlcat(prefix, url_path, sizeof(prefix)); } got_path_strip_trailing_slashes(prefix); blob - e356cea29b105402010bc95aa09710adfd5c96ef blob + 6e931ee077053256fdea2e1ba51a4d329d71f30c --- gotwebd/parse.y +++ gotwebd/parse.y @@ -427,13 +427,11 @@ main : PREFORK NUMBER { " string"); free($2); YYERROR; } - if (!got_path_is_root_dir($2)) - got_path_strip_trailing_slashes($2); - + got_path_strip_trailing_slashes($2); n = strlcpy(gotwebd->gotweb_url_root, $2, sizeof(gotwebd->gotweb_url_root)); if (n >= sizeof(gotwebd->gotweb_url_root)) { yyerror("gotweb_url_root too long, exceeds " "%zd bytes: %s", @@ -458,13 +456,11 @@ main : PREFORK NUMBER { " string"); free($2); YYERROR; } - if (!got_path_is_root_dir($2)) - got_path_strip_trailing_slashes($2); - + got_path_strip_trailing_slashes($2); n = strlcpy(gotwebd->repos_url_path, $2, sizeof(gotwebd->repos_url_path)); if (n >= sizeof(gotwebd->repos_url_path)) { yyerror("repos_url_path too long, exceeds " "%zd bytes: %s", @@ -809,13 +805,11 @@ serveropts1 : REPOS_PATH STRING { " string"); free($2); YYERROR; } - if (!got_path_is_root_dir($2)) - got_path_strip_trailing_slashes($2); - + got_path_strip_trailing_slashes($2); n = strlcpy(new_srv->gotweb_url_root, $2, sizeof(new_srv->gotweb_url_root)); if (n >= sizeof(new_srv->gotweb_url_root)) { yyerror("gotweb_url_root too long, exceeds " "%zd bytes: %s", @@ -839,13 +833,11 @@ serveropts1 : REPOS_PATH STRING { " string"); free($2); YYERROR; } - if (!got_path_is_root_dir($2)) - got_path_strip_trailing_slashes($2); - + got_path_strip_trailing_slashes($2); n = strlcpy(new_srv->repos_url_path, $2, sizeof(new_srv->repos_url_path)); if (n >= sizeof(new_srv->repos_url_path)) { yyerror("repos_url_path too long, exceeds " "%zd bytes: %s", @@ -2010,15 +2002,11 @@ parse_config(const char *filename, struct gotwebd *env sizeof(srv->full_repos_url_path) - 1); return -1; } } - if (!got_path_is_root_dir(srv->full_repos_url_path)) { - got_path_strip_trailing_slashes( - srv->full_repos_url_path); - } - + got_path_strip_trailing_slashes(srv->full_repos_url_path); RB_FOREACH(pe, got_pathlist_head, &srv->websites) { const char *url_path = pe->path; struct website *site = pe->data; if (site->auth_config == 0) blob - c52de692cb3df4f92464143bd3f8bdd3a32ebc90 blob + ccd4ca57ebc3da3041d26be979d2339e3fb5cf9d --- lib/dial.c +++ lib/dial.c @@ -188,12 +188,13 @@ got_dial_parse_uri(char **proto, char **host, char **p *server_path = strdup(p); if (*server_path == NULL) { err = got_error_from_errno("strdup"); goto done; } + got_path_strip_trailing_slashes(*server_path); - if ((*server_path)[0] == '\0') { + if (got_path_is_root_dir(*server_path)) { err = got_error(GOT_ERR_PARSE_URI); goto done; } err = got_path_basename(repo_name, *server_path); blob - b43f45fcda4659959a1775f2a328234862001f60 blob + 1be262eb423fe458698d66819eb694c1d1eef476 --- lib/path.c +++ lib/path.c @@ -429,11 +429,13 @@ void got_path_strip_trailing_slashes(char *path) { size_t x; x = strlen(path); - while (x-- > 0 && path[x] == '/') + + /* if the input is "/" don't trim it */ + while (x-- > 1 && path[x] == '/') path[x] = '\0'; } /* based on findprog() from usr.bin/which/which.c */ const struct got_error * blob - fca04b3b62b7ecd7035e827c334fa35ad977b7a3 blob + 1a6c5d63489f0fc3240c2601bd4a04ce00ad43ad --- lib/worktree.c +++ lib/worktree.c @@ -183,13 +183,13 @@ got_worktree_init(const char *path, struct got_referen } else { absprefix = strdup(prefix); if (absprefix == NULL) return got_error_from_errno("strdup"); } - if (!got_path_is_root_dir(absprefix)) - got_path_strip_trailing_slashes(absprefix); + got_path_strip_trailing_slashes(absprefix); + /* Create top-level directory (may already exist). */ if (mkdir(path, GOT_DEFAULT_DIR_MODE) == -1 && errno != EEXIST) { err = got_error_from_errno2("mkdir", path); goto done; } blob - 9c87f1cb18b976ef796bcbe79abffd540c2e80a8 blob + 5406ccec12650b57a31461290e66973c8fda4b86 --- lib/worktree_open.c +++ lib/worktree_open.c @@ -193,13 +193,13 @@ open_worktree(struct got_worktree **worktree, const ch err = read_meta_file(&(*worktree)->path_prefix, path_meta, GOT_WORKTREE_PATH_PREFIX); if (err) goto done; - if (!got_path_is_root_dir((*worktree)->path_prefix)) - got_path_strip_trailing_slashes((*worktree)->path_prefix); + got_path_strip_trailing_slashes((*worktree)->path_prefix); + err = read_meta_file(&base_commit_id_str, path_meta, GOT_WORKTREE_BASE_COMMIT); if (err) goto done; blob - c4da368ef9fe5fb3b4c6ff3a0879637e0a4a2611 blob + c3febf63fe7daf00c838081bf3ab693ce816f381 --- regress/path/path_test.c +++ regress/path/path_test.c @@ -21,10 +21,11 @@ #include #include #include #include #include +#include #include "got_error.h" #include "got_path.h" #ifndef nitems @@ -97,10 +98,43 @@ path_cmp(void) } return 1; } +static int +path_strip_trailing_slashes(void) +{ + const struct path_cmp_test { + const char *input; + const char *expected; + } test_data[] = { + { "", "" }, + { "/", "/" }, + { "///", "/" }, + { "/a", "/a" }, + { "/a/", "/a" }, + { "/a//", "/a" }, + }; + size_t i; + + for (i = 0; i < nitems(test_data); i++) { + char p[PATH_MAX]; + const char *input = test_data[i].input; + const char *expected = test_data[i].expected; + + strlcpy(p, input, sizeof(p)); + got_path_strip_trailing_slashes(p); + if (strcmp(p, expected) != 0) { + test_printf("%d: '%s' -> '%s'; expected '%s'\n", + i, input, p, expected); + return 0; + } + } + + return 1; +} + const char *path_list_input[] = { "", "/", "a", "/b", "/bar", "bar/sub", "/bar/sub", "/bar/", "/bar.c", "/bar/sub/sub2", "/bar.sub.sub2", "/foo", "/foo/sub", "/foo/", "/foo/", "x/a", }; @@ -246,10 +280,11 @@ main(int argc, char *argv[]) } argc -= optind; argv += optind; RUN_TEST(path_cmp(), "path_cmp"); + RUN_TEST(path_strip_trailing_slashes(), "path_strip_trailing_slashes"); RUN_TEST(path_list(), "path_list"); RUN_TEST(path_list_reverse_input(), "path_list_reverse_input"); return failure ? 1 : 0; }