From: Lady Date: Tue, 18 Mar 2025 04:31:32 +0000 (-0400) Subject: Add request parsing and related tests X-Git-Url: https://git.ladys.computer/CGirls/commitdiff_plain/f6f2fd79a596ecedaadd8b605b7de9d8c662151c Add request parsing and related tests This commit adds a function for processing a “path info” string, for example one received through C·G·I (as the `PATH_INFO´ environment variable), into a structure which represents its semantics, `cgirls_req´. It also adds a function for reserializing this structure into a canonical form. The program `cgirls-test-pathinfo´ is used with the existing test infrastructure to ensure that strings are processed correctly. There is a flaw in this design (which I realized after making the original commit, but before writing this updated message), in that an empty identifier string is represented as `..´, which in a URL already has a different, and very normative, meaning of “parent directory”. This flaw will need to be fixed in a later commit. Probably some more tests could be added here; in particular only a few verbs and extensions are being tested right now and ideally they all would be. --- diff --git a/cgirls-test-pathinfo.c b/cgirls-test-pathinfo.c new file mode 100644 index 0000000..160797c --- /dev/null +++ b/cgirls-test-pathinfo.c @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2025 Lady +// SPDX-License-Identifier: GPL-2.0-only + +#include "aa.h" +#include "request.h" + +// Read in lines from standard input, parse them as path info, and then +// reserialize them to standard output. +int cmd_main(int argc, [[maybe_unused]] const char* argv[argc+1]) { + char* lineptr[1] = { nullptr }; + size_t linelen[1] = { 0 }; + while (true) { + ssize_t readlen = getline(lineptr, linelen, stdin); + if (!feof(stdin) && readlen > 0) { + char* line = nullptr; + char* reline = nullptr; + bool empty = false; + if (readlen == 1) { + line = ""; + empty = true; + } else { + line = strndup(lineptr[0], readlen - 1); + if (!line) { + free(lineptr[0]); + fprintf(stderr, "Error: Failed to allocate string for line.\n"); + return EXIT_FAILURE; + } + } + cgirls_req req = cgirls_path2req(line); + if (!empty) { + free(line); + } + line = cgirls_req2path(req); + cgirls_freereq(req); + if (!line) { + free(lineptr[0]); + fprintf(stderr, "Error: Failed to allocate string for path.\n"); + return EXIT_FAILURE; + } + req = cgirls_path2req(line); + reline = cgirls_req2path(req); + cgirls_freereq(req); + if (!reline) { + free(lineptr[0]); + free(line); + fprintf(stderr, "Error: Failed to allocate another string for path.\n"); + return EXIT_FAILURE; + } + if (strcmp(line, reline) != 0) { + free(lineptr[0]); + fprintf(stderr, "Error: Path normalization was not idempotent for path <%s> (got <%s>).\n", line, reline); + free(line); + free(reline); + return EXIT_FAILURE; + } + free(reline); + fprintf(stdout, "%s\n", line); + free(line); + } else if (!feof(stdin)) { + free(lineptr[0]); + fprintf(stderr, "Error: Got an error from trying to read from file.\n"); + return EXIT_FAILURE; + } else if (readlen > 0) { + free(lineptr[0]); + fprintf(stderr, "Error: Final line in file was not blank.\n"); + return EXIT_FAILURE; + } else { + free(lineptr[0]); + return EXIT_SUCCESS; + } + } +} diff --git a/expect/pathinfo/01-emptyproj b/expect/pathinfo/01-emptyproj new file mode 100644 index 0000000..1556201 --- /dev/null +++ b/expect/pathinfo/01-emptyproj @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 + diff --git a/expect/pathinfo/02-blush b/expect/pathinfo/02-blush new file mode 100644 index 0000000..0cd0ae8 --- /dev/null +++ b/expect/pathinfo/02-blush @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 + +p/unknown/../d diff --git a/expect/pathinfo/03-noverb b/expect/pathinfo/03-noverb new file mode 100644 index 0000000..5e7ea13 --- /dev/null +++ b/expect/pathinfo/03-noverb @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/unknown/b..i/j diff --git a/expect/pathinfo/04-badverbwext b/expect/pathinfo/04-badverbwext new file mode 100644 index 0000000..40a59d6 --- /dev/null +++ b/expect/pathinfo/04-badverbwext @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/unknown.txt diff --git a/expect/pathinfo/05-noverbokext b/expect/pathinfo/05-noverbokext new file mode 100644 index 0000000..59372b6 --- /dev/null +++ b/expect/pathinfo/05-noverbokext @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/unknown diff --git a/expect/pathinfo/06-dotid b/expect/pathinfo/06-dotid new file mode 100644 index 0000000..3db7ef0 --- /dev/null +++ b/expect/pathinfo/06-dotid @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/show +p/show/../d diff --git a/expect/pathinfo/07-nobase b/expect/pathinfo/07-nobase new file mode 100644 index 0000000..3bf4cc2 --- /dev/null +++ b/expect/pathinfo/07-nobase @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/show/i diff --git a/expect/pathinfo/08-notarget b/expect/pathinfo/08-notarget new file mode 100644 index 0000000..aa33f15 --- /dev/null +++ b/expect/pathinfo/08-notarget @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/show/b.. diff --git a/expect/pathinfo/09-canonical b/expect/pathinfo/09-canonical new file mode 100644 index 0000000..1872535 --- /dev/null +++ b/expect/pathinfo/09-canonical @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/log.txt/b..i/s diff --git a/expect/pathinfo/10-blushypath b/expect/pathinfo/10-blushypath new file mode 100644 index 0000000..38db5b2 --- /dev/null +++ b/expect/pathinfo/10-blushypath @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/log/b..i/m/n/o diff --git a/request.c b/request.c new file mode 100644 index 0000000..ca58f0b --- /dev/null +++ b/request.c @@ -0,0 +1,375 @@ +// SPDX-FileCopyrightText: 2025 Lady +// SPDX-License-Identifier: GPL-2.0-only + +#include "aa.h" +#include "request.h" + +void cgirls_freereq (cgirls_req req) { + free(req.cgirls_project); + free(req.cgirls_id); + if (req.cgirls_subpath) { + size_t i = 0; + char* c = req.cgirls_subpath[i]; + while (c) { + free(c); + c = req.cgirls_subpath[++i]; + } + free(req.cgirls_subpath); + } + free(req.cgirls_baseid); + free(req.cgirls_status.cgirls_message); +} + +cgirls_req cgirls_path2req(char const*const pathinfo) { + assert(pathinfo != nullptr); + + // Initialize the result. + cgirls_req req = { + .cgirls_action = cgirls_vb_index, + .cgirls_type = cgirls_mediatype_any, + .cgirls_project = nullptr, + .cgirls_id = nullptr, + .cgirls_subpath = nullptr, + .cgirls_baseid = nullptr, + .cgirls_status = { + .cgirls_code = 200, + .cgirls_message = nullptr, + }, + }; + + // `sont´ stores the start of the next term; `eopi´ stores the end of + // the `pathinfo´ string, excluding any extension. + char const* sont = pathinfo; + char const*const eopi = strchr(pathinfo, 0); + + // The portion of the pathinfo which precedes the first slash gives + // the project of the request. If there is no first slash, the + // project extends to the end of the string. An empty string is + // equivalent to having no project. + char const* eopj = strchr(sont, '/'); + if (!eopj) { + eopj = eopi; + } + if (eopj > sont) { + req.cgirls_project = strndup(sont, eopj - sont); + } + if (eopj < eopi) { + sont = eopj + 1; + } else { + sont = eopi; + } + + // The portion of the pathinfo which follows the first slash but + // precedes the second gives the verb of the request. If there is no + // second slash, the verb extends to the end of the string. If the + // verb is not present, or is the empty string, it is treated as + // `"index"´, unless the second slash is present, in which case it is + // treated as `"unknown"´. + // + // Verbs may be suffixed with one of a small number of extensions to + // request a specific type of response. + // + // Only a few verbs are recognized (corresponding to the `cgirls_vb´ + // constants). If a verb is present, but unrecognized, it is assigned + // the special value `cgirls_vb_unknown´, which should generally be + // interpreted as an error. + char const* eovb = strchr(sont, '/'); + if (!eovb) { + eovb = eopi; + } + char const*const eove = eovb; + char* verb = nullptr; + if (eovb - sont > 4) { + // If the verb is at least 5 characters, extract the extension if + // present (it will be the last 4), and then set the end of the + // verb to the start of the extension. + char const* exts = eovb - 4; + do { + // This “loop” encapsulates extension checking for readability. + // If an extension matches, `eovb´ is re·assigned to point to the + // beginning of the extension. Otherwise, the loop exits early + // and `eovb´ keeps pointing at the end of the string. + if (strncmp(exts, ".txt", 4) == 0) { + req.cgirls_type = cgirls_mediatype_txt; + } else if (strncmp(exts, ".htm", 4) == 0) { + req.cgirls_type = cgirls_mediatype_htm; + } else if (strncmp(exts, ".xml", 4) == 0) { + req.cgirls_type = cgirls_mediatype_xml; + } else if (strncmp(exts, ".rdf", 4) == 0) { + req.cgirls_type = cgirls_mediatype_rdf; + } else { + break; // do not re·assign `eovb´ + } + eovb = exts; + } while (false); + } + if (eovb > sont) { + verb = strndup(sont, eovb - sont); + } + if (eove < eopi) { + sont = eove + 1; + } else { + sont = eopi; + } + if (verb) { + if (strcmp(verb, "branches") == 0) { + req.cgirls_action = cgirls_vb_branches; + } else if (strcmp(verb, "tags") == 0) { + req.cgirls_action = cgirls_vb_tags; + } else if (strcmp(verb, "show") == 0) { + req.cgirls_action = cgirls_vb_show; + } else if (strcmp(verb, "raw") == 0) { + req.cgirls_action = cgirls_vb_raw; + } else if (strcmp(verb, "blame") == 0) { + req.cgirls_action = cgirls_vb_blame; + } else if (strcmp(verb, "log") == 0) { + req.cgirls_action = cgirls_vb_log; + } else if (strcmp(verb, "shortlog") == 0) { + req.cgirls_action = cgirls_vb_shortlog; + } else if (strcmp(verb, "atom") == 0) { + req.cgirls_action = cgirls_vb_atom; + } else if (strcmp(verb, "patch") == 0) { + req.cgirls_action = cgirls_vb_patch; + } else if (strcmp(verb, "index") != 0) { + req.cgirls_action = cgirls_vb_unknown; + } + free(verb); + } else if (eovb < eopi) { + req.cgirls_action = cgirls_vb_unknown; + } + + // The portion of the pathinfo which follows the second slash but + // precedes the third identifies the identifiers for the request. If + // there is no third slash, the identifiers extend to the end of the + // string. A single identifier may be given, or two identifiers may + // be given separated by two periods. An empty string is equivalent + // to no identifier. + char const* eoid = strchr(sont, '/'); + if (!eoid) { + eoid = eopi; + } + char* idid = nullptr; + if (eoid > sont) { + idid = strndup(sont, eoid - sont); + } + if (eoid < eopi) { + sont = eoid + 1; + } else { + sont = eopi; + } + if (idid) { + // If the identifier string contains two successive dots, the base + // and target identifiers must be extracted and the original + // identifier string freed. Otherwise, the identifier string is the + // target identifier, and there is no base. + char const*const dots = strstr(idid, ".."); + if (dots) { + char const*const eods = dots + 2; + char const*const eoii = strchr(idid, 0); + if (dots > idid) { + req.cgirls_baseid = strndup(idid, dots - idid); + } + if (eods < eoii) { + req.cgirls_id = strndup(eods, eoii - eods); + } + free(idid); + } else { + req.cgirls_id = idid; + } + } + + // The portion of the pathinfo which follows the third slash is the + // subpath of the request. An empty sting is equivalent to having no + // subpath. Trailing and successive slashes are dropped. + char const* soct = sont; + char const* psep = nullptr; + size_t npth = 0; + while (eopi > soct) { + // Count the number of segments in the pathinfo so that the correct + // amount of space can be allocated. + psep = strchr(soct, '/'); + if (!psep) { + psep = eopi; + } + if (psep > soct) { + ++npth; + } + if (eopi > psep) { + soct = psep + 1; + } else { + soct = eopi; + } + } + req.cgirls_subpath = calloc(npth + 1, sizeof(char*)); + if (!req.cgirls_subpath) { + return req; + } + size_t pthi = 0; + while (eopi > sont) { + // Add the segments to the newly allocated array. + psep = strchr(sont, '/'); + if (!psep) { + psep = eopi; + } + if (psep > sont) { + req.cgirls_subpath[pthi++] = strndup(sont, psep - sont); + } + if (eopi > psep) { + sont = psep + 1; + } else { + sont = eopi; + } + } + assert(pthi == npth); + req.cgirls_subpath[pthi] = nullptr; + + // Return the result. + return req; +} + +char* cgirls_req2path(cgirls_req req) { + char* action = "unknown"; + char* extnsn = ""; + size_t length = 8; // length of `action´ plus 1, to start + + // Get the length of the various parts, saving the verb and the + // extension. This length includes a trailing slash, but in practice + // this will be replaced by the final null byte. + switch (req.cgirls_action) { + case cgirls_vb_index: + action = "index"; + length = 6; + break; + case cgirls_vb_branches: + action = "branches"; + length = 9; + break; + case cgirls_vb_tags: + action = "tags"; + length = 5; + break; + case cgirls_vb_show: + action = "show"; + length = 5; + break; + case cgirls_vb_raw: + action = "raw"; + length = 4; + break; + case cgirls_vb_blame: + action = "blame"; + length = 6; + break; + case cgirls_vb_log: + action = "log"; + length = 4; + break; + case cgirls_vb_shortlog: + action = "shortlog"; + length = 9; + break; + case cgirls_vb_atom: + action = "atom"; + length = 5; + break; + case cgirls_vb_patch: + action = "patch"; + length = 6; + break; + default: + break; + } + switch (req.cgirls_type) { + case cgirls_mediatype_txt: + extnsn = ".txt"; + break; + case cgirls_mediatype_htm: + extnsn = ".htm"; + break; + case cgirls_mediatype_xml: + extnsn = ".xml"; + break; + case cgirls_mediatype_rdf: + extnsn = ".rdf"; + break; + default: + break; + } + if (req.cgirls_project) { + length += strlen(req.cgirls_project) + 1; + if (req.cgirls_type != cgirls_mediatype_any) { + length += 4; + } + if (req.cgirls_baseid || req.cgirls_id) { + if (req.cgirls_baseid) { + length += strlen(req.cgirls_baseid) + 2; + } + if (req.cgirls_id) { + length += strlen(req.cgirls_id); + } + length += 1; + } else if (req.cgirls_subpath && req.cgirls_subpath[0]) { + length += 3; + } + if (req.cgirls_subpath) { + size_t i = 0; + char* c = req.cgirls_subpath[i]; + while (c) { + length += strlen(c) + 1; + c = req.cgirls_subpath[++i]; + } + } + } else { + // If there is no project, then the action must be removed, and the + // length is just that of the trailing slash. + length = 1; + } + // Create and compose the final path. + char* result = calloc(length, sizeof(char*)); + if (!result) { + return nullptr; + } + char* cursor = result; + if (req.cgirls_project) { + cursor = stpcpy(cursor, req.cgirls_project); + (cursor++)[0] = '/'; + cursor = stpcpy(cursor, action); + if (req.cgirls_type != cgirls_mediatype_any) { + cursor = stpcpy(cursor, extnsn); + } + (cursor++)[0] = '/'; + if (req.cgirls_baseid || req.cgirls_id) { + if (req.cgirls_baseid) { + cursor = stpcpy(cursor, req.cgirls_baseid); + cursor[0] = '.'; + cursor[1] = '.'; + cursor += 2; + } + if (req.cgirls_id) { + cursor = stpcpy(cursor, req.cgirls_id); + } + (cursor++)[0] = '/'; + } else if (req.cgirls_subpath && req.cgirls_subpath[0]) { + cursor = stpcpy(cursor, "../"); + } + if (req.cgirls_subpath) { + size_t i = 0; + char* c = req.cgirls_subpath[i]; + while (c) { + cursor = stpcpy(cursor, c); + c = req.cgirls_subpath[++i]; + (cursor++)[0] = '/'; + } + } + } else { + (cursor++)[0] = '/'; + } + + // At this point, `cursor´ points one ⹐past⹑ the last element of the + // array (this is allowed in C), and the last element is a slash. + // Rewind and set it to the null byte, and assert that everything was + // done correctly. + (--cursor)[0] = 0; + assert((cursor + 1) - result == length); + return result; +} diff --git a/request.h b/request.h new file mode 100644 index 0000000..2ab70e0 --- /dev/null +++ b/request.h @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2025 Lady +// SPDX-License-Identifier: GPL-2.0-only + +#ifndef CGIRLS_REQUEST_H +#define CGIRLS_REQUEST_H + +enum cgirls_mediatype : unsigned char { + // Unspecified media type + cgirls_mediatype_any = 0x00, + // Text media types + cgirls_mediatype_txt = 0x10, + cgirls_mediatype_htm = 0x11, + // X·M·L media types + cgirls_mediatype_xml = 0x20, + cgirls_mediatype_rdf = 0x21, +}; +typedef enum cgirls_mediatype cgirls_mediatype; + +enum cgirls_vb : unsigned char { + // Actions in general + cgirls_vb_index = 0x00, + // Actions on projects + cgirls_vb_branches = 0x10, + cgirls_vb_tags = 0x11, + // Actions on single objects + cgirls_vb_show = 0x20, + cgirls_vb_raw = 0x21, + cgirls_vb_blame = 0x22, + // Actions on ranges of objects + cgirls_vb_log = 0x30, + cgirls_vb_shortlog = 0x31, + cgirls_vb_atom = 0x32, + cgirls_vb_patch = 0x33, + // Unknown verb + cgirls_vb_unknown = 0xFF, +}; +typedef enum cgirls_vb cgirls_vb; + +typedef struct cgirls_req_status cgirls_req_status; +struct cgirls_req_status { + unsigned short cgirls_code; + char* cgirls_message; // if `cgirls_code´ is not ok +}; + +typedef struct cgirls_req cgirls_req; +struct cgirls_req { + cgirls_vb cgirls_action; + cgirls_mediatype cgirls_type; + char* cgirls_project; + char* cgirls_id; + char** cgirls_subpath; + char* cgirls_baseid; + cgirls_req_status cgirls_status; +}; + +/* +Frees up any dynamically‐allocated memory which was allocated by +`cgirls_path2req´. +*/ +void cgirls_freereq (cgirls_req req); + +/* +Converts the provided “path info” string into a `cgirls_req´ struct +and returns the result. + +This struct contains dynamically‐allocated strings which must be freed +by calling `cgirls_freereq´. + +Maximally, a “path info” string has the following form :— + + {project}/{action}/{baseid}..{id}/{subpath} + +—: (where subpath can contain additional slashes, and action may +optionally include one of a small number of supported extensions). +`baseid´ is optional; if omitted, the dots preceding `id´ are also +dropped. For all other components, all preceding components must be +provided if a given component is provided. +*/ +cgirls_req cgirls_path2req(char const*const pathinfo); + +/* +Returns the canonical “path info” string which represents the provided +`cgirls_req´. + +Note that if `cgirls_req.cgirls_project´ is the null pointer, the +canonical “path info” string is always the empty string. +*/ +char* cgirls_req2path(cgirls_req); + +#endif /* CGIRLS_REQUEST_H */ diff --git a/sh/test.sh b/sh/test.sh index fc0935b..1d9b5e2 100755 --- a/sh/test.sh +++ b/sh/test.sh @@ -80,9 +80,9 @@ do : else : printf '%27b \n' '\0033[1;93;41;m[NG]\0033[0m' printf '\033[7m \033[0m \033[2m%b\033[0m \n' 'Result did not match expectation.' 'Got:' - printf '\033[30;107m%-49s\033[0m\n' "${TEST_RESULT}" + printf '%s\n' "${TEST_RESULT}" | sed 's/'"'"'/'"'"'"'"'"'"'"'"'/g;s/^/'"'"'/;s/$/'"'"'/' | xargs -E '' printf '\033[30;107m%-49s\033[0m\n' printf '\033[7m \033[0m \033[2m%b\033[0m \n' 'Expected:' - printf '\033[30;107m%-49s\033[0m\n' "${TEST_EXPECTATION}" + printf '%s\n' "${TEST_EXPECTATION}" | sed 's/'"'"'/'"'"'"'"'"'"'"'"'/g;s/^/'"'"'/;s/$/'"'"'/' | xargs -E '' printf '\033[30;107m%-49s\033[0m\n' TEST_EXIT_STATUS=$((${TEST_EXIT_STATUS} | 1)) fi fi diff --git a/test/pathinfo/01-emptyproj b/test/pathinfo/01-emptyproj new file mode 100644 index 0000000..37442f4 --- /dev/null +++ b/test/pathinfo/01-emptyproj @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 + +/show diff --git a/test/pathinfo/02-blush b/test/pathinfo/02-blush new file mode 100644 index 0000000..63e9bfe --- /dev/null +++ b/test/pathinfo/02-blush @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +//////////// +p//////////d diff --git a/test/pathinfo/03-noverb b/test/pathinfo/03-noverb new file mode 100644 index 0000000..316d7f9 --- /dev/null +++ b/test/pathinfo/03-noverb @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p//b..i//j diff --git a/test/pathinfo/04-badverbwext b/test/pathinfo/04-badverbwext new file mode 100644 index 0000000..b5cbee1 --- /dev/null +++ b/test/pathinfo/04-badverbwext @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/na.txt diff --git a/test/pathinfo/05-noverbokext b/test/pathinfo/05-noverbokext new file mode 100644 index 0000000..2b4866f --- /dev/null +++ b/test/pathinfo/05-noverbokext @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/.txt diff --git a/test/pathinfo/06-dotid b/test/pathinfo/06-dotid new file mode 100644 index 0000000..2f46dfe --- /dev/null +++ b/test/pathinfo/06-dotid @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/show/../ +p/show/../d diff --git a/test/pathinfo/07-nobase b/test/pathinfo/07-nobase new file mode 100644 index 0000000..c3a5e31 --- /dev/null +++ b/test/pathinfo/07-nobase @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/show/..i/ diff --git a/test/pathinfo/08-notarget b/test/pathinfo/08-notarget new file mode 100644 index 0000000..aa33f15 --- /dev/null +++ b/test/pathinfo/08-notarget @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/show/b.. diff --git a/test/pathinfo/09-canonical b/test/pathinfo/09-canonical new file mode 100644 index 0000000..1872535 --- /dev/null +++ b/test/pathinfo/09-canonical @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/log.txt/b..i/s diff --git a/test/pathinfo/10-blushypath b/test/pathinfo/10-blushypath new file mode 100644 index 0000000..85d519e --- /dev/null +++ b/test/pathinfo/10-blushypath @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Lady +# SPDX-License-Identifier: CC0-1.0 +p/log/b..i//m//n///o////