]> Lady’s Gitweb - CGirls/commitdiff
Add request parsing and related tests
authorLady <redacted>
Tue, 18 Mar 2025 04:31:32 +0000 (00:31 -0400)
committerLady <redacted>
Thu, 20 Mar 2025 04:22:22 +0000 (00:22 -0400)
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.

24 files changed:
cgirls-test-pathinfo.c [new file with mode: 0644]
expect/pathinfo/01-emptyproj [new file with mode: 0644]
expect/pathinfo/02-blush [new file with mode: 0644]
expect/pathinfo/03-noverb [new file with mode: 0644]
expect/pathinfo/04-badverbwext [new file with mode: 0644]
expect/pathinfo/05-noverbokext [new file with mode: 0644]
expect/pathinfo/06-dotid [new file with mode: 0644]
expect/pathinfo/07-nobase [new file with mode: 0644]
expect/pathinfo/08-notarget [new file with mode: 0644]
expect/pathinfo/09-canonical [new file with mode: 0644]
expect/pathinfo/10-blushypath [new file with mode: 0644]
request.c [new file with mode: 0644]
request.h [new file with mode: 0644]
sh/test.sh
test/pathinfo/01-emptyproj [new file with mode: 0644]
test/pathinfo/02-blush [new file with mode: 0644]
test/pathinfo/03-noverb [new file with mode: 0644]
test/pathinfo/04-badverbwext [new file with mode: 0644]
test/pathinfo/05-noverbokext [new file with mode: 0644]
test/pathinfo/06-dotid [new file with mode: 0644]
test/pathinfo/07-nobase [new file with mode: 0644]
test/pathinfo/08-notarget [new file with mode: 0644]
test/pathinfo/09-canonical [new file with mode: 0644]
test/pathinfo/10-blushypath [new file with mode: 0644]

diff --git a/cgirls-test-pathinfo.c b/cgirls-test-pathinfo.c
new file mode 100644 (file)
index 0000000..160797c
--- /dev/null
@@ -0,0 +1,72 @@
+// SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..1556201
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#lady>
+# SPDX-License-Identifier: CC0-1.0
+
diff --git a/expect/pathinfo/02-blush b/expect/pathinfo/02-blush
new file mode 100644 (file)
index 0000000..0cd0ae8
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..5e7ea13
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..40a59d6
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..59372b6
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..3db7ef0
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..3bf4cc2
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..aa33f15
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..1872535
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..38db5b2
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..ca58f0b
--- /dev/null
+++ b/request.c
@@ -0,0 +1,375 @@
+// SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..2ab70e0
--- /dev/null
+++ b/request.h
@@ -0,0 +1,90 @@
+// SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 */
index fc0935b61449510062f3a1dec3c745ce4242f873..1d9b5e24d3dc90a830e49b79034fb0e8c631fe01 100755 (executable)
@@ -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 (file)
index 0000000..37442f4
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#lady>
+# SPDX-License-Identifier: CC0-1.0
+
+/show
diff --git a/test/pathinfo/02-blush b/test/pathinfo/02-blush
new file mode 100644 (file)
index 0000000..63e9bfe
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..316d7f9
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..b5cbee1
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..2b4866f
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..2f46dfe
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..c3a5e31
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..aa33f15
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..1872535
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#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 (file)
index 0000000..85d519e
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2025 Lady <https://www.ladys.computer/about/#lady>
+# SPDX-License-Identifier: CC0-1.0
+p/log/b..i//m//n///o////
This page took 0.635103 seconds and 4 git commands to generate.