92 lines
4.1 KiB
C#
92 lines
4.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Paca.Core.Platform;
|
|
using Xunit;
|
|
|
|
namespace Paca.Tests.Core;
|
|
|
|
/// <summary>
|
|
/// Cycle 3: 극한 RED 시나리오 - 인플레이스 덮어쓰기 원본 파괴 방어, FFmpeg 필터 인젝션 이스케이프, 메타데이터 문법 오염 차단
|
|
/// </summary>
|
|
public class MediaProcessingExtremeRedTests
|
|
{
|
|
private sealed class DummyProcessRunner : IProcessRunner
|
|
{
|
|
public Task<ProcessResult> RunAsync(ProcessSpec spec, Action<string, string>? 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<ArgumentException>(() =>
|
|
trimmer.TrimLosslessAsync("ffmpeg.exe", path, path, 0, 10, default));
|
|
Assert.Contains("동일", exTrimmer.Message);
|
|
|
|
// 2. 오디오 정규화기가 동일 경로에 덮어쓰기 시 ArgumentException 발생
|
|
var exNormalizer = await Assert.ThrowsAsync<ArgumentException>(() =>
|
|
normalizer.NormalizeAudioAsync("ffmpeg.exe", path, path, LoudnessSpec.WebStandard, default));
|
|
Assert.Contains("동일", exNormalizer.Message);
|
|
|
|
// 3. 워터마크 버너가 동일 경로에 덮어쓰기 시 ArgumentException 발생
|
|
var exBurner = await Assert.ThrowsAsync<ArgumentException>(() =>
|
|
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<ChapterInfo>
|
|
{
|
|
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);
|
|
}
|
|
}
|