diff options
| author | Peter Geissler <peter@haxx.in> | 2026-08-28 15:04:28 +0200 |
|---|---|---|
| committer | Peter Geissler <peter@haxx.in> | 2026-08-28 15:09:18 +0200 |
| commit | 6f8bdf838b82c0c3326993a741985840dc130af6 (patch) | |
| tree | e3cca1f71709761deca4422bc3d597b7916e76e5 | |
| parent | path: an absolute path does not always start with a slash (diff) | |
| download | slosh-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.h | 4 | ||||
| -rw-r--r-- | src/config.c | 8 | ||||
| -rw-r--r-- | src/path.c | 46 | ||||
| -rw-r--r-- | src/server.c | 13 |
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 @@ -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. */ |
