DodoTransfer/lib/utils.js
2026-11-05 10:28:10 +09:00

97 lines
3.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const path = require('path');
// Control chars incl. NUL. Keep it strict and cross-platform.
const CONTROL_CHARS_RE = /[\u0000-\u001F\u007F]/g;
function toSafeBasename(input, fallback = 'file', opts = {}) {
const maxLength = Number.isInteger(opts.maxLength) ? opts.maxLength : 255;
let name = '';
if (typeof input === 'string') name = input;
else if (input == null) name = '';
else name = String(input);
// Remove NUL/control chars early (these can truncate paths in some tooling).
name = name.replace(CONTROL_CHARS_RE, '');
// Normalize separators to POSIX then force basename (flat archives).
name = name.replace(/\\/g, '/');
name = path.posix.basename(name);
// Trim to avoid whitespace-only names; keep the caller’s original if they need strict equality.
name = name.trim();
// Disallow dot/dotdot and empty.
if (!name || name === '.' || name === '..') name = '';
// Length cap.
if (name.length > maxLength) name = name.slice(0, maxLength);
if (!name) {
const fb = (fallback == null ? '' : String(fallback)).replace(CONTROL_CHARS_RE, '').trim();
return fb ? fb.slice(0, maxLength) : 'file';
}
return name;
}
function isSafeBasename(name, opts = {}) {
if (typeof name !== 'string') return false;
return toSafeBasename(name, '', opts) === name;
}
// Node randomUUID() v4 shape (case-insensitive).
const UUID_V4_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
/**
* Tus resume URL id: exactly one "++", safe bucket sid, UUID key.
*/
function isSafeTusUploadId(uploadId) {
if (typeof uploadId !== 'string' || uploadId.includes('\0')) return false;
const parts = uploadId.split('++');
if (parts.length !== 2) return false;
const [sid, key] = parts;
if (!sid || !key) return false;
return isSafeBasename(sid) && UUID_V4_RE.test(key);
}
/**
* Bucket-only id (lock PATCH, middleware checks without "++").
*/
function isSafeBucketFid(fid) {
if (typeof fid !== 'string' || fid.includes('++') || fid.includes('\0')) return false;
return isSafeBasename(fid);
}
function toAsciiFallbackFilename(name, fallback = 'file') {
const safe = toSafeBasename(name, fallback);
const normalized = safe.normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
const ascii = normalized.replace(/[^\x20-\x7E]+/g, '').trim();
const cleaned = ascii.replace(/["\\]/g, '_');
if (cleaned) return cleaned;
const fallbackSafe = toSafeBasename(fallback, 'file');
return fallbackSafe.replace(/[^\x20-\x7E]+/g, '').trim() || 'file';
}
/** Content-Disposition value with an ASCII fallback and the UTF-8 name (RFC 5987). */
function contentDisposition(name, fallback = 'file', type = 'attachment') {
const safe = toSafeBasename(name, fallback);
const asciiFallback = toAsciiFallbackFilename(safe, fallback);
const encoded = encodeURIComponent(safe)
.replace(/['()]/g, c => `%${ c.charCodeAt(0).toString(16).toUpperCase() }`)
.replace(/\*/g, '%2A');
return `${ type }; filename="${ asciiFallback }"; filename*=UTF-8''${ encoded }`;
}
module.exports = {
contentDisposition,
toSafeBasename,
isSafeBasename,
isSafeTusUploadId,
isSafeBucketFid,
UUID_V4_RE,
};