blob: a7cbc5a0f5b6d52fa6a2ddd13229e2f6cb70964b [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/formats/hls/tags.h"
#include "base/location.h"
#include "media/formats/hls/items.h"
#include "media/formats/hls/source_string.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace media::hls {
namespace {
template <typename T>
void ErrorTest(absl::optional<base::StringPiece> content,
ParseStatusCode expected_status,
const base::Location& from = base::Location::Current()) {
auto tag = content ? TagItem::Create(ToTagName(T::kName),
SourceString::CreateForTesting(*content))
: TagItem::CreateEmpty(ToTagName(T::kName), 1);
auto result = T::Parse(tag);
ASSERT_TRUE(result.has_error()) << from.ToString();
auto error = std::move(result).error();
EXPECT_EQ(error.code(), expected_status) << from.ToString();
}
template <typename T>
void ErrorTest(absl::optional<base::StringPiece> content,
const VariableDictionary& variable_dict,
VariableDictionary::SubstitutionBuffer& sub_buffer,
ParseStatusCode expected_status,
const base::Location& from = base::Location::Current()) {
auto tag = content ? TagItem::Create(ToTagName(T::kName),
SourceString::CreateForTesting(*content))
: TagItem::CreateEmpty(ToTagName(T::kName), 1);
auto result = T::Parse(tag, variable_dict, sub_buffer);
ASSERT_TRUE(result.has_error()) << from.ToString();
auto error = std::move(result).error();
EXPECT_EQ(error.code(), expected_status);
}
template <typename T>
T OkTest(absl::optional<base::StringPiece> content,
const base::Location& from = base::Location::Current()) {
auto tag = content ? TagItem::Create(ToTagName(T::kName),
SourceString::CreateForTesting(*content))
: TagItem::CreateEmpty(ToTagName(T::kName), 1);
auto result = T::Parse(tag);
EXPECT_TRUE(result.has_value()) << from.ToString();
return std::move(result).value();
}
template <typename T>
T OkTest(absl::optional<base::StringPiece> content,
const VariableDictionary& variable_dict,
VariableDictionary::SubstitutionBuffer& sub_buffer,
const base::Location& from = base::Location::Current()) {
auto tag = content ? TagItem::Create(ToTagName(T::kName),
SourceString::CreateForTesting(*content))
: TagItem::CreateEmpty(ToTagName(T::kName), 1);
auto result = T::Parse(tag, variable_dict, sub_buffer);
EXPECT_TRUE(result.has_value()) << from.ToString();
return std::move(result).value();
}
// Helper to test identification of this tag in a manifest.
// `line` must be a sample line containing this tag, and must end with a
// newline. This DOES NOT parse the item content (only that the item content
// matches what was expected), use `OkTest` and `ErrorTest` for that.
template <typename T>
void RunTagIdenficationTest(
base::StringPiece line,
absl::optional<base::StringPiece> expected_content,
const base::Location& from = base::Location::Current()) {
auto iter = SourceLineIterator(line);
auto item_result = GetNextLineItem(&iter);
ASSERT_TRUE(item_result.has_value()) << from.ToString();
auto item = std::move(item_result).value();
auto* tag = absl::get_if<TagItem>(&item);
ASSERT_NE(tag, nullptr) << from.ToString();
EXPECT_EQ(tag->GetName(), ToTagName(T::kName)) << from.ToString();
EXPECT_EQ(tag->GetContent().has_value(), expected_content.has_value())
<< from.ToString();
if (tag->GetContent().has_value() && expected_content.has_value()) {
EXPECT_EQ(tag->GetContent()->Str(), *expected_content) << from.ToString();
}
}
// Test helper for tags which are expected to have no content
template <typename T>
void RunEmptyTagTest() {
// Empty content is the only allowed content
OkTest<T>(absl::nullopt);
// Test with non-empty content
ErrorTest<T>("", ParseStatusCode::kMalformedTag);
ErrorTest<T>(" ", ParseStatusCode::kMalformedTag);
ErrorTest<T>("a", ParseStatusCode::kMalformedTag);
ErrorTest<T>("1234", ParseStatusCode::kMalformedTag);
ErrorTest<T>("\t", ParseStatusCode::kMalformedTag);
}
types::VariableName CreateVarName(base::StringPiece name) {
return types::VariableName::Parse(SourceString::CreateForTesting(name))
.value();
}
VariableDictionary CreateBasicDictionary(
const base::Location& from = base::Location::Current()) {
VariableDictionary dict;
EXPECT_TRUE(dict.Insert(CreateVarName("FOO"), "bar")) << from.ToString();
EXPECT_TRUE(dict.Insert(CreateVarName("BAR"), "baz")) << from.ToString();
return dict;
}
} // namespace
TEST(HlsFormatParserTest, TagNameIdentifyTest) {
std::set<base::StringPiece> names;
for (TagName name = kMinTagName; name <= kMaxTagName; ++name) {
auto name_str = TagNameToString(name);
// Name must be unique
EXPECT_EQ(names.find(name_str), names.end());
names.insert(name_str);
// Name must parse to the original constant
EXPECT_EQ(ParseTagName(name_str), name);
}
}
TEST(HlsFormatParserTest, ParseM3uTagTest) {
RunTagIdenficationTest<M3uTag>("#EXTM3U\n", absl::nullopt);
RunEmptyTagTest<M3uTag>();
}
TEST(HlsFormatParserTest, ParseXVersionTagTest) {
RunTagIdenficationTest<XVersionTag>("#EXT-X-VERSION:123\n", "123");
// Test valid versions
auto tag = OkTest<XVersionTag>("1");
EXPECT_EQ(tag.version, 1u);
tag = OkTest<XVersionTag>("2");
EXPECT_EQ(tag.version, 2u);
tag = OkTest<XVersionTag>("3");
EXPECT_EQ(tag.version, 3u);
tag = OkTest<XVersionTag>("4");
EXPECT_EQ(tag.version, 4u);
tag = OkTest<XVersionTag>("5");
EXPECT_EQ(tag.version, 5u);
tag = OkTest<XVersionTag>("6");
EXPECT_EQ(tag.version, 6u);
tag = OkTest<XVersionTag>("7");
EXPECT_EQ(tag.version, 7u);
tag = OkTest<XVersionTag>("8");
EXPECT_EQ(tag.version, 8u);
tag = OkTest<XVersionTag>("9");
EXPECT_EQ(tag.version, 9u);
tag = OkTest<XVersionTag>("10");
EXPECT_EQ(tag.version, 10u);
// While unsupported playlist versions are rejected, that's NOT the
// responsibility of this tag parsing function. The playlist should be
// rejected at a higher level.
tag = OkTest<XVersionTag>("99999");
EXPECT_EQ(tag.version, 99999u);
// Test invalid versions
ErrorTest<XVersionTag>(absl::nullopt, ParseStatusCode::kMalformedTag);
ErrorTest<XVersionTag>("", ParseStatusCode::kMalformedTag);
ErrorTest<XVersionTag>("0", ParseStatusCode::kInvalidPlaylistVersion);
ErrorTest<XVersionTag>("-1", ParseStatusCode::kMalformedTag);
ErrorTest<XVersionTag>("1.0", ParseStatusCode::kMalformedTag);
ErrorTest<XVersionTag>("asdf", ParseStatusCode::kMalformedTag);
ErrorTest<XVersionTag>(" 1 ", ParseStatusCode::kMalformedTag);
}
TEST(HlsFormatParserTest, ParseInfTagTest) {
RunTagIdenficationTest<InfTag>("#EXTINF:123,\t\n", "123,\t");
// Test some valid tags
auto tag = OkTest<InfTag>("1234,");
EXPECT_EQ(tag.duration, 1234.0);
EXPECT_EQ(tag.title.Str(), "");
tag = OkTest<InfTag>("1.234,");
EXPECT_EQ(tag.duration, 1.234);
EXPECT_EQ(tag.title.Str(), "");
// The spec implies that whitespace characters like this usually aren't
// permitted, but "\t" is a common occurrence for the title value.
tag = OkTest<InfTag>("99.5,\t");
EXPECT_EQ(tag.duration, 99.5);
EXPECT_EQ(tag.title.Str(), "\t");
tag = OkTest<InfTag>("9.5,,,,");
EXPECT_EQ(tag.duration, 9.5);
EXPECT_EQ(tag.title.Str(), ",,,");
tag = OkTest<InfTag>("12,asdfsdf ");
EXPECT_EQ(tag.duration, 12.0);
EXPECT_EQ(tag.title.Str(), "asdfsdf ");
// Test some invalid tags
ErrorTest<InfTag>(absl::nullopt, ParseStatusCode::kMalformedTag);
ErrorTest<InfTag>("", ParseStatusCode::kMalformedTag);
ErrorTest<InfTag>(",", ParseStatusCode::kMalformedTag);
ErrorTest<InfTag>("-123,", ParseStatusCode::kMalformedTag);
ErrorTest<InfTag>("123", ParseStatusCode::kMalformedTag);
ErrorTest<InfTag>("asdf,", ParseStatusCode::kMalformedTag);
}
TEST(HlsFormatParserTest, ParseXIndependentSegmentsTest) {
RunTagIdenficationTest<XIndependentSegmentsTag>(
"#EXT-X-INDEPENDENT-SEGMENTS\n", absl::nullopt);
RunEmptyTagTest<XIndependentSegmentsTag>();
}
TEST(HlsFormatParserTest, ParseXEndListTagTest) {
RunTagIdenficationTest<XEndListTag>("#EXT-X-END-LIST\n", absl::nullopt);
RunEmptyTagTest<XEndListTag>();
}
TEST(HlsFormatParserTest, ParseXIFramesOnlyTagTest) {
RunTagIdenficationTest<XIFramesOnlyTag>("#EXT-X-I-FRAMES-ONLY\n",
absl::nullopt);
RunEmptyTagTest<XIFramesOnlyTag>();
}
TEST(HlsFormatParserTest, ParseXDiscontinuityTagTest) {
RunTagIdenficationTest<XDiscontinuityTag>("#EXT-X-DISCONTINUITY\n",
absl::nullopt);
RunEmptyTagTest<XDiscontinuityTag>();
}
TEST(HlsFormatParserTest, ParseXGapTagTest) {
RunTagIdenficationTest<XGapTag>("#EXT-X-GAP\n", absl::nullopt);
RunEmptyTagTest<XGapTag>();
}
TEST(HlsFormatParserTest, ParseXDefineTagTest) {
RunTagIdenficationTest<XDefineTag>(
"#EXT-X-DEFINE:NAME=\"FOO\",VALUE=\"Bar\",\n",
"NAME=\"FOO\",VALUE=\"Bar\",");
// Test some valid inputs
auto tag = OkTest<XDefineTag>(R"(NAME="Foo",VALUE="bar",)");
EXPECT_EQ(tag.name.GetName(), "Foo");
EXPECT_TRUE(tag.value.has_value());
EXPECT_EQ(tag.value.value(), "bar");
tag = OkTest<XDefineTag>(R"(VALUE="90/12#%)(zx./",NAME="Hello12_-")");
EXPECT_EQ(tag.name.GetName(), "Hello12_-");
EXPECT_TRUE(tag.value.has_value());
EXPECT_EQ(tag.value.value(), "90/12#%)(zx./");
tag = OkTest<XDefineTag>(R"(IMPORT="-F90_Baz")");
EXPECT_EQ(tag.name.GetName(), "-F90_Baz");
EXPECT_FALSE(tag.value.has_value());
// IMPORT and VALUE are not currently considered an error
tag = OkTest<XDefineTag>(R"(IMPORT="F00_Bar",VALUE="Test")");
EXPECT_EQ(tag.name.GetName(), "F00_Bar");
EXPECT_FALSE(tag.value.has_value());
// NAME with empty value is allowed
tag = OkTest<XDefineTag>(R"(NAME="HELLO",VALUE="")");
EXPECT_EQ(tag.name.GetName(), "HELLO");
EXPECT_TRUE(tag.value.has_value());
EXPECT_EQ(tag.value.value(), "");
// Empty content is not allowed
ErrorTest<XDefineTag>(absl::nullopt, ParseStatusCode::kMalformedTag);
ErrorTest<XDefineTag>("", ParseStatusCode::kMalformedTag);
// NAME and IMPORT are NOT allowed
ErrorTest<XDefineTag>(R"(NAME="Foo",IMPORT="Foo")",
ParseStatusCode::kMalformedTag);
// Name without VALUE is NOT allowed
ErrorTest<XDefineTag>(R"(NAME="Foo",)", ParseStatusCode::kMalformedTag);
// Empty NAME is not allowed
ErrorTest<XDefineTag>(R"(NAME="",VALUE="Foo")",
ParseStatusCode::kMalformedTag);
// Non-valid NAME is not allowed
ErrorTest<XDefineTag>(R"(NAME=".FOO",VALUE="Foo")",
ParseStatusCode::kMalformedTag);
ErrorTest<XDefineTag>(R"(NAME="F++OO",VALUE="Foo")",
ParseStatusCode::kMalformedTag);
ErrorTest<XDefineTag>(R"(NAME=" FOO",VALUE="Foo")",
ParseStatusCode::kMalformedTag);
ErrorTest<XDefineTag>(R"(NAME="FOO ",VALUE="Foo")",
ParseStatusCode::kMalformedTag);
}
TEST(HlsFormatParserTest, ParseXPlaylistTypeTagTest) {
RunTagIdenficationTest<XPlaylistTypeTag>("#EXT-X-PLAYLIST-TYPE:VOD\n", "VOD");
RunTagIdenficationTest<XPlaylistTypeTag>("#EXT-X-PLAYLIST-TYPE:EVENT\n",
"EVENT");
auto tag = OkTest<XPlaylistTypeTag>("EVENT");
EXPECT_EQ(tag.type, PlaylistType::kEvent);
tag = OkTest<XPlaylistTypeTag>("VOD");
EXPECT_EQ(tag.type, PlaylistType::kVOD);
ErrorTest<XPlaylistTypeTag>("FOOBAR", ParseStatusCode::kUnknownPlaylistType);
ErrorTest<XPlaylistTypeTag>("EEVENT", ParseStatusCode::kUnknownPlaylistType);
ErrorTest<XPlaylistTypeTag>(" EVENT", ParseStatusCode::kUnknownPlaylistType);
ErrorTest<XPlaylistTypeTag>("EVENT ", ParseStatusCode::kUnknownPlaylistType);
ErrorTest<XPlaylistTypeTag>("", ParseStatusCode::kMalformedTag);
ErrorTest<XPlaylistTypeTag>(absl::nullopt, ParseStatusCode::kMalformedTag);
}
TEST(HlsFormatParserTest, ParseXStreamInfTest) {
RunTagIdenficationTest<XStreamInfTag>(
"#EXT-X-STREAM-INF:BANDWIDTH=1010,CODECS=\"foo,bar\"\n",
"BANDWIDTH=1010,CODECS=\"foo,bar\"");
VariableDictionary variable_dict = CreateBasicDictionary();
VariableDictionary::SubstitutionBuffer sub_buffer;
auto tag = OkTest<XStreamInfTag>(
R"(BANDWIDTH=1010,AVERAGE-BANDWIDTH=1000,CODECS="foo,bar",SCORE=12.2)",
variable_dict, sub_buffer);
EXPECT_EQ(tag.bandwidth, 1010u);
EXPECT_EQ(tag.average_bandwidth, 1000u);
EXPECT_DOUBLE_EQ(tag.score.value(), 12.2);
EXPECT_EQ(tag.codecs, "foo,bar");
// "BANDWIDTH" is the only required attribute
tag = OkTest<XStreamInfTag>(R"(BANDWIDTH=5050)", variable_dict, sub_buffer);
EXPECT_EQ(tag.bandwidth, 5050u);
EXPECT_EQ(tag.average_bandwidth, absl::nullopt);
EXPECT_EQ(tag.score, absl::nullopt);
EXPECT_EQ(tag.codecs, absl::nullopt);
ErrorTest<XStreamInfTag>(absl::nullopt, variable_dict, sub_buffer,
ParseStatusCode::kMalformedTag);
ErrorTest<XStreamInfTag>("", variable_dict, sub_buffer,
ParseStatusCode::kMalformedTag);
ErrorTest<XStreamInfTag>(R"(CODECS="foo,bar")", variable_dict, sub_buffer,
ParseStatusCode::kMalformedTag);
// "BANDWIDTH" must be a valid DecimalInteger (non-negative)
ErrorTest<XStreamInfTag>(R"(BANDWIDTH="111")", variable_dict, sub_buffer,
ParseStatusCode::kMalformedTag);
ErrorTest<XStreamInfTag>(R"(BANDWIDTH=-1)", variable_dict, sub_buffer,
ParseStatusCode::kMalformedTag);
ErrorTest<XStreamInfTag>(R"(BANDWIDTH=1.5)", variable_dict, sub_buffer,
ParseStatusCode::kMalformedTag);
// "AVERAGE-BANDWIDTH" must be a valid DecimalInteger (non-negative)
ErrorTest<XStreamInfTag>(R"(BANDWIDTH=1010,AVERAGE-BANDWIDTH="111")",
variable_dict, sub_buffer,
ParseStatusCode::kMalformedTag);
ErrorTest<XStreamInfTag>(R"(BANDWIDTH=1010,AVERAGE-BANDWIDTH=-1)",
variable_dict, sub_buffer,
ParseStatusCode::kMalformedTag);
ErrorTest<XStreamInfTag>(R"(BANDWIDTH=1010,AVERAGE-BANDWIDTH=1.5)",
variable_dict, sub_buffer,
ParseStatusCode::kMalformedTag);
// "SCORE" must be a valid DecimalFloatingPoint (non-negative)
ErrorTest<XStreamInfTag>(R"(BANDWIDTH=1010,SCORE="1")", variable_dict,
sub_buffer, ParseStatusCode::kMalformedTag);
ErrorTest<XStreamInfTag>(R"(BANDWIDTH=1010,SCORE=-1)", variable_dict,
sub_buffer, ParseStatusCode::kMalformedTag);
ErrorTest<XStreamInfTag>(R"(BANDWIDTH=1010,SCORE=ONE)", variable_dict,
sub_buffer, ParseStatusCode::kMalformedTag);
// "CODECS" must be a valid string
ErrorTest<XStreamInfTag>(R"(BANDWIDTH=1010,CODECS=abc,123)", variable_dict,
sub_buffer, ParseStatusCode::kMalformedTag);
ErrorTest<XStreamInfTag>(R"(BANDWIDTH=1010,CODECS=abc)", variable_dict,
sub_buffer, ParseStatusCode::kMalformedTag);
ErrorTest<XStreamInfTag>(R"(BANDWIDTH=1010,CODECS=123)", variable_dict,
sub_buffer, ParseStatusCode::kMalformedTag);
// "CODECS" is subject to variable substitution
tag = OkTest<XStreamInfTag>(R"(BANDWIDTH=1010,CODECS="{$FOO},{$BAR}")",
variable_dict, sub_buffer);
EXPECT_EQ(tag.bandwidth, 1010u);
EXPECT_EQ(tag.average_bandwidth, absl::nullopt);
EXPECT_EQ(tag.score, absl::nullopt);
EXPECT_EQ(tag.codecs, "bar,baz");
}
} // namespace media::hls