Fix 9814 file system paths "pragma: \ define" tiddler (#9815)

* New test for all "forbidden" directory characters

* Fix for 9814 Pragma: \define creates dir problem

* improve handling of tiddlers in root/community directory

* Add ChangeNote

* Apply suggestion from @saqimtiaz

* Apply suggestion from @saqimtiaz

---------

Co-authored-by: Saq Imtiaz <saq.imtiaz@gmail.com>
This commit is contained in:
Mario Pietsch
2026-07-22 13:45:25 +02:00
committed by GitHub
co-authored by Saq Imtiaz
parent 97e27d0dad
commit abf8ffdbca
3 changed files with 113 additions and 3 deletions
+17 -3
View File
@@ -217,6 +217,11 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
fileInfo.isEditableFile = true;
fileInfo.originalpath = options.fileInfo.originalpath;
}
// Propagate the pinFilepath flag (so generateTiddlerFilepath can short-circuit
// FileSystemPaths and write back to the original location)
if(options.fileInfo && !!options.fileInfo.pinFilepath) {
fileInfo.pinFilepath = true;
}
// Check if the tiddler has any unsafe fields that can't be expressed in a .tid or .meta file: containing control characters, or leading/trailing whitespace
var hasUnsafeFields = false;
$tw.utils.each(tiddler.getFieldStrings(),function(value,fieldName) {
@@ -319,9 +324,16 @@ exports.generateTiddlerFilepath = function(title,options) {
extension = options.extension || "",
originalpath = (options.fileInfo && options.fileInfo.originalpath) ? options.fileInfo.originalpath : "",
overwrite = options.fileInfo && options.fileInfo.overwrite || false,
pinFilepath = !!(options.fileInfo && options.fileInfo.pinFilepath),
filepath;
// If the tiddler's filepath is pinned via tiddlywiki.files, the originalpath
// wins and the path filters are skipped entirely.
if(pinFilepath && originalpath) {
var pinnedExt = path.extname(originalpath);
filepath = originalpath.substring(0,originalpath.length - pinnedExt.length);
}
// Check if any of the pathFilters applies
if(options.pathFilters && options.wiki) {
if(!filepath && options.pathFilters && options.wiki) {
$tw.utils.each(options.pathFilters,function(filter) {
if(!filepath) {
var source = options.wiki.makeTiddlerIterator([title]),
@@ -352,8 +364,10 @@ exports.generateTiddlerFilepath = function(title,options) {
}
// Replace any Unicode control codes
filepath = filepath.replace(/[\x00-\x1f\x80-\x9f]/g,"_");
// Replace any characters that can't be used in cross-platform filenames
filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^/g,"_"));
// Replace any characters that can't be used in cross-platform filenames.
if(!pinFilepath) {
filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^|\\/g,"_"));
}
// Replace any dots or spaces at the end of the extension with the same number of underscores
extension = extension.replace(/[\. ]+$/, function (u) { return u.replace(/[\. ]/g, "_");});
// Truncate the extension if it is too long
@@ -0,0 +1,83 @@
/*\
title: test-filesystem.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests for $:/core-server filesystem utilities.
\*/
"use strict";
if($tw.node) {
var path = require("path");
describe("generateTiddlerFilepath", function() {
var directory = path.resolve("/tmp/tw5-test-filesystem");
// Characters stripped by the cross-platform-filename regex at
// core-server/filesystem.js:356. Each entry is [char, description].
// The forbidden set includes all characters disallowed by Windows
// (< > : " | ? *), backslash (directory separator on Windows),
// tilde (legacy 8.3 short-name marker), and caret (disallowed in
// some shell/FS contexts).
var forbiddenChars = [
["<","less-than"],
[">","greater-than"],
["~","tilde"],
[":","colon"],
["\"","double-quote"],
["|","pipe"],
["?","question-mark"],
["*","asterisk"],
["^","caret"],
["\\","backslash"]
];
// Use originalpath so we exercise the line-356 regex directly.
// The title-branch at line ~342 pre-strips "/" and "\" before the
// main regex runs, which would mask the backslash case.
function filepathFromOriginalpath(title,extension) {
return $tw.utils.generateTiddlerFilepath(title,{
extension: extension,
directory: directory,
fileInfo: {
overwrite: true,
originalpath: title + extension
}
});
}
forbiddenChars.forEach(function(entry) {
var char = entry[0], name = entry[1];
it("should replace " + name + " (" + char + ") with underscore", function() {
var result = filepathFromOriginalpath("a" + char + "b",".tid");
expect(path.dirname(result)).toBe(directory);
expect(path.basename(result)).toBe("a_b.tid");
});
});
it("should replace every forbidden char in a dense string", function() {
// Prefix with "x" so the sanitized result isn't all underscores,
// which would trigger the charcode-fallback branch at line ~371.
var title = "x" + forbiddenChars.map(function(e) { return e[0]; }).join("");
var expected = "x" + forbiddenChars.map(function() { return "_"; }).join("");
var result = filepathFromOriginalpath(title,".tid");
expect(path.dirname(result)).toBe(directory);
expect(path.basename(result)).toBe(expected + ".tid");
});
it("should sanitize real-world 'Pragma: \\define' title without creating a subdirectory", function() {
// Issue for editions/tw5.com titles like "Pragma: \define".
// Before the fix, backslash survived sanitization and path.resolve
// treated it as a separator on Windows, producing a "Pragma_ "
// subdir containing "define.tid".
var result = filepathFromOriginalpath("Pragma: \\define",".tid");
expect(path.dirname(result)).toBe(directory);
expect(path.basename(result)).toBe("Pragma_ _define.tid");
});
});
}
@@ -0,0 +1,13 @@
change-category: nodejs
change-type: bugfix
created: 20260711175727000
description: Tiddler titles containing a backslash no longer break generated file paths
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9815
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9815
type: text/vnd.tiddlywiki
* Titles containing a backslash, such as `pragma: \define`, get the backslash replaced like other forbidden filename characters instead of it acting as a Windows path separator (fixes [[Issue #9814|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9814]])
* Tiddler files declared in `tiddlywiki.files` are pinned to their original location and skip the `$:/config/FileSystemPaths` filters and filename sanitising