using System; using System.IO; using Paca.Core.Platform; using Paca.Core.Util; using Xunit; namespace Paca.Tests.Core; /// /// Cycle 1: 극한 RED 시나리오 - 파일명 정제 및 윈도우 장치 예약어, 트로이목마 Bidi 스푸핑, 경로 누수 방어 검증 /// public class FileNameUtilAndCleanupExtremeRedTests { [Theory] [InlineData("CON_long_stream_title", 3, "_CON")] [InlineData("PRN_document_capture", 3, "_PRN")] [InlineData("AUX_recording_session", 3, "_AUX")] [InlineData("NUL_blackhole_stream", 3, "_NUL")] [InlineData("COM1_serial_diagnostic", 4, "_COM1")] [InlineData("LPT1_parallel_port", 4, "_LPT1")] [InlineData("con.something.video", 3, "_con")] public void Cycle1_Gate1_TruncatedToReservedName_MustPrependUnderscore(string input, int maxLen, string expectedPrefix) { // Act var sanitized = FileNameUtil.Sanitize(input, maxLen); // Assert: 윈도우 장치 예약어(CON, PRN, AUX, NUL, COM1-9, LPT1-9)와 일치해서는 안 됨 Assert.StartsWith(expectedPrefix, sanitized, StringComparison.OrdinalIgnoreCase); Assert.DoesNotMatch(@"^(?i)(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$", sanitized); } [Fact] public void Cycle1_Gate2_BidiTrojanSourceAndZeroWidth_MustBeSanitized() { // CVE 클래스: Trojan Source / Unicode Bidi Spoofing // \u202E (Right-to-Left Override): "video\u202E4pm.exe" -> 화면에는 "videoexe.mp4" 로 착시 유도 var trojanBidi = "my_video\u202E4pm.exe"; var sanitizedBidi = FileNameUtil.Sanitize(trojanBidi); Assert.DoesNotContain('\u202E', sanitizedBidi); Assert.DoesNotContain('\u202A', sanitizedBidi); Assert.DoesNotContain('\u202B', sanitizedBidi); Assert.DoesNotContain('\u202C', sanitizedBidi); Assert.DoesNotContain('\u202D', sanitizedBidi); // 제로 너비 공백(\u200B), BOM(\uFEFF) 등 인비저블 캐릭터 제거 var zeroWidth = "video\u200B\u200C\u200D\uFEFFtitle"; var sanitizedZero = FileNameUtil.Sanitize(zeroWidth); Assert.DoesNotContain('\u200B', sanitizedZero); Assert.DoesNotContain('\u200C', sanitizedZero); Assert.DoesNotContain('\u200D', sanitizedZero); Assert.DoesNotContain('\uFEFF', sanitizedZero); } [Theory] [InlineData("............", 5, "video")] [InlineData(" ", 8, "video")] [InlineData("valid_name", 0, "video")] [InlineData("valid_name", -5, "video")] [InlineData("...", 1, "video")] public void Cycle1_Gate3_DegenerateMaxLenAndDotSpaceCollapse_MustFallbackToVideo(string input, int maxLen, string expected) { // Act var result = FileNameUtil.Sanitize(input, maxLen); // Assert: 빈 문자열이나 ArgumentOutOfRangeException 없이 안전한 기본값 반환 Assert.Equal(expected, result); } [Fact] public void Cycle1_Gate4_DownloadMetaPathResolver_TempAndParts_MustBeInPacaHiddenDirectory() { var tempRoot = Path.Combine(Path.GetTempPath(), "paca_test_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempRoot); try { var outputBase = Path.Combine(tempRoot, "sample_video"); var partsDir = DownloadMetaPathResolver.GetPartsDirectory(outputBase); var tempPartFile = DownloadMetaPathResolver.GetTempPartFilePath(outputBase); // .paca 격리 서브디렉터리 내부에 생성되어야 함 Assert.Contains(".paca", partsDir); Assert.Contains(".paca", tempPartFile); Assert.EndsWith("sample_video.parts", partsDir); Assert.EndsWith("sample_video.part", tempPartFile); } finally { try { if (Directory.Exists(tempRoot)) Directory.Delete(tempRoot, true); } catch { } } } }