From 1ae2ad4b4ea4f2549bede33e895d487071a1959c Mon Sep 17 00:00:00 2001 From: Darren Salt Date: Wed, 22 Apr 2026 20:29:18 +0100 Subject: [PATCH 1/2] Improve path canonicalisation. * Convert flags for canonical() into an enum * Add flags for ~ expansion and full path * Do full pathname expansion (using realpath(3)), but retain the leafname (to avoid breaking symlink-breaking) --- joe/b.c | 85 +++++++++++++++++++++++++++++++++++++++++++++++----- joe/b.h | 10 +++++-- joe/path.c | 2 +- joe/pw.c | 8 ++--- joe/pw.h | 2 +- joe/tab.c | 2 +- joe/uerror.c | 4 +-- joe/ufile.c | 4 +-- 8 files changed, 97 insertions(+), 20 deletions(-) diff --git a/joe/b.c b/joe/b.c index 5fa0bd51..befb494d 100644 --- a/joe/b.c +++ b/joe/b.c @@ -2569,15 +2569,42 @@ char *parsens(const char *s, off_t *skip, off_t *amnt, int *binary) return n; } +#ifndef __MSDOS__ +#if defined(PATH_MAX) && PATH_MAX >= 4096 +#define JOE_PATH_MAX PATH_MAX +#elif defined(MAXPATHLEN) && MAXPATHLEN >= 4096 +#define JOE_PATH_MAX MAXPATHLEN +#else +#define JOE_PATH_MAX 4096 +#endif + +#if _POSIX_VERSION >= 200809L +static char *joe_realpath(const char *path) +{ + char *buf = realpath(path, NULL); + char *ret = buf ? vsndup(NULL, 0, sz(buf)) : NULL; + free(buf); + return ret; +} +#else +static char *joe_realpath(const char *path) +{ + char buf[JOE_PATH_MAX]; + char *ret = realpath(path, buf); + return ret ? vsndup(NULL, 0, sz(ret)) : NULL; +} +#endif +#endif + /* Canonicalize file name: do ~ expansion */ -char *canonical(char *n, int flags) +char *canonical(char *n, enum canflags flags) { ptrdiff_t y = 0; #ifndef __MSDOS__ ptrdiff_t x; - char *s; - if (!(flags & CANFLAG_NORESTART)) { + char *s, *filepath; + if (flags & CANFLAG_RESTART) { for (y = zlen(n); ; --y) if (y <= 2) { y = 0; @@ -2587,7 +2614,7 @@ char *canonical(char *n, int flags) break; } } - if (n[y] == '~') { + if (flags & CANFLAG_TILDE && n[y] == '~') { for (x = y + 1; n[x] && n[x] != '/'; ++x) ; if (n[x] == '/') { if (x == y + 1) { @@ -2617,6 +2644,32 @@ char *canonical(char *n, int flags) } } } + + if (flags & CANFLAG_FULLPATH) { + filepath = joe_realpath(n + y); + if (filepath) { + vsrm(n); + n = filepath; + } else { + x = zlen(n + y); + while (x > y && n[--x] != '/') ; /* chop off leafname */ + if (x > y) { + n[x] = 0; + filepath = joe_realpath(n + y); + n[x++] = '/'; + } else { + filepath = joe_realpath("."); + } + if (filepath) { + if (filepath[sLen(filepath) - 1] != '/') + filepath = vsncpy(sv(filepath), "/", 1); + filepath = vsncpy(sv(filepath), sz(n+x)); + vsrm(n); + n = filepath; + y = 0; + } + } + } #endif if (y) { char *z = vsncpy(NULL, 0, n + y, zlen(n + y)); @@ -2626,6 +2679,12 @@ char *canonical(char *n, int flags) return n; } +char *canonical_copy(const char *s, enum canflags flags) +{ + char *sp = (char *)s; + return canonical(vsndup(NULL, 0, sz(sp)), flags); +} + static off_t euclid(off_t a, off_t b) { if (!a) @@ -2995,6 +3054,7 @@ B *bload(const char *s) B *bfind(const char *s) { B *b; + char *s_full = NULL; if (!s || !s[0]) { berror = -1; @@ -3005,18 +3065,29 @@ B *bfind(const char *s) b->er = berror; return b; } - for (b = bufs.link.next; b != &bufs; b = b->link.next) - if (b->name && !zcmp(s, b->name)) { + + if (*s != '*') + s_full = canonical_copy(s, CANFLAG_TILDE | CANFLAG_FULLPATH); + for (b = bufs.link.next; b != &bufs; b = b->link.next) { + char *n_full = b->name ? canonical_copy(b->name, CANFLAG_TILDE | CANFLAG_FULLPATH) : NULL; + + if (b->name && !zcmp(s_full ? s_full : s, n_full ? n_full : b->name)) { if (!b->orphan) ++b->count; /* Assumes caller is going to put this in a window! */ else b->orphan = 0; berror = 0; b->internal = 0; - return b; + if (n_full) vsrm(n_full); + goto ret; } + if (n_full && n_full != b->name) vsrm(n_full); + } b = bload(s); /* Returns count==1 */ + +ret: b->internal = 0; + if (s_full && s_full != s) vsrm(s_full); return b; } diff --git a/joe/b.h b/joe/b.h index 5276db0a..886fb34f 100644 --- a/joe/b.h +++ b/joe/b.h @@ -294,8 +294,14 @@ int bsavefd(P *p, int fd, off_t size); char *parsens(const char *s, off_t *skip, off_t *amnt, int *binary); -char *canonical(char *s, int flags); -#define CANFLAG_NORESTART 1 /* Support path restart feature */ +enum canflags { + CANFLAG_RESTART = 1, /* path restart feature */ + CANFLAG_TILDE = 2, /* tilde expansion */ + CANFLAG_FULLPATH = 4, /* get the full path */ +}; + +char *canonical(char *s, enum canflags flags); +char *canonical_copy(const char *s, enum canflags flags); /* Get byte at pointer or return NO_MORE_DATA if pointer is at end of buffer */ int brc(P *p); diff --git a/joe/path.c b/joe/path.c index 3d5ced90..57ecdad9 100644 --- a/joe/path.c +++ b/joe/path.c @@ -612,7 +612,7 @@ static char *open_configrc_file(JFILE **result, const char *sys, const char *pre /* Absolute path given, don't add suffix (too confusing when user knows it's a file) */ vsrm(fullpath); fullpath = vsncpy(NULL, 0, sz(name)); - fullpath = canonical(fullpath, 0); + fullpath = canonical(fullpath, CANFLAG_RESTART | CANFLAG_TILDE | CANFLAG_FULLPATH); f = jfopen(fullpath, "r"); if (f) { *result = f; diff --git a/joe/pw.c b/joe/pw.c index 0ba46291..7e10223c 100644 --- a/joe/pw.c +++ b/joe/pw.c @@ -146,10 +146,10 @@ static int rtnpw(W *w) p_goto_bol(bw->cursor); s = brvs(bw->cursor, byte - bw->cursor->byte); - if (pw->file_prompt & PWFLAG_FILENAME) - s = canonical(s, 0); /* Tilde expansion and // restart */ - else if (pw->file_prompt & PWFLAG_COMMAND) - s = canonical(s, CANFLAG_NORESTART); /* Tilde expansion only */ + if (pw->file_prompt & PWFLAG_COMMAND) + s = canonical(s, CANFLAG_TILDE); /* Tilde expansion only */ + else if (pw->file_prompt & PWFLAG_FILENAME) + s = canonical(s, CANFLAG_RESTART | CANFLAG_TILDE | CANFLAG_FULLPATH); /* Tilde expansion and // restart */ /* Save text into history buffer */ if (pw->hist) { diff --git a/joe/pw.h b/joe/pw.h index 9f007434..ee2af003 100644 --- a/joe/pw.h +++ b/joe/pw.h @@ -17,7 +17,7 @@ struct pw { ptrdiff_t promptofst; /* Prompt scroll offset */ B *hist; /* History buffer */ void *object; /* Object */ - bool file_prompt; /* Set if this is a file name prompt, so do ~ expansion */ + int file_prompt; /* Set if this is a file name prompt, so do ~ expansion */ }; /* BW *wmkpw(BW *bw,char *prompt,int (*func)(),char *huh,int (*abrt)(), diff --git a/joe/tab.c b/joe/tab.c index e14bec42..457c06c4 100644 --- a/joe/tab.c +++ b/joe/tab.c @@ -55,7 +55,7 @@ static int get_entries(TAB *tab, ino_t prv) int only_cmds = 0; tmp = vsncpy(NULL,0,sv(tab->path)); - tmp = canonical(tmp, 0); + tmp = canonical(tmp, CANFLAG_RESTART); if (chpwd(tmp)) { vsrm(tmp); diff --git a/joe/uerror.c b/joe/uerror.c index c1b6aeae..708151f6 100644 --- a/joe/uerror.c +++ b/joe/uerror.c @@ -351,7 +351,7 @@ static int parseit(struct charmap *map,const char *s, off_t row, if (current_dir && *name != '/') { t = vsncpy(NULL, 0, sv(current_dir)); t = vsncpy(sv(t), sv(name)); - t = canonical(t, CANFLAG_NORESTART); + t = canonical(t, CANFLAG_FULLPATH); vsrm(name); } else { t = name; @@ -646,7 +646,7 @@ int ujump(W *w, int k) fullname = vsncpy(NULL, 0, sv(curd)); fullname = vsncpy(sv(fullname), sv(name)); vsrm(name); - name = canonical(fullname, CANFLAG_NORESTART); + name = canonical(fullname, CANFLAG_FULLPATH); if (name && line != -1) { ERROR *er = srcherr(bw, name, line); uprevw(bw->parent, 0); diff --git a/joe/ufile.c b/joe/ufile.c index 709607c6..c2f6759c 100644 --- a/joe/ufile.c +++ b/joe/ufile.c @@ -187,7 +187,7 @@ static int backup(BW *bw) if (backpath) { char *t = vsncpy(NULL,0,sz(backpath)); - t = canonical(t, CANFLAG_NORESTART); + t = canonical(t, CANFLAG_TILDE | CANFLAG_FULLPATH); joe_snprintf_2(name, SIZEOF(name), "%s/%s", t, namepart(tmp, SIZEOF(tmp), bw->b->name)); vsrm(t); } else { @@ -207,7 +207,7 @@ static int backup(BW *bw) /* Create backup file name */ if (backpath) { char *t = vsncpy(NULL, 0, sz(backpath)); - t = canonical(t, CANFLAG_NORESTART); + t = canonical(t, CANFLAG_TILDE | CANFLAG_FULLPATH); mkpath(t); joe_snprintf_3(name, SIZEOF(name), "%s/%s%s", t, namepart(tmp, SIZEOF(tmp), dequote(bw->b->name)), backup_file_suffix); vsrm(t); From eb2ccd28cd875b3ab8ed0123d00c3969201ec308 Mon Sep 17 00:00:00 2001 From: Darren Salt Date: Sat, 25 Apr 2026 14:24:27 +0100 Subject: [PATCH 2/2] Fix up strictness about canonicalisation when saving. --- joe/pw.c | 2 +- joe/ufile.c | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/joe/pw.c b/joe/pw.c index 7e10223c..11f344de 100644 --- a/joe/pw.c +++ b/joe/pw.c @@ -149,7 +149,7 @@ static int rtnpw(W *w) if (pw->file_prompt & PWFLAG_COMMAND) s = canonical(s, CANFLAG_TILDE); /* Tilde expansion only */ else if (pw->file_prompt & PWFLAG_FILENAME) - s = canonical(s, CANFLAG_RESTART | CANFLAG_TILDE | CANFLAG_FULLPATH); /* Tilde expansion and // restart */ + s = canonical(s, CANFLAG_RESTART | CANFLAG_TILDE); /* Tilde expansion and // restart */ /* Save text into history buffer */ if (pw->hist) { diff --git a/joe/ufile.c b/joe/ufile.c index c2f6759c..9893bc4c 100644 --- a/joe/ufile.c +++ b/joe/ufile.c @@ -480,8 +480,10 @@ static int dosave1(W *w, char *s, void *object, int *notify) req->name = s; if (s[0] != '!' && !(s[0] == '>' && s[1] == '>')) { + char *cname = bw->b->name ? canonical_copy(bw->b->name, CANFLAG_TILDE|CANFLAG_FULLPATH) : NULL; /* It's a normal file: not a pipe or append */ - if (!bw->b->name || zcmp(s, bw->b->name)) { + if (!cname || (zcmp(s, cname) && zcmp(s, bw->b->name))) { + vsrm(cname); /* Newly named file or name is different than buffer */ f = open(dequote(s), O_RDONLY); if (f != -1) { @@ -493,6 +495,7 @@ static int dosave1(W *w, char *s, void *object, int *notify) } } else { + vsrm(cname); /* We're saving a newer version of the same file */ if (check_mod(bw->b)) { req->message = joe_gettext(_("File on disk is newer. Overwrite (y,n,%{abort})? "));