fix(release): harden packaging and filename localization

This commit is contained in:
NimBold
2026-07-30 05:08:26 +03:30
parent 1634118f11
commit 924e108f3c
6 changed files with 41 additions and 5 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ let receivedSignal;
export const APPIMAGE_CONFIG = JSON.stringify({
bundle: {
resources: {
'engine-dist/': null,
'../THIRD_PARTY_NOTICES.md': 'THIRD_PARTY_NOTICES.md',
},
},
});
+2 -2
View File
@@ -2,11 +2,11 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import { APPIMAGE_CONFIG, appImageBundleArguments } from './build-linux-appimage.js';
test('AppImage config removes only engine-dist from Tauri resources', () => {
test('AppImage config keeps notices while excluding the staged engine payload', () => {
assert.deepEqual(JSON.parse(APPIMAGE_CONFIG), {
bundle: {
resources: {
'engine-dist/': null,
'../THIRD_PARTY_NOTICES.md': 'THIRD_PARTY_NOTICES.md',
},
},
});
+8
View File
@@ -58,6 +58,14 @@ describe('date/time formatting', () => {
expect(formatted).toContain('123');
});
it('uses the default Persian date when only locale or timezone options are supplied', () => {
expect(formatDateTime(instant, {
locale: 'fa',
calendar: 'persian',
options: { timeZone: 'UTC' }
})).toBe('۱۴۰۵/۰۱/۰۱');
});
it('returns a safe placeholder for malformed timestamps and rejects unknown preferences', () => {
expect(formatDateTime('not-a-timestamp', { locale: 'en' })).toBe('-');
expect(isCalendarPreference('gregorian')).toBe(true);
+4 -1
View File
@@ -79,7 +79,10 @@ const formatPersianDateTime = (
const hasDateOptions = Object.keys(options).some(key => DATE_OPTION_KEYS.has(key));
const hasTimeOptions = options.timeStyle !== undefined ||
TIME_OPTION_KEYS.some(key => (options as unknown as Record<string, unknown>)[key] !== undefined);
const includeDate = Object.keys(options).length === 0 || hasDateOptions;
// Locale and timezone options do not select a date or time field by
// themselves. Match Intl's default date behavior for those option-only
// calls instead of returning an empty string.
const includeDate = hasDateOptions || !hasTimeOptions;
const parts: string[] = [];
if (includeDate) {
+7
View File
@@ -66,6 +66,13 @@ describe('download connection resolution', () => {
});
describe('download filename matching', () => {
it('matches frontend filenames to the backend Windows device-name canonicalization', () => {
expect(canonicalizeDownloadFileName('CON.txt')).toBe('CON-.txt');
expect(canonicalizeDownloadFileName('com1.archive.zip')).toBe('com1.archive-.zip');
expect(downloadFileNamesMatch('CON-.txt', 'CON.txt')).toBe(true);
expect(downloadFileNamesMatch('console.txt', 'CON.txt')).toBe(false);
});
it('truncates long names by UTF-8 bytes while preserving the extension', () => {
const filename = canonicalizeDownloadFileName(`${'title '.repeat(100)}.mp4`);
+19 -1
View File
@@ -48,6 +48,17 @@ export const DOWNLOAD_CONNECTIONS_MAX = 16;
// bound is also conservative for Windows filename components.
export const MAX_DOWNLOAD_FILENAME_BYTES = 255;
const FILENAME_TRUNCATION_MARKER = '…';
const WINDOWS_RESERVED_FILENAME_STEMS = new Set([
'CON',
'PRN',
'AUX',
'NUL',
'CLOCK$',
'CONIN$',
'CONOUT$',
...Array.from({ length: 9 }, (_, index) => `COM${index + 1}`),
...Array.from({ length: 9 }, (_, index) => `LPT${index + 1}`)
]);
const utf8ByteLength = (value: string): number => new TextEncoder().encode(value).length;
@@ -150,7 +161,14 @@ export const canonicalizeDownloadFileName = (fileName: string): string => {
.replace(/[\u0000-\u001f\u007f-\u009f<>:"/\\|?*]/g, '-')
.trim()
.replace(/[. ]+$/g, '');
const canonical = sanitized && sanitized !== '.' && sanitized !== '..' ? sanitized : 'download';
let canonical = sanitized && sanitized !== '.' && sanitized !== '..' ? sanitized : 'download';
const reservedStem = canonical.split('.')[0]?.toUpperCase();
if (reservedStem && WINDOWS_RESERVED_FILENAME_STEMS.has(reservedStem)) {
const extensionStart = canonical.lastIndexOf('.');
const base = extensionStart > 0 ? canonical.slice(0, extensionStart) : canonical;
const extension = extensionStart > 0 ? canonical.slice(extensionStart) : '';
canonical = `${base}-${extension}`;
}
if (utf8ByteLength(canonical) <= MAX_DOWNLOAD_FILENAME_BYTES) return canonical;
const extensionStart = canonical.lastIndexOf('.');