mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
Sanitize sync backup filenames for remote upload
This commit is contained in:
66
bme-sync.js
66
bme-sync.js
@@ -1,5 +1,6 @@
|
||||
const BME_SYNC_FILE_PREFIX = "ST-BME_sync_";
|
||||
const BME_SYNC_FILE_SUFFIX = ".json";
|
||||
const BME_SYNC_FILENAME_MAX_LENGTH = 180;
|
||||
|
||||
export const BME_SYNC_DEVICE_ID_KEY = "st_bme_sync_device_id_v1";
|
||||
export const BME_SYNC_UPLOAD_DEBOUNCE_MS = 2500;
|
||||
@@ -23,6 +24,54 @@ function normalizeChatId(chatId) {
|
||||
return String(chatId ?? "").trim();
|
||||
}
|
||||
|
||||
function createStableFilenameHash(input = "") {
|
||||
let hash = 2166136261;
|
||||
const normalized = String(input ?? "");
|
||||
for (let index = 0; index < normalized.length; index++) {
|
||||
hash ^= normalized.charCodeAt(index);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return (hash >>> 0).toString(36);
|
||||
}
|
||||
|
||||
function normalizeRemoteFilenameCandidate(fileName, fallbackValue = "ST-BME_sync_unknown.json") {
|
||||
const raw = String(fileName ?? "");
|
||||
const normalized = typeof raw.normalize === "function" ? raw.normalize("NFKD") : raw;
|
||||
const sanitized = normalized
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^A-Za-z0-9._~-]+/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^\.+/g, "")
|
||||
.slice(0, BME_SYNC_FILENAME_MAX_LENGTH)
|
||||
.trim();
|
||||
return sanitized || fallbackValue;
|
||||
}
|
||||
|
||||
function buildSyncFilename(chatId) {
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
const legacyName = `${BME_SYNC_FILE_PREFIX}${normalizedChatId}${BME_SYNC_FILE_SUFFIX}`;
|
||||
if (
|
||||
normalizedChatId
|
||||
&& /^[A-Za-z0-9._~-]+$/.test(normalizedChatId)
|
||||
&& legacyName.length <= BME_SYNC_FILENAME_MAX_LENGTH
|
||||
) {
|
||||
return legacyName;
|
||||
}
|
||||
|
||||
const hash = createStableFilenameHash(normalizedChatId || "unknown");
|
||||
const rawSlug = normalizeRemoteFilenameCandidate(normalizedChatId, "");
|
||||
const suffixPart = `~${hash}${BME_SYNC_FILE_SUFFIX}`;
|
||||
const maxSlugLength = Math.max(
|
||||
0,
|
||||
BME_SYNC_FILENAME_MAX_LENGTH - BME_SYNC_FILE_PREFIX.length - suffixPart.length,
|
||||
);
|
||||
const safeSlug = rawSlug.slice(0, maxSlugLength).replace(/^[_~.-]+|[_~.-]+$/g, "");
|
||||
const core = safeSlug
|
||||
? `${BME_SYNC_FILE_PREFIX}${safeSlug}~${hash}`
|
||||
: `${BME_SYNC_FILE_PREFIX}${hash}`;
|
||||
return `${core}${BME_SYNC_FILE_SUFFIX}`;
|
||||
}
|
||||
|
||||
function normalizeRevision(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) return 0;
|
||||
@@ -856,13 +905,10 @@ async function invokeSyncAppliedHook(options = {}, payload = {}) {
|
||||
}
|
||||
|
||||
async function sanitizeFilename(fileName, options = {}) {
|
||||
const fallbackSanitized = String(fileName || "")
|
||||
.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_")
|
||||
.replace(/\s+/g, "_")
|
||||
.replace(/^\.+/g, "")
|
||||
.slice(0, 180);
|
||||
|
||||
const finalFallback = fallbackSanitized || "ST-BME_sync_unknown.json";
|
||||
const finalFallback = normalizeRemoteFilenameCandidate(
|
||||
fileName,
|
||||
"ST-BME_sync_unknown.json",
|
||||
);
|
||||
|
||||
if (options.disableRemoteSanitize) {
|
||||
return finalFallback;
|
||||
@@ -885,7 +931,7 @@ async function sanitizeFilename(fileName, options = {}) {
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
const sanitized = String(payload?.fileName || "").trim();
|
||||
return sanitized || finalFallback;
|
||||
return normalizeRemoteFilenameCandidate(sanitized, finalFallback);
|
||||
} catch {
|
||||
return finalFallback;
|
||||
}
|
||||
@@ -901,9 +947,9 @@ async function resolveSyncFilename(chatId, options = {}) {
|
||||
return sanitizedFilenameByChatId.get(normalizedChatId);
|
||||
}
|
||||
|
||||
const rawFileName = `${BME_SYNC_FILE_PREFIX}${normalizedChatId}${BME_SYNC_FILE_SUFFIX}`;
|
||||
const rawFileName = buildSyncFilename(normalizedChatId);
|
||||
const sanitized = await sanitizeFilename(rawFileName, options);
|
||||
const finalName = sanitized || rawFileName;
|
||||
const finalName = normalizeRemoteFilenameCandidate(sanitized, rawFileName);
|
||||
sanitizedFilenameByChatId.set(normalizedChatId, finalName);
|
||||
return finalName;
|
||||
}
|
||||
|
||||
@@ -141,6 +141,12 @@ function createMockFetchEnvironment() {
|
||||
if (url === "/api/files/upload" && method === "POST") {
|
||||
logs.uploadCalls += 1;
|
||||
const body = JSON.parse(String(options.body || "{}"));
|
||||
if (!/^[A-Za-z0-9._~-]+$/.test(String(body.name || ""))) {
|
||||
return createJsonResponse(
|
||||
400,
|
||||
"Illegal character in filename; only alphanumeric, '-', '_', '.', '~' are accepted.",
|
||||
);
|
||||
}
|
||||
const decoded = __testOnlyDecodeBase64Utf8(body.data);
|
||||
const payload = JSON.parse(decoded);
|
||||
remoteFiles.set(body.name, payload);
|
||||
@@ -286,6 +292,21 @@ async function testUploadPayloadMetaFirstAndDebounce() {
|
||||
assert.equal(logs.uploadCalls, 2);
|
||||
}
|
||||
|
||||
async function testUploadSanitizesIllegalChatIdFilename() {
|
||||
const { fetch, logs } = createMockFetchEnvironment();
|
||||
const dbByChatId = new Map();
|
||||
const chatId = "世界书 测试(chat)#1";
|
||||
dbByChatId.set(chatId, new FakeDb(chatId));
|
||||
|
||||
const runtime = buildRuntimeOptions({ dbByChatId, fetch });
|
||||
const uploadResult = await upload(chatId, runtime);
|
||||
|
||||
assert.equal(uploadResult.uploaded, true);
|
||||
assert.equal(logs.uploadCalls, 1);
|
||||
assert.match(uploadResult.filename, /^ST-BME_sync_[A-Za-z0-9._~-]+\.json$/);
|
||||
assert.match(logs.uploadedPayloads[0].name, /^[A-Za-z0-9._~-]+$/);
|
||||
}
|
||||
|
||||
async function testDownloadImport() {
|
||||
const { fetch, remoteFiles } = createMockFetchEnvironment();
|
||||
const dbByChatId = new Map();
|
||||
@@ -725,6 +746,7 @@ async function main() {
|
||||
await testDeviceId();
|
||||
await testRemoteStatusMissing();
|
||||
await testUploadPayloadMetaFirstAndDebounce();
|
||||
await testUploadSanitizesIllegalChatIdFilename();
|
||||
await testDownloadImport();
|
||||
await testMergeRules();
|
||||
await testMergeRuntimeMetaPolicies();
|
||||
|
||||
Reference in New Issue
Block a user