128 lines
5.6 KiB
TypeScript
128 lines
5.6 KiB
TypeScript
export function safeFilenameDecode(filename: string): string {
|
|
let originalFilename = filename;
|
|
let pos = filename.indexOf('%');
|
|
if (pos < 0) {
|
|
return filename;
|
|
}
|
|
let result = "";
|
|
while (true) {
|
|
if (pos < 0) {
|
|
result += filename;
|
|
return result;
|
|
}
|
|
result += filename.substring(0, pos);
|
|
if (pos+3 >= filename.length) {
|
|
// invalid encoding. just use the original
|
|
return originalFilename;
|
|
}
|
|
let hex = filename.charAt(pos+1) + filename.charAt(pos+2);
|
|
let byte = parseInt(hex, 16);
|
|
|
|
// Handle UTF-8 to UTF-16 conversion
|
|
if (byte < 0x80) {
|
|
// Single-byte character (ASCII)
|
|
result += String.fromCharCode(byte);
|
|
filename = filename.substring(pos+3);
|
|
} else if ((byte & 0xE0) === 0xC0) {
|
|
// 2-byte sequence
|
|
if (pos+6 > filename.length || filename.charAt(pos+3) !== '%') {
|
|
return originalFilename; // invalid encoding
|
|
}
|
|
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
|
let codePoint = ((byte & 0x1F) << 6) | (byte2 & 0x3F);
|
|
result += String.fromCharCode(codePoint);
|
|
filename = filename.substring(pos+6);
|
|
} else if ((byte & 0xF0) === 0xE0) {
|
|
// 3-byte sequence
|
|
if (pos+9 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%') {
|
|
return originalFilename; // invalid encoding
|
|
}
|
|
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
|
let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16);
|
|
let codePoint = ((byte & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);
|
|
result += String.fromCharCode(codePoint);
|
|
filename = filename.substring(pos+9);
|
|
} else if ((byte & 0xF8) === 0xF0) {
|
|
// 4-byte sequence (surrogate pair needed)
|
|
if (pos+12 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%' || filename.charAt(pos+9) !== '%') {
|
|
return originalFilename; // invalid encoding
|
|
}
|
|
let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16);
|
|
let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16);
|
|
let byte4 = parseInt(filename.charAt(pos+10) + filename.charAt(pos+11), 16);
|
|
let codePoint = ((byte & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
|
|
// Convert to UTF-16 surrogate pair
|
|
codePoint -= 0x10000;
|
|
result += String.fromCharCode(0xD800 + (codePoint >> 10));
|
|
result += String.fromCharCode(0xDC00 + (codePoint & 0x3FF));
|
|
filename = filename.substring(pos+12);
|
|
} else {
|
|
// Invalid UTF-8 start byte
|
|
return originalFilename;
|
|
}
|
|
pos = filename.indexOf('%');
|
|
}
|
|
}
|
|
|
|
export function safeFilenameEncode(filename: string): string {
|
|
let result = "";
|
|
|
|
for (let i = 0; i < filename.length; i++) {
|
|
let charCode = filename.charCodeAt(i);
|
|
|
|
// Handle UTF-16 surrogate pairs
|
|
if (charCode >= 0xD800 && charCode <= 0xDBFF) {
|
|
// High surrogate - must be followed by low surrogate
|
|
if (i + 1 < filename.length) {
|
|
let lowSurrogate = filename.charCodeAt(i + 1);
|
|
if (lowSurrogate >= 0xDC00 && lowSurrogate <= 0xDFFF) {
|
|
// Combine surrogates to get the code point
|
|
let codePoint = 0x10000 + ((charCode & 0x3FF) << 10) + (lowSurrogate & 0x3FF);
|
|
|
|
// Encode as 4-byte UTF-8
|
|
let byte1 = 0xF0 | ((codePoint >> 18) & 0x07);
|
|
let byte2 = 0x80 | ((codePoint >> 12) & 0x3F);
|
|
let byte3 = 0x80 | ((codePoint >> 6) & 0x3F);
|
|
let byte4 = 0x80 | (codePoint & 0x3F);
|
|
|
|
result += '%' + byte1.toString(16).padStart(2, '0');
|
|
result += '%' + byte2.toString(16).padStart(2, '0');
|
|
result += '%' + byte3.toString(16).padStart(2, '0');
|
|
result += '%' + byte4.toString(16).padStart(2, '0');
|
|
|
|
i++; // Skip the low surrogate
|
|
continue;
|
|
}
|
|
}
|
|
// Invalid surrogate pair - encode as-is (shouldn't happen in valid strings)
|
|
}
|
|
|
|
// Handle control characters (< 0x20)
|
|
if (charCode < 0x20) {
|
|
result += '%' + charCode.toString(16).padStart(2, '0');
|
|
}
|
|
// Handle ASCII printable characters (0x20-0x7F), space is NOT encoded
|
|
else if (charCode <= 0x7F) {
|
|
result += filename.charAt(i);
|
|
}
|
|
// Handle 2-byte UTF-8 (0x80-0x7FF)
|
|
else if (charCode <= 0x7FF) {
|
|
let byte1 = 0xC0 | ((charCode >> 6) & 0x1F);
|
|
let byte2 = 0x80 | (charCode & 0x3F);
|
|
result += '%' + byte1.toString(16).padStart(2, '0');
|
|
result += '%' + byte2.toString(16).padStart(2, '0');
|
|
}
|
|
// Handle 3-byte UTF-8 (0x800-0xFFFF)
|
|
else {
|
|
let byte1 = 0xE0 | ((charCode >> 12) & 0x0F);
|
|
let byte2 = 0x80 | ((charCode >> 6) & 0x3F);
|
|
let byte3 = 0x80 | (charCode & 0x3F);
|
|
result += '%' + byte1.toString(16).padStart(2, '0');
|
|
result += '%' + byte2.toString(16).padStart(2, '0');
|
|
result += '%' + byte3.toString(16).padStart(2, '0');
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|