using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Paca.Core.Platform; using Xunit; namespace Paca.Tests.Core; /// /// Cycle 3: 극한 RED 시나리오 - 인플레이스 덮어쓰기 원본 파괴 방어, FFmpeg 필터 인젝션 이스케이프, 메타데이터 문법 오염 차단 /// public class MediaProcessingExtremeRedTests { private sealed class DummyProcessRunner : IProcessRunner { public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) => Task.FromResult(new ProcessResult(0, "", "")); } [Fact] public async Task Cycle3_Gate1_InPlaceOverwrite_MustThrowArgumentException_ToPreventSourceDestruction() { var runner = new DummyProcessRunner(); var trimmer = new LosslessSmartTrimmer(runner); var normalizer = new AudioNormalizer(runner); var burner = new VideoWatermarkBurner(runner); var path = "C:/media/target_video.mp4"; // 1. 트리머가 동일 경로에 덮어쓰기를 시도하면 원본 파괴(0바이트 절단) 방지를 위해 ArgumentException 발생 var exTrimmer = await Assert.ThrowsAsync(() => trimmer.TrimLosslessAsync("ffmpeg.exe", path, path, 0, 10, default)); Assert.Contains("동일", exTrimmer.Message); // 2. 오디오 정규화기가 동일 경로에 덮어쓰기 시 ArgumentException 발생 var exNormalizer = await Assert.ThrowsAsync(() => normalizer.NormalizeAudioAsync("ffmpeg.exe", path, path, LoudnessSpec.WebStandard, default)); Assert.Contains("동일", exNormalizer.Message); // 3. 워터마크 버너가 동일 경로에 덮어쓰기 시 ArgumentException 발생 var exBurner = await Assert.ThrowsAsync(() => burner.ApplyWatermarkAsync("ffmpeg.exe", path, path, new WatermarkSpec("wm.png", WatermarkPosition.TopLeft), default)); Assert.Contains("동일", exBurner.Message); } [Fact] public void Cycle3_Gate2_DrawTextFilter_EscapesSingleQuotesColonsAndBackslashes() { // FFmpeg filtergraph syntax: drawtext filter requires escaping for ', :, \, % var dangerousText = "It's a: sample\\path %date%"; var filter = VideoWatermarkBurner.BuildDrawTextFilter(dangerousText); // 텍스트 인젝션 방어: 따옴표, 콜론, 백슬래시, 퍼센트가 안전하게 이스케이프되었는지 검증 Assert.DoesNotContain("text='It's", filter); // 원시 따옴표 탈출 불가 Assert.Contains(@"\'", filter); // 따옴표 이스케이프 Assert.Contains(@"\:", filter); // 콜론 이스케이프 Assert.Contains(@"\\", filter); // 백슬래시 이스케이프 Assert.Contains("%%", filter); // 퍼센트 매크로 이스케이프 } [Fact] public void Cycle3_Gate3_ChapterSplitter_GenerateFfmetadata_EscapesSpecialCharacters() { // FFMETADATA1 포맷 규격: =, ;, #, \, 개행은 백슬래시로 이스케이프되어야 함 var chapters = new List { new(1, @"Part#1; Season=2 \ Special" + "\n" + "Subtitle", 0.0, 10.0) }; var metadata = ChapterSplitter.GenerateFfmetadata(chapters, @"Main=Title; #1 \ Top"); // FFMETADATA1 헤더 검증 Assert.StartsWith(";FFMETADATA1", metadata); // 특수문자 이스케이프 검증 Assert.Contains(@"Main\=Title\; \#1 \\ Top", metadata); Assert.Contains(@"Part\#1\; Season\=2 \\ Special", metadata); } [Theory] [InlineData(-10.5, "00:00:00.000")] [InlineData(-0.001, "00:00:00.000")] [InlineData(double.NaN, "00:00:00.000")] [InlineData(double.PositiveInfinity, "99:59:59.999")] [InlineData(3661.123, "01:01:01.123")] public void Cycle3_Gate4_VoiceActivitySubtitleAdapter_FormatTimecode_ClampsNegativeAndHandlesDegenerate(double inputSec, string expected) { var result = VoiceActivitySubtitleAdapter.FormatTimecode(inputSec); Assert.Equal(expected, result); } }