aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPeter Geissler <peter@haxx.in>2026-08-28 15:04:28 +0200
committerPeter Geissler <peter@haxx.in>2026-08-28 15:09:18 +0200
commit6f8bdf838b82c0c3326993a741985840dc130af6 (patch)
treee3cca1f71709761deca4422bc3d597b7916e76e5
parentpath: an absolute path does not always start with a slash (diff)
downloadslosh-6f8bdf838b82c0c3326993a741985840dc130af6.tar.gz
slosh-6f8bdf838b82c0c3326993a741985840dc130af6.tar.xz
slosh-6f8bdf838b82c0c3326993a741985840dc130af6.zip
path: a directory ends at either separator on Windows
Two places split a path by hand, both with strrchr(path, '/'), both falling back to "." when there was none: include_path() in config.c and watch_config() in server.c. On Windows "none" is the ordinary case. `SLOSH_CONFIG=C:\Users\you\slosh\config.kdl` is how a person writes it, and a path that has been near a native API comes back all backslashes whatever it started as. So an include resolved against "." -- `cannot open ./themes/t.kdl` -- and, worse and quietly, the config watcher watched "." too. Not the config's directory: the current directory of whatever started the session. Hot reload did not fail, it watched somewhere else and waited. Saving your config did nothing at all, with no error to go looking for. The default path escaped this by accident. slosh builds it as `C:\Users\you/.config/slosh/config.kdl` -- mixed, because a literal joined to $HOME always is -- and that one embedded '/' is what strrchr found. It worked for the path slosh writes and not for the one a user does. Both callers now use path_dir(), which learns to stop at a backslash too, and server.c takes path_base() for the other half rather than keeping its own copy of the arithmetic. Windows-only: a backslash is a legal character in a POSIX file name, and splitting on it there would invent a directory out of a file called `a\b`. path_dir() also keeps the separator when what is left would be a bare drive, because `C:\config.kdl` lives in `C:\` and `C:` names the current directory on drive C, which is a different place. Verified on the Windows box with a fully backslashed SLOSH_CONFIG: --check passes, the include loads, and editing the config reloads the running session. Watching a second directory is still unreliable; that is the watcher's own bug and the next commit.
-rw-r--r--include/slosh.h4
-rw-r--r--src/config.c8
-rw-r--r--src/path.c46
-rw-r--r--src/server.c13
4 files changed, 47 insertions, 24 deletions
diff --git a/include/slosh.h b/include/slosh.h
index 793e32c..e624bda 100644
--- a/include/slosh.h
+++ b/include/slosh.h
@@ -119,8 +119,10 @@ const char *path_expand(const char *path, char *buf, size_t cap);
const char *path_resolve(const char *path, const char *base, char *buf,
size_t cap);
/* The directory part of a path: the base `path_resolve` wants. `.` when there
- * is no `/` in it. */
+ * is no separator in it. On Windows a backslash separates too. */
const char *path_dir(const char *path, char *buf, size_t cap);
+/* The name part: the other half of `path_dir`, pointing into `path`. */
+const char *path_base(const char *path);
/* The inverse of `path_resolve` for writing one back out: a path under `base`
* relative to it, `.` for `base` itself, and anything not under it unchanged.
* Points into `path` or a literal, so it needs no buffer. */
diff --git a/src/config.c b/src/config.c
index f015892..f85ccb1 100644
--- a/src/config.c
+++ b/src/config.c
@@ -1938,13 +1938,7 @@ static void include_path(const char *base_file, const char *ref, char *out,
return;
}
char dir[512];
- snprintf(dir, sizeof dir, "%s", base_file);
- char *slash = strrchr(dir, '/');
- if (slash)
- *slash = 0;
- else
- snprintf(dir, sizeof dir, ".");
- snprintf(out, cap, "%s/%s", dir, r);
+ snprintf(out, cap, "%s/%s", path_dir(base_file, dir, sizeof dir), r);
}
/* Every `include` at the top level of this file, in order, applied *before* the
diff --git a/src/path.c b/src/path.c
index 00a15f6..7828a11 100644
--- a/src/path.c
+++ b/src/path.c
@@ -38,8 +38,8 @@ bool path_is_absolute(const char *path) {
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');
+ 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
@@ -83,21 +83,55 @@ const char *path_resolve(const char *path, const char *base, char *buf,
return buf;
}
+/* The last separator in a path, whichever kind it is.
+ *
+ * Windows takes both and mixes them without being asked: slosh joins with '/'
+ * and the environment supplies '\\', so `$HOME` plus a literal lands as
+ * `C:\Users\you/.config/slosh/config.kdl`. A path that has been near a native
+ * API comes back all backslashes, and `SLOSH_CONFIG=C:\Users\you\slosh.kdl`
+ * is how a person would write it in the first place.
+ *
+ * Windows-only, because a backslash is a legal character in a POSIX file
+ * name: splitting on it there would invent a directory out of a file called
+ * `a\b`. */
+static const char *last_sep(const char *path) {
+ const char *slash = strrchr(path, '/');
+#ifdef _WIN32
+ const char *back = strrchr(path, '\\');
+ if (back && (!slash || back > slash)) slash = back;
+#endif
+ return slash;
+}
+
/* The directory a file lives in: what `path_resolve` wants as its base. A path
- * with no `/` in it is in the current one, which is `.` rather than `""` --
- * an empty base means "no base" to everything downstream. */
+ * with no separator in it is in the current one, which is `.` rather than `""`
+ * -- an empty base means "no base" to everything downstream. */
const char *path_dir(const char *path, char *buf, size_t cap) {
if (!path) return NULL;
- const char *slash = strrchr(path, '/');
+ const char *slash = last_sep(path);
if (!slash) return ".";
- if (slash == path) return "/";
+ if (slash == path) return *slash == '/' ? "/" : "\\";
size_t n = (size_t)(slash - path);
+#ifdef _WIN32
+ /* `C:\config.kdl` lives in `C:\`, not in `C:` -- which names the current
+ * directory *on* drive C and is a different place. Keep the separator.
+ * Windows-only: on POSIX `a:` is just a directory called `a:`. */
+ if (n == 2 && path[1] == ':') n = 3;
+#endif
if (n >= cap) n = cap - 1;
memcpy(buf, path, n);
buf[n] = 0;
return buf;
}
+/* The name part, the other half of path_dir. Points into `path`, so there is
+ * nothing to size and nothing to free. */
+const char *path_base(const char *path) {
+ if (!path) return NULL;
+ const char *slash = last_sep(path);
+ return slash ? slash + 1 : path;
+}
+
/* The inverse, for writing a layout back out: a path under `base` becomes what
* it is relative to it, and anything else is left absolute. Left absolute
* rather than climbed out of with `../..`, because a pane whose directory is
diff --git a/src/server.c b/src/server.c
index 3a3669b..a0ef1c6 100644
--- a/src/server.c
+++ b/src/server.c
@@ -297,16 +297,9 @@ static void watch_config(watchset_t *w) {
const char *files[WATCH_MAX];
size_t n = app_config_files(files, WATCH_MAX);
for (size_t i = 0; i < n; i++) {
- char dir[512], base[128];
- snprintf(dir, sizeof dir, "%s", files[i]);
- char *slash = strrchr(dir, '/');
- if (slash) {
- *slash = 0;
- snprintf(base, sizeof base, "%s", slash + 1);
- } else {
- snprintf(base, sizeof base, "%s", files[i]);
- snprintf(dir, sizeof dir, ".");
- }
+ char dirbuf[512], base[128], dir[512];
+ snprintf(dir, sizeof dir, "%s", path_dir(files[i], dirbuf, sizeof dirbuf));
+ snprintf(base, sizeof base, "%s", path_base(files[i]));
/* The main config's directory is ours to create, so that a session started
* before the file exists still notices it appearing. An include's is not:
* a mistyped path should not leave a directory behind. */