Files
op-pedal/vite/src/pipedal/SafeFilename.tsx
T
2026-02-10 19:59:05 -05:00

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;
}