aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPeter Geissler <peter@haxx.in>2026-08-28 15:02:49 +0200
committerPeter Geissler <peter@haxx.in>2026-08-28 15:02:49 +0200
commit22e70baa7172e8487913cceae3d3ec89bbe40627 (patch)
treecd067b59109b829e423995ee10d02f30392ff47a
parentctrl_d_exits: Ctrl-D at a prompt, where the tty cannot do it (diff)
downloadslosh-22e70baa7172e8487913cceae3d3ec89bbe40627.tar.gz
slosh-22e70baa7172e8487913cceae3d3ec89bbe40627.tar.xz
slosh-22e70baa7172e8487913cceae3d3ec89bbe40627.zip
path: an absolute path does not always start with a slash
`include "C:/themes/t.kdl"` on Windows did not fail to find the file. It found a different one, or rather looked for it: absoluteness was tested as `path[0] == '/'`, a drive letter is not that, so the path was read as relative and joined to the including file's directory. The result was cannot open C:/Users/you/cfgA/C:/Users/you/cfgB/t.kdl which at least says what happened. A layout's `cwd="C:/srv/api"` took the same turn through path_resolve() and said nothing at all -- the pane started in the wrong directory, which is the failure this file's own header comment is about. Windows spells absolute three ways and all three now count: rooted on the current drive (`\dir`), rooted on a named one (`C:\dir`, `C:/dir`), and a UNC share. A drive letter has to be followed by a separator to qualify, because `C:file` is relative to the current directory *of that drive* -- a real thing, and not one we can resolve for anyone. Windows-only, deliberately. `C:` is an ordinary file name on POSIX and a path beginning with one is relative there, so reading it as a drive would break the platform that works in order to fix the one that does not.
-rw-r--r--include/slosh.h5
-rw-r--r--src/config.c2
-rw-r--r--src/path.c33
3 files changed, 38 insertions, 2 deletions
diff --git a/include/slosh.h b/include/slosh.h
index e1ab08b..793e32c 100644
--- a/include/slosh.h
+++ b/include/slosh.h
@@ -104,6 +104,11 @@ char *screen_dump_json(screen_t *s);
/* ---- paths -------------------------------------------------------------- */
+/* Whether a path stands on its own. A leading `/` everywhere; on Windows also
+ * a drive (`C:\dir`, `C:/dir`), a drive-relative root (`\dir`) and a UNC
+ * share. Drive letters are read as such only on Windows, because `C:` is an
+ * ordinary file name on POSIX. */
+bool path_is_absolute(const char *path);
/* Expand a leading `~`, using `buf` when it has to. Returns `path` itself when
* there is nothing to do, so `buf` only has to outlive the result's use. */
const char *path_expand(const char *path, char *buf, size_t cap);
diff --git a/src/config.c b/src/config.c
index ae22278..f015892 100644
--- a/src/config.c
+++ b/src/config.c
@@ -1933,7 +1933,7 @@ static void include_path(const char *base_file, const char *ref, char *out,
size_t cap) {
char buf[512];
const char *r = path_expand(ref, buf, sizeof buf);
- if (r[0] == '/') {
+ if (path_is_absolute(r)) {
snprintf(out, cap, "%s", r);
return;
}
diff --git a/src/path.c b/src/path.c
index 9f962b6..00a15f6 100644
--- a/src/path.c
+++ b/src/path.c
@@ -15,6 +15,37 @@
#include <stdlib.h>
#include <string.h>
+/* Whether a path names a place outright, rather than one to be read against
+ * somewhere else.
+ *
+ * On POSIX that is a leading `/` and nothing else. Windows spells it three
+ * ways and all three have to count: rooted on the current drive (`\dir`),
+ * rooted on a named one (`C:\dir`, `C:/dir`), and a UNC share
+ * (`\\server\share`). Getting it wrong is not a lookup that fails, it is a
+ * concatenation that succeeds at being nonsense -- `include "C:/themes/t.kdl"`
+ * was resolved against the including file's directory and opened as
+ * `C:/Users/you/.config/slosh/C:/themes/t.kdl`.
+ *
+ * A drive letter must be followed by a separator to be absolute: `C:file` is
+ * relative to the current directory *of drive C*, which is a different thing
+ * and not one we can resolve for anybody.
+ *
+ * All of it is Windows-only on purpose. `C:` is an ordinary file name on
+ * POSIX and a path that begins with one is relative there, so reading it as a
+ * drive would break the platform that works to fix the one that does not. */
+bool path_is_absolute(const char *path) {
+ if (!path || !*path) return false;
+ if (path[0] == '/') return true;
+#ifdef _WIN32
+ if (path[0] == '\\') return true;
+ bool drive = (path[0] >= 'A' && path[0] <= 'Z') ||
+ (path[0] >= 'a' && path[0] <= 'z');
+ if (drive && path[1] == ':' && (path[2] == '/' || path[2] == '\\'))
+ return true;
+#endif
+ return false;
+}
+
const char *path_expand(const char *path, char *buf, size_t cap) {
if (!path) return NULL;
/* `~` and `~/...` only. `~user` needs the password database and is not
@@ -42,7 +73,7 @@ const char *path_resolve(const char *path, const char *base, char *buf,
size_t cap) {
if (!path) return NULL;
if (path[0] == '~') return path_expand(path, buf, cap);
- if (path[0] == '/' || !base || !*base) return path;
+ if (path_is_absolute(path) || !base || !*base) return path;
/* `.` is the project directory itself, which is what a dump writes for the
* pane that started there -- joined naively it would be `/home/you/dev/api/.`,
* which works and reads like a bug. */