From ff5bc291c5b5b9f5df5d7438092a806ae933510d Mon Sep 17 00:00:00 2001 From: apnadkarni Date: Sun, 24 May 2026 12:39:59 +0000 Subject: [PATCH 1/3] Bug [1215dca7] - inconsistent file join with volume relative arguments --- changes.md | 1 + generic/tclIOUtil.c | 3 ++- tests/fileName.test | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/changes.md b/changes.md index 11072ad990..894c95cf47 100644 --- a/changes.md +++ b/changes.md @@ -24,6 +24,7 @@ to the userbase. - [File normalization inconsistency for glob result on Windows](https://core.tcl-lang.org/tcl/info/108904) - [Avoid ClockClientData typedef redefinition](https://core.tcl-lang.org/tcl/info/4724f3) - [Building sqlite3 for tcl8 redefines typedef](https://core.tcl-lang.org/tcl/info/ad08bc) + - [Inconsistent file join with volume-relative arguments.](https://core.tcl-lang.org/tcl/info/1215dc) # Updated bundled packages, libraries, standards, data - autoconf 2.73 diff --git a/generic/tclIOUtil.c b/generic/tclIOUtil.c index 066b65bfea..2b94831f18 100644 --- a/generic/tclIOUtil.c +++ b/generic/tclIOUtil.c @@ -3914,7 +3914,8 @@ TclGetPathType( if (type != TCL_PATH_ABSOLUTE) { type = TclpGetNativePathType(pathPtr, driveNameLengthPtr, driveNameRef); - if ((type == TCL_PATH_ABSOLUTE) && (filesystemPtrPtr != NULL)) { + /* Bug 1215dca78f - If not relative, need to update owning FS. */ + if ((type != TCL_PATH_RELATIVE) && (filesystemPtrPtr != NULL)) { *filesystemPtrPtr = &tclNativeFilesystem; } } diff --git a/tests/fileName.test b/tests/fileName.test index 6672f28a98..827959f96d 100644 --- a/tests/fileName.test +++ b/tests/fileName.test @@ -576,6 +576,20 @@ test filename-9.24 {Tcl_JoinPath: unix} {testsetplatform} { [file join /x /x {foo/bar}] string map [list /x ""] $res } {foo/bar /foo/bar /foo/bar} +test filename-9.25 { + Bug 1215dc - Inconsistent file join with volume relative arguments: unix +} -constraints testsetplatform -body { + testsetplatform unix + file join //zipfs:/foo c:bar +} -result "//zipfs:/foo/c:bar" +test filename-9.26 { + Bug 1215dc - Inconsistent file join with volume relative arguments: win +} -constraints testsetplatform -body { + testsetplatform win + file join //zipfs:/foo c:bar +} -result "c:bar" + + test filename-10.1 {Tcl_TranslateFileName} -body { testsetplatform unix From 88f91a837a4d4b4bb6248f3f7412219d041af4d7 Mon Sep 17 00:00:00 2001 From: apnadkarni Date: Mon, 25 May 2026 04:26:36 +0000 Subject: [PATCH 2/3] zipfs also supports volume-relative paths and requires a separate check --- generic/tclIOUtil.c | 20 ++++++++++++++++++-- generic/tclPathObj.c | 10 +++++++++- tests/fileName.test | 17 ++++++++++++++--- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/generic/tclIOUtil.c b/generic/tclIOUtil.c index 2b94831f18..2f36be1b65 100644 --- a/generic/tclIOUtil.c +++ b/generic/tclIOUtil.c @@ -3911,7 +3911,7 @@ TclGetPathType( type = TclFSNonnativePathType(path, pathLen, filesystemPtrPtr, driveNameLengthPtr, driveNameRef); - if (type != TCL_PATH_ABSOLUTE) { + if (type == TCL_PATH_RELATIVE) { type = TclpGetNativePathType(pathPtr, driveNameLengthPtr, driveNameRef); /* Bug 1215dca78f - If not relative, need to update owning FS. */ @@ -4031,9 +4031,25 @@ TclFSNonnativePathType( } break; } + if (len > 2 && strVol[len-1] == '/' && strVol[len-2] == ':' && + strncmp(strVol, path, len-2) == 0) { + type = TCL_PATH_VOLUME_RELATIVE; + if (filesystemPtrPtr != NULL) { + *filesystemPtrPtr = fsRecPtr->fsPtr; + } + if (driveNameLengthPtr != NULL) { + *driveNameLengthPtr = len-1; + } + if (driveNameRef != NULL) { + Tcl_SetObjLength(vol, len-1); + *driveNameRef = vol; + Tcl_IncrRefCount(vol); + } + break; + } } Tcl_DecrRefCount(thisFsVolumes); - if (type == TCL_PATH_ABSOLUTE) { + if (type != TCL_PATH_RELATIVE) { /* * No need to examine additional filesystems. */ diff --git a/generic/tclPathObj.c b/generic/tclPathObj.c index 095f5a55b1..31ddc69a2b 100644 --- a/generic/tclPathObj.c +++ b/generic/tclPathObj.c @@ -1081,7 +1081,15 @@ TclJoinPath( } } - if (length > 0 && ptr[length -1] != '/') { + /* + * The check against //zipfs: is required when joining relative + * zipfs paths. For example, [file join c:/ //zipfs:foo]. See + * [1215dca7] or tests filename-9.25.{3,4}. Unfortunately, bit + * of a hack but Tcl lacks VFS abstractions to generalize this. + * Happy to be proven wrong. + */ + if (length > 0 && ptr[length - 1] != '/' && + (length != 8 || strcmp(ptr, "//zipfs:"))) { Tcl_AppendToObj(res, &separator, 1); (void)TclGetStringFromObj(res, &length); } diff --git a/tests/fileName.test b/tests/fileName.test index 827959f96d..96f1846324 100644 --- a/tests/fileName.test +++ b/tests/fileName.test @@ -576,19 +576,30 @@ test filename-9.24 {Tcl_JoinPath: unix} {testsetplatform} { [file join /x /x {foo/bar}] string map [list /x ""] $res } {foo/bar /foo/bar /foo/bar} -test filename-9.25 { +test filename-9.25.1 { Bug 1215dc - Inconsistent file join with volume relative arguments: unix } -constraints testsetplatform -body { testsetplatform unix file join //zipfs:/foo c:bar } -result "//zipfs:/foo/c:bar" -test filename-9.26 { +test filename-9.25.2 { Bug 1215dc - Inconsistent file join with volume relative arguments: win } -constraints testsetplatform -body { testsetplatform win file join //zipfs:/foo c:bar } -result "c:bar" - +test filename-9.25.3 { + Bug 1215dc - Inconsistent file join with volume relative arguments: unix,zipfs +} -constraints testsetplatform -body { + testsetplatform unix + file join / //zipfs:foo +} -result "//zipfs:foo" +test filename-9.25.4 { + Bug 1215dc - Inconsistent file join with volume relative arguments: win,zipfs +} -constraints testsetplatform -body { + testsetplatform win + file join C:\\ //zipfs:foo +} -result "//zipfs:foo" test filename-10.1 {Tcl_TranslateFileName} -body { From 456e957ebe8335a2d29712d7617ccc18bad27dc8 Mon Sep 17 00:00:00 2001 From: apnadkarni Date: Mon, 25 May 2026 09:47:13 +0000 Subject: [PATCH 3/3] Minor refactor --- generic/tclIOUtil.c | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/generic/tclIOUtil.c b/generic/tclIOUtil.c index 2f36be1b65..bc6bee8000 100644 --- a/generic/tclIOUtil.c +++ b/generic/tclIOUtil.c @@ -4010,6 +4010,7 @@ TclFSNonnativePathType( Tcl_Obj *vol; Tcl_Size len; const char *strVol; + bool matched = false; numVolumes--; Tcl_ListObjIndex(NULL, thisFsVolumes, numVolumes, &vol); @@ -4019,6 +4020,16 @@ TclFSNonnativePathType( } if (strncmp(strVol, path, len) == 0) { type = TCL_PATH_ABSOLUTE; + matched = true; + } else if (len > 2 && strVol[len - 1] == '/' && + strVol[len - 2] == ':' && + strncmp(strVol, path, len - 2) == 0) { + matched = true; + type = TCL_PATH_VOLUME_RELATIVE; + len--; + Tcl_SetObjLength(vol, len); + } + if (matched) { if (filesystemPtrPtr != NULL) { *filesystemPtrPtr = fsRecPtr->fsPtr; } @@ -4031,22 +4042,6 @@ TclFSNonnativePathType( } break; } - if (len > 2 && strVol[len-1] == '/' && strVol[len-2] == ':' && - strncmp(strVol, path, len-2) == 0) { - type = TCL_PATH_VOLUME_RELATIVE; - if (filesystemPtrPtr != NULL) { - *filesystemPtrPtr = fsRecPtr->fsPtr; - } - if (driveNameLengthPtr != NULL) { - *driveNameLengthPtr = len-1; - } - if (driveNameRef != NULL) { - Tcl_SetObjLength(vol, len-1); - *driveNameRef = vol; - Tcl_IncrRefCount(vol); - } - break; - } } Tcl_DecrRefCount(thisFsVolumes); if (type != TCL_PATH_RELATIVE) {