test: more checks on % filenames
Massimo Melina committed
Jan 24, 2026 at 14:49 UTC
996c2df362b3c5369cc13ebe0726154e841f14fb
4 files changed
+69
-13
frontend/src/show.ts
+2
-2
@@ -2,7 +2,7 @@ import { DirEntry, DirList, ext2type, state, useSnapState } from './state'
2
import { createElement as h, forwardRef, Fragment, useEffect, useMemo, useRef, useState } from 'react'
3
import {
4
basename, dirname, domOn, hfsEvent, hIcon, isMac, newDialog, pathEncode, restartAnimation, useStateMounted,
5
- isNumeric,
5
+ isNumeric, safeDecodeURIComponent,
6
} from './misc'
7
import { useEventListener, useWindowSize } from 'usehooks-ts'
8
import { EntryDetails, useMidnight } from './BrowseFiles'
@@ -204,7 +204,7 @@ export function fileShow(entry: DirEntry, { startPlaying=false, startShuffle=fal
204
setCover(pathEncode(_.maxBy(covers, 's')?.n || ''))
205
const meta = {
206
title: cur.name,
207
- album: decodeURIComponent(basename(dirname(cur.uri))),
207
+ album: safeDecodeURIComponent(basename(dirname(cur.uri)), ''),
208
artwork: covers.map(x => ({ src: x.n }))
209
}
210
const m = window.MediaMetadata && (navigator.mediaSession.metadata = new MediaMetadata(meta))
src/vfs.ts
+2
-2
@@ -5,7 +5,7 @@ import { basename, dirname, join, resolve } from 'path'
5
import {
6
makeMatcher, setHidden, onlyTruthy, isValidFileName, throw_, VfsPerms, Who, debounceAsync,
7
isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS, removeStarting, HTTP_SERVER_ERROR, try_, matches,
8
- statWithTimeout,
8
+ statWithTimeout, safeDecodeURIComponent,
9
} from './misc'
10
import Koa from 'koa'
11
import _ from 'lodash'
@@ -117,7 +117,7 @@ export async function urlToNode(
117
const slice = url.slice(initialSlashes, nextSlash < 0 ? undefined : nextSlash)
118
if (!slice)
119
return parent
120
- const name = try_(() => decodeURIComponent(slice))
120
+ const name = safeDecodeURIComponent(slice, '')
121
if (!name) // failed decoding
122
return
123
const hasTrailingSlash = url.endsWith('/')
tests/config.yaml
+1
-1
@@ -51,7 +51,7 @@ vfs:
51
source: ../tmp/no-upload
52
can_upload: false
53
- name: cant-overwrite
54
- source: ../tmp/cant-overwrite
54
+ source: ../work/cant-overwrite
55
can_upload:
56
- admins
57
can_delete: false
tests/test.ts
+64
-8
@@ -205,7 +205,7 @@ describe('basics', () => {
205
await Promise.all([rmAny(absPath), rmAny(storedPath)])
206
}
207
})
208
- test('create_folder', reqApi('create_folder', { uri: UPLOAD_ROOT, name: 'temp' }, 401))
208
+ test('create_folder', reqApi('create_folder', { uri: UPLOAD_ROOT, name: UPLOAD_DIR }, 401))
209
test('create_folder.bad type', reqApi('create_folder', { uri: UPLOAD_ROOT, name: 123 }, { status: 400, re: /name/ }))
210
test('delete.no perm', req('/for-admins/', 405, { method: 'delete' }))
211
test('delete.need account', req(UPLOAD_ROOT + 'alfa.txt', 401, { method: 'delete'}))
@@ -254,7 +254,7 @@ describe('accounts', () => {
254
describe('after-login', () => {
255
before(() => login(username))
256
const trickyChars = '%strange#'
257
- test('create_folder', reqApi('create_folder', { uri: UPLOAD_ROOT, name: 'temp' }, 200))
257
+ test('create_folder', reqApi('create_folder', { uri: UPLOAD_ROOT, name: UPLOAD_DIR }, 200))
258
test('create_folder.empty name', reqApi('create_folder', { uri: UPLOAD_ROOT, name: '' }, 409))
259
test('create_folder.tricky chars', async () => {
260
await reqApi('create_folder', { uri: UPLOAD_ROOT, name: trickyChars }, 200)()
@@ -295,6 +295,62 @@ describe('after-login', () => {
295
throwIf(!u?.ip ? 'ip' : u?.username !== username ? 'username' : '')
296
}))
297
test('file_details.non-admin', reqApi('get_file_details', { uris: [UPLOAD_DEST] }, res => res?.details?.[0] === false, { jar: {} }))
298
+ test('percent name apis.details', async () => {
299
+ const percentName = `x%25-${randomId(4)}`
300
+ const percentUri = `${UPLOAD_ROOT}${pathEncode(percentName)}`
301
+ const comment = `note-${randomId(6)}`
302
+ await reqUpload(percentUri, 200)()
303
+ try {
304
+ await reqApi('get_file_details', { uris: [percentUri] }, res => !!res?.details?.[0]?.upload)()
305
+ await reqApi('comment', { uri: percentUri, comment }, 200)()
306
+ await reqApi('get_file_list', { uri: UPLOAD_ROOT }, res => _.find(res?.list, { n: percentName })?.comment === comment)()
307
+ await reqApi('get_folder_size', { uri: percentUri }, 405)()
308
+ }
309
+ finally {
310
+ await req(percentUri, 200, { method: 'delete' })().catch(() => {})
311
+ await rmAny(resolve(__dirname, percentName))
312
+ }
313
+ })
314
+
315
+ test('percent name apis.rename', async () => {
316
+ const percentName = `x%25-${randomId(4)}`
317
+ const percentUri = `${UPLOAD_ROOT}${pathEncode(percentName)}`
318
+ const renameName = `${percentName}-renamed`
319
+ const renamedUri = `${UPLOAD_ROOT}${pathEncode(renameName)}`
320
+ await reqUpload(percentUri, 200)()
321
+ try {
322
+ await reqApi('rename', { uri: percentUri, dest: renameName }, 200)()
323
+ await req(percentUri, 404)()
324
+ await req(renamedUri, 200)()
325
+ }
326
+ finally {
327
+ await req(renamedUri, 200, { method: 'delete' })().catch(() => {})
328
+ await rmAny(resolve(__dirname, renameName))
329
+ }
330
+ })
331
+
332
+ test('percent name apis.move-copy', async () => {
333
+ const percentName = `x%25-${randomId(4)}`
334
+ const percentUri = `${UPLOAD_ROOT}${pathEncode(percentName)}`
335
+ const folderName = `pct-${randomId(6)}`
336
+ const folderUri = `${UPLOAD_ROOT}${folderName}/`
337
+ const movedUri = `${UPLOAD_ROOT}${folderName}/${pathEncode(percentName)}`
338
+ await reqUpload(percentUri, 200)()
339
+ try {
340
+ await reqApi('create_folder', { uri: UPLOAD_ROOT, name: folderName }, 200)()
341
+ await reqApi('move_files', { uri_from: [percentUri], uri_to: folderUri }, res => !res?.errors?.[0])()
342
+ await req(movedUri, 200)()
343
+ await reqApi('copy_files', { uri_from: [movedUri], uri_to: UPLOAD_ROOT }, res => !res?.errors?.[0])()
344
+ await req(percentUri, 200)()
345
+ }
346
+ finally {
347
+ await req(percentUri, 200, { method: 'delete' })().catch(() => {})
348
+ await req(movedUri, 200, { method: 'delete' })().catch(() => {})
349
+ await rmAny(resolve(__dirname, percentName))
350
+ await rmAny(resolve(__dirname, folderName, percentName))
351
+ await rmAny(resolve(__dirname, folderName))
352
+ }
353
+ })
354
test('zip.no-list but archive', req('/zipNoList/?get=zip', 403, { jar: {} }))
355
test('upload but not delete', async () => {
356
const name = `cant-delete`
@@ -349,7 +405,7 @@ describe('after-login', () => {
405
if (after !== before)
406
throw "size changed"
407
})
352
- test('upload.crossing', reqUpload(UPLOAD_DEST.replace('temp', '../..'), 404))
408
+ test('upload.crossing', reqUpload(UPLOAD_DEST.replace(UPLOAD_DIR, '../..'), 404))
409
test('upload.overlap', async () => {
410
const ms = 300
411
const first = reqUpload(UPLOAD_DEST, 200, makeReadableThatTakes(ms))()
@@ -434,15 +490,15 @@ describe('after-login', () => {
490
const res = statfsSync(ROOT)
491
const free = res.bavail * res.bsize
492
const fakeSize = Math.round(free * 0.51)
437
- const r1 = reqUpload(UPLOAD_ROOT + 'temp/free1', 400, makeReadableThatTakes(1000), fakeSize)()
493
+ const r1 = reqUpload(`${UPLOAD_ROOT}${UPLOAD_DIR}/free1`, 400, makeReadableThatTakes(1000), fakeSize)()
494
setTimeout(r1.abort, 1500)
495
await Promise.all([
496
r1.catch(() => {}),
441
- wait(100).then(() => reqUpload(UPLOAD_ROOT + 'temp/free2', 507, makeReadableThatTakes(500), fakeSize)())
497
+ wait(100).then(() => reqUpload(`${UPLOAD_ROOT}${UPLOAD_DIR}/free2`, 507, makeReadableThatTakes(500), fakeSize)())
498
])
499
})
500
test('max_dl.account', async () => {
445
- const uri = UPLOAD_ROOT + 'temp/big'
501
+ const uri = `${UPLOAD_ROOT}${UPLOAD_DIR}/big`
502
await reqUpload(uri, 200, BIG_CONTENT)()
503
await testMaxDl(uri, 2, 1)
504
})
@@ -451,7 +507,7 @@ describe('after-login', () => {
507
await reqApi('logout', {}, 401)()
508
await reqApi('get_accounts', {}, 401)() // no more
509
})
454
- after(() => rmAny(resolve(__dirname, 'temp')))
510
+ after(() => rmAny(resolve(__dirname, UPLOAD_DIR)))
511
})
512
513
describe('admin', () => {
@@ -700,7 +756,7 @@ function throwIf(msg: any) {
756
}
757
758
async function ensureCantOverwriteDir() {
703
- const baseDir = resolve(__dirname, 'tmp', CANT_OVERWRITE_NAME)
759
+ const baseDir = resolve(__dirname, 'work', CANT_OVERWRITE_NAME)
760
await mkdir(baseDir, { recursive: true })
761
return baseDir
762
}