|
| 1 | +// SPDX-FileCopyrightText: Nextcloud GmbH |
| 2 | +// SPDX-FileCopyrightText: 2025 Milen Pivchev |
| 3 | +// SPDX-License-Identifier: GPL-3.0-or-later |
| 4 | + |
| 5 | +import Testing |
| 6 | +import Foundation |
| 7 | +@testable import NextcloudKit |
| 8 | + |
| 9 | +@Suite(.serialized) struct FileSanitizingUnitTests { |
| 10 | + // MARK: - Helper for test expectation |
| 11 | + func expectedSanitized(for filename: String, isFolder: Bool, isRTL: Bool) -> String { |
| 12 | + let ns = filename as NSString |
| 13 | + let base = ns.deletingPathExtension |
| 14 | + let ext = ns.pathExtension |
| 15 | + |
| 16 | + if isFolder || ext.isEmpty { return base } |
| 17 | + |
| 18 | + let dangerousBidiScalars: Set<UInt32> = [ |
| 19 | + 0x202A, 0x202B, 0x202C, 0x202D, 0x202E, |
| 20 | + 0x200E, 0x200F, 0x2066, 0x2067, 0x2068, |
| 21 | + 0x2069, 0x061C |
| 22 | + ] |
| 23 | + let containsBidi = base.unicodeScalars.contains { dangerousBidiScalars.contains($0.value) } |
| 24 | + |
| 25 | + if isRTL { |
| 26 | + return containsBidi |
| 27 | + ? "\u{202C}\u{2066}.\(ext)\u{2069}" + base |
| 28 | + : ".\(ext)" + base |
| 29 | + } else { |
| 30 | + return containsBidi |
| 31 | + ? base + "\u{202C}\u{2066}.\(ext)\u{2069}" |
| 32 | + : base + "." + ext |
| 33 | + } |
| 34 | + } |
| 35 | + |
| 36 | + // MARK: - Test Cases |
| 37 | + @Test |
| 38 | + func testSanitizeForBidiCharacters_UIRendering() { |
| 39 | + let cases: [(String, Bool, Bool)] = [ |
| 40 | + // LTR, normal and malicious |
| 41 | + ("invoice\u{202E}cod.exe", false, false), // malicious RLO |
| 42 | + ("archive.tar.gz", false, false), // multiple dots |
| 43 | + ("myFolder", true, false), // folder |
| 44 | + ("document.txt", false, false), // normal file |
| 45 | + ("Foo\u{202E}dm.exe", false, false), // another malicious |
| 46 | + |
| 47 | + // RTL Hebrew / Arabic safe |
| 48 | + ("תמונה.jpg", false, true), // Hebrew base |
| 49 | + ("מכתב.pdf", false, true), // Hebrew base |
| 50 | + ("שלום", true, true), // Hebrew folder |
| 51 | + ("مرحبا", true, true), // Arabic folder |
| 52 | + ("ملف.pdf", false, true), // Arabic file |
| 53 | + |
| 54 | + // Mixed-language |
| 55 | + ("report.ملف", false, true), // English base, Arabic extension |
| 56 | + ("وثيقة.docx", false, true), // Arabic base, English extension |
| 57 | + ("summary.תמונה", false, true), // English base, Hebrew extension |
| 58 | + ("מסמך.txt", false, true), // Hebrew base, English extension |
| 59 | + |
| 60 | + // Mixed-language with malicious bidi |
| 61 | + ("report\u{202E}cod.exe", false, true), // English base + RLO trick |
| 62 | + ("ملف\u{202E}cod.exe", false, true), // Arabic base + RLO trick |
| 63 | + ("תמונה\u{202E}cod.exe", false, true) // Hebrew base + RLO trick |
| 64 | + ] |
| 65 | + |
| 66 | + for (filename, isFolder, isRTL) in cases { |
| 67 | + let result = filename.sanitizeForBidiCharacters(isFolder: isFolder, isRTL: isRTL) |
| 68 | + let expected = expectedSanitized(for: filename, isFolder: isFolder, isRTL: isRTL) |
| 69 | + #expect(result == expected, "Failed for filename: \(filename), isFolder: \(isFolder), isRTL: \(isRTL)") |
| 70 | + } |
| 71 | + } |
| 72 | +} |
| 73 | + |
0 commit comments