From 4d8e2a12798708f1c4688562d29667ed980cc92e Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 13 Dec 2022 17:46:18 -0600 Subject: [PATCH] Update:Max filename to 255 bytes in utf-16 #1261 --- client/plugins/init.client.js | 51 ++++++++++++++++++++++------------- server/utils/fileUtils.js | 46 ++++++++++++++++++++----------- 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js index bc185c5f..b65ab155 100644 --- a/client/plugins/init.client.js +++ b/client/plugins/init.client.js @@ -30,23 +30,26 @@ Vue.prototype.$addDaysToDate = (jsdate, daysToAdd) => { return date } -Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => { - if (typeof input !== 'string') { +Vue.prototype.$sanitizeFilename = (filename, colonReplacement = ' - ') => { + if (typeof filename !== 'string') { return false } - // Max is actually 255-260 for windows but this leaves padding incase ext wasnt put on yet - const MAX_FILENAME_LEN = 240 + // Most file systems use number of bytes for max filename + // to support most filesystems we will use max of 255 bytes in utf-16 + // Ref: https://doc.owncloud.com/server/next/admin_manual/troubleshooting/path_filename_length.html + // Issue: https://github.com/advplyr/audiobookshelf/issues/1261 + const MAX_FILENAME_BYTES = 255 - var replacement = '' - var illegalRe = /[\/\?<>\\:\*\|"]/g - var controlRe = /[\x00-\x1f\x80-\x9f]/g - var reservedRe = /^\.+$/ - var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i - var windowsTrailingRe = /[\. ]+$/ - var lineBreaks = /[\n\r]/g + const replacement = '' + const illegalRe = /[\/\?<>\\:\*\|"]/g + const controlRe = /[\x00-\x1f\x80-\x9f]/g + const reservedRe = /^\.+$/ + const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i + const windowsTrailingRe = /[\. ]+$/ + const lineBreaks = /[\n\r]/g - var sanitized = input + sanitized = filename .replace(':', colonReplacement) // Replace first occurrence of a colon .replace(illegalRe, replacement) .replace(controlRe, replacement) @@ -55,13 +58,25 @@ Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => { .replace(windowsReservedRe, replacement) .replace(windowsTrailingRe, replacement) + // Check if basename is too many bytes + const ext = Path.extname(sanitized) // separate out file extension + const basename = Path.basename(sanitized, ext) + const extByteLength = Buffer.byteLength(ext, 'utf16le') + const basenameByteLength = Buffer.byteLength(basename, 'utf16le') + if (basenameByteLength + extByteLength > MAX_FILENAME_BYTES) { + const MaxBytesForBasename = MAX_FILENAME_BYTES - extByteLength + let totalBytes = 0 + let trimmedBasename = '' - if (sanitized.length > MAX_FILENAME_LEN) { - var lenToRemove = sanitized.length - MAX_FILENAME_LEN - var ext = Path.extname(sanitized) - var basename = Path.basename(sanitized, ext) - basename = basename.slice(0, basename.length - lenToRemove) - sanitized = basename + ext + // Add chars until max bytes is reached + for (const char of basename) { + totalBytes += Buffer.byteLength(char, 'utf16le') + if (totalBytes > MaxBytesForBasename) break + else trimmedBasename += char + } + + trimmedBasename = trimmedBasename.trim() + sanitized = trimmedBasename + ext } return sanitized diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 51f07787..2ce7e0d2 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -183,16 +183,19 @@ module.exports.sanitizeFilename = (filename, colonReplacement = ' - ') => { return false } - // Max is actually 255-260 for windows but this leaves padding incase ext wasnt put on yet - const MAX_FILENAME_LEN = 240 + // Most file systems use number of bytes for max filename + // to support most filesystems we will use max of 255 bytes in utf-16 + // Ref: https://doc.owncloud.com/server/next/admin_manual/troubleshooting/path_filename_length.html + // Issue: https://github.com/advplyr/audiobookshelf/issues/1261 + const MAX_FILENAME_BYTES = 255 - var replacement = '' - var illegalRe = /[\/\?<>\\:\*\|"]/g - var controlRe = /[\x00-\x1f\x80-\x9f]/g - var reservedRe = /^\.+$/ - var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i - var windowsTrailingRe = /[\. ]+$/ - var lineBreaks = /[\n\r]/g + const replacement = '' + const illegalRe = /[\/\?<>\\:\*\|"]/g + const controlRe = /[\x00-\x1f\x80-\x9f]/g + const reservedRe = /^\.+$/ + const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i + const windowsTrailingRe = /[\. ]+$/ + const lineBreaks = /[\n\r]/g sanitized = filename .replace(':', colonReplacement) // Replace first occurrence of a colon @@ -203,12 +206,25 @@ module.exports.sanitizeFilename = (filename, colonReplacement = ' - ') => { .replace(windowsReservedRe, replacement) .replace(windowsTrailingRe, replacement) - if (sanitized.length > MAX_FILENAME_LEN) { - var lenToRemove = sanitized.length - MAX_FILENAME_LEN - var ext = Path.extname(sanitized) - var basename = Path.basename(sanitized, ext) - basename = basename.slice(0, basename.length - lenToRemove) - sanitized = basename + ext + // Check if basename is too many bytes + const ext = Path.extname(sanitized) // separate out file extension + const basename = Path.basename(sanitized, ext) + const extByteLength = Buffer.byteLength(ext, 'utf16le') + const basenameByteLength = Buffer.byteLength(basename, 'utf16le') + if (basenameByteLength + extByteLength > MAX_FILENAME_BYTES) { + const MaxBytesForBasename = MAX_FILENAME_BYTES - extByteLength + let totalBytes = 0 + let trimmedBasename = '' + + // Add chars until max bytes is reached + for (const char of basename) { + totalBytes += Buffer.byteLength(char, 'utf16le') + if (totalBytes > MaxBytesForBasename) break + else trimmedBasename += char + } + + trimmedBasename = trimmedBasename.trim() + sanitized = trimmedBasename + ext } return sanitized