using System; using System.IO; using System.Security.Cryptography; using Paca.Core.Config; using Paca.Core.Dash; using Paca.Core.Hls; using Paca.Core.Models; using Xunit; namespace Paca.Tests.Core; /// /// Cycle 2: 극한 RED 시나리오 - HLS AES-128 암호화 IV/키 경계값 패딩, 블록 크기 손상 예외 정밀화, DASH 매니페스트 퍼징 검증 /// public class HlsAndDashResilienceExtremeRedTests { [Fact] public void Cycle2_Gate1_ResolveIv_ShortHex_PadsTo16BytesBigEndian() { // RFC 8216: IV 속성은 128비트(16바이트) 빅엔디언 정수 16진수 표현 // 0x1 과 같이 축약된 hex 가 주어졌을 때 1바이트가 아닌 16바이트로 좌측 0 패딩되어야 함 var keyInfo = new KeyInfo(new Uri("https://example.com/key.bin"), "0x1"); var iv = HlsDownloader.ResolveIv(keyInfo, 100); Assert.Equal(16, iv.Length); for (int i = 0; i < 15; i++) { Assert.Equal(0, iv[i]); } Assert.Equal(1, iv[15]); // 3바이트 hex (6글자) var keyHex6 = new KeyInfo(new Uri("https://example.com/key.bin"), "0xABCDEF"); var iv6 = HlsDownloader.ResolveIv(keyHex6, 100); Assert.Equal(16, iv6.Length); Assert.Equal(0xAB, iv6[13]); Assert.Equal(0xCD, iv6[14]); Assert.Equal(0xEF, iv6[15]); // 잘못된 비-16진수("0xZZZZ") 주입 시 크래시 없이 시퀀스 번호(100)로 안전 폴백 var malformedKey = new KeyInfo(new Uri("https://example.com/key.bin"), "0xZZZZ"); var ivFallback = HlsDownloader.ResolveIv(malformedKey, 100); Assert.Equal(16, ivFallback.Length); Assert.Equal(100, ivFallback[15]); } [Fact] public void Cycle2_Gate2_DecryptSegment_InvalidKeyOrCorruptedBlock_ThrowsInvalidDataException() { var config = new AppConfig(); var downloader = new HlsDownloader(new System.Net.Http.HttpClient(), config); var keyUri = new Uri("https://example.com/aes.key"); var seg = new Segment(new Uri("https://example.com/seg1.ts"), 0, 1, new KeyInfo(keyUri, "0x1")); // 1. 비정상 키 길이 (16바이트가 아닌 10바이트 등) -> InvalidDataException 발생 확인 downloader.SetPreloadedKeyForTesting(keyUri, new byte[10]); var exKey = Assert.Throws(() => downloader.DecryptSegmentForTesting(new byte[16], seg)); Assert.Contains("16바이트", exKey.Message); // 2. 비정상 데이터 블록 크기 (AES CBC는 16의 배수여야 함, 15바이트 주입) -> InvalidDataException 발생 확인 downloader.SetPreloadedKeyForTesting(keyUri, new byte[16]); var exData = Assert.Throws(() => downloader.DecryptSegmentForTesting(new byte[15], seg)); Assert.Contains("블록 크기", exData.Message); } [Fact] public void Cycle2_Gate3_DashParser_TimescaleZeroOrNegative_DefaultsToOne() { var mpdXml = """ """; var manifest = DashParser.Parse(mpdXml, new Uri("https://cdn.example.com/manifest.mpd")); Assert.NotNull(manifest); Assert.NotNull(manifest.Video); // timescale="0"이어도 0으로 나누기 예외 없이 1로 폴백되어 5개 세그먼트 생성 Assert.NotEmpty(manifest.Video.Segments); } [Theory] [InlineData("P1D", 86400.0)] [InlineData("P1DT2H", 93600.0)] [InlineData("PT1H30M", 5400.0)] [InlineData("PT1.5S", 1.5)] [InlineData("", 0.0)] [InlineData("INVALID_DURATION", 0.0)] public void Cycle2_Gate4_DashParser_Iso8601Duration_SupportsDaysAndFractions(string isoDuration, double expectedSec) { double actual = DashParser.ParseDuration(isoDuration); Assert.Equal(expectedSec, actual, precision: 2); } }