mirror of
https://github.com/91270/MeiamSubtitles.git
synced 2026-11-02 05:24:17 +08:00
fix: strengthen Thunder subtitle validation
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
@@ -19,26 +20,34 @@ namespace MeiamSubtitles.Shared
|
||||
throw new InvalidDataException("Compressed subtitle responses are not supported by Thunder.");
|
||||
}
|
||||
|
||||
var sample = DecodeSample(data);
|
||||
if (mediaType?.IndexOf("html", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
mediaType?.IndexOf("json", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
throw new InvalidDataException("Thunder returned an error document instead of a subtitle.");
|
||||
}
|
||||
|
||||
var valid = format == "srt"
|
||||
? sample.IndexOf("-->", StringComparison.Ordinal) >= 0
|
||||
: sample.IndexOf("[Script Info]", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
sample.IndexOf("Dialogue:", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
if (valid)
|
||||
var errorDocument = false;
|
||||
foreach (var sample in DecodeSamples(data))
|
||||
{
|
||||
return;
|
||||
if (sample.StartsWith("<", StringComparison.Ordinal) ||
|
||||
sample.StartsWith("{", StringComparison.Ordinal))
|
||||
{
|
||||
errorDocument = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsValidSubtitle(sample, format))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sample.StartsWith("[", StringComparison.Ordinal))
|
||||
{
|
||||
errorDocument = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sample.StartsWith("<", StringComparison.Ordinal) ||
|
||||
sample.StartsWith("{", StringComparison.Ordinal) ||
|
||||
sample.StartsWith("[", StringComparison.Ordinal))
|
||||
if (errorDocument)
|
||||
{
|
||||
throw new InvalidDataException("Thunder returned an error document instead of a subtitle.");
|
||||
}
|
||||
@@ -46,63 +55,51 @@ namespace MeiamSubtitles.Shared
|
||||
throw new InvalidDataException($"Downloaded content is not a valid {format} subtitle.");
|
||||
}
|
||||
|
||||
private static string DecodeSample(byte[] data)
|
||||
private static bool IsValidSubtitle(string sample, string format)
|
||||
{
|
||||
var length = Math.Min(data.Length, 4096);
|
||||
var encoding = DetectEncoding(data);
|
||||
return encoding.GetString(data, 0, length)
|
||||
.TrimStart('\uFEFF', ' ', '\r', '\n', '\t');
|
||||
return format == "srt"
|
||||
? sample.IndexOf("-->", StringComparison.Ordinal) >= 0
|
||||
: sample.IndexOf("[Script Info]", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
sample.IndexOf("Dialogue:", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static Encoding DetectEncoding(byte[] data)
|
||||
private static IEnumerable<string> DecodeSamples(byte[] data)
|
||||
{
|
||||
var length = Math.Min(data.Length, 4096);
|
||||
|
||||
if (data.Length >= 2)
|
||||
{
|
||||
if (data[0] == 0xFF && data[1] == 0xFE)
|
||||
{
|
||||
return Encoding.Unicode;
|
||||
yield return DecodeSample(data, length, Encoding.Unicode);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (data[0] == 0xFE && data[1] == 0xFF)
|
||||
{
|
||||
return Encoding.BigEndianUnicode;
|
||||
yield return DecodeSample(data, length, Encoding.BigEndianUnicode);
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
var length = Math.Min(data.Length, 512);
|
||||
var pairs = length / 2;
|
||||
if (pairs >= 4)
|
||||
if (data.Length >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF)
|
||||
{
|
||||
// Some Thunder SRT responses are UTF-16 without a BOM. Detect the
|
||||
// characteristic null-byte pattern before falling back to UTF-8.
|
||||
var littleEndianNulls = 0;
|
||||
var bigEndianNulls = 0;
|
||||
for (var i = 0; i < pairs * 2; i += 2)
|
||||
{
|
||||
if (data[i + 1] == 0)
|
||||
{
|
||||
littleEndianNulls++;
|
||||
}
|
||||
|
||||
if (data[i] == 0)
|
||||
{
|
||||
bigEndianNulls++;
|
||||
}
|
||||
}
|
||||
|
||||
var threshold = Math.Max(2, pairs / 4);
|
||||
if (littleEndianNulls >= threshold && littleEndianNulls > bigEndianNulls * 2)
|
||||
{
|
||||
return Encoding.Unicode;
|
||||
}
|
||||
|
||||
if (bigEndianNulls >= threshold && bigEndianNulls > littleEndianNulls * 2)
|
||||
{
|
||||
return Encoding.BigEndianUnicode;
|
||||
}
|
||||
yield return DecodeSample(data, length, Encoding.UTF8);
|
||||
yield break;
|
||||
}
|
||||
|
||||
return new UTF8Encoding(false, false);
|
||||
// Thunder may return UTF-16 subtitles without a BOM. Try the common
|
||||
// single-byte/UTF-8 representation and both UTF-16 byte orders, then
|
||||
// accept only a candidate that contains the expected subtitle syntax.
|
||||
yield return DecodeSample(data, length, new UTF8Encoding(false, false));
|
||||
yield return DecodeSample(data, length, Encoding.Unicode);
|
||||
yield return DecodeSample(data, length, Encoding.BigEndianUnicode);
|
||||
}
|
||||
|
||||
private static string DecodeSample(byte[] data, int length, Encoding encoding)
|
||||
{
|
||||
return encoding.GetString(data, 0, length)
|
||||
.TrimStart('\uFEFF', ' ', '\r', '\n', '\t');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,30 @@ public class SubtitleContentValidatorTests
|
||||
SubtitleContentValidator.Validate(data, "srt", "application/octet-stream");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcceptsUtf16LittleEndianSrtWithoutBomWithLongChineseCue()
|
||||
{
|
||||
var content = "1\r\n00:00:01,000 --> 00:00:20,000\r\n" + new string('中', 300) + "\r\n";
|
||||
var data = Encoding.Unicode.GetBytes(content);
|
||||
|
||||
SubtitleContentValidator.Validate(data, "srt", "application/octet-stream");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void AcceptsUtf16SrtInEitherByteOrder(bool bigEndian, bool withBom)
|
||||
{
|
||||
var encoding = new UnicodeEncoding(bigEndian, withBom);
|
||||
var content = "1\r\n00:00:01,000 --> 00:00:02,000\r\n你好\r\n";
|
||||
var body = encoding.GetBytes(content);
|
||||
var data = withBom ? encoding.GetPreamble().Concat(body).ToArray() : body;
|
||||
|
||||
SubtitleContentValidator.Validate(data, "srt", "application/octet-stream");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("<html><body>temporary error</body></html>", "application/octet-stream")]
|
||||
[InlineData("{\"error\":\"temporary error\"}", "application/json")]
|
||||
@@ -45,4 +69,16 @@ public class SubtitleContentValidatorTests
|
||||
|
||||
Assert.Contains("Compressed", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("<html><!-- service unavailable --><body>Error</body></html>", "srt")]
|
||||
[InlineData("{\"error\":\"download --> failed\"}", "srt")]
|
||||
[InlineData("<html><body>Dialogue: download failed</body></html>", "ass")]
|
||||
public void RejectsErrorDocumentsThatContainSubtitleMarkers(string content, string format)
|
||||
{
|
||||
var exception = Assert.Throws<InvalidDataException>(() =>
|
||||
SubtitleContentValidator.Validate(Encoding.UTF8.GetBytes(content), format, "application/octet-stream"));
|
||||
|
||||
Assert.Contains("error document", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user