Coverage Report

Created: 2025-11-16 07:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibAudio/VorbisComment.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include "VorbisComment.h"
8
#include <AK/Endian.h>
9
#include <AK/MemoryStream.h>
10
#include <AK/Span.h>
11
#include <AK/String.h>
12
13
namespace Audio {
14
15
static StringView vorbis_field_for_role(Person::Role role)
16
23.0k
{
17
23.0k
    static HashMap<Person::Role, StringView> role_map {
18
23.0k
        { Person::Role::Artist, "ARTIST"sv },
19
23.0k
        { Person::Role::Performer, "PERFORMER"sv },
20
23.0k
        { Person::Role::Lyricist, "LYRICIST"sv },
21
23.0k
        { Person::Role::Conductor, "CONDUCTOR"sv },
22
23.0k
        { Person::Role::Publisher, "PUBLISHER"sv },
23
23.0k
        { Person::Role::Engineer, "ENCODED-BY"sv },
24
23.0k
        { Person::Role::Composer, "COMPOSER"sv },
25
23.0k
    };
26
23.0k
    return role_map.get(role).value();
27
23.0k
}
28
29
// "Content vector format"
30
static ErrorOr<void> read_vorbis_field(Metadata& metadata_to_write_into, String const& unparsed_user_comment)
31
3.88k
{
32
    // Technically the field name has to be ASCII, but we just accept all UTF-8.
33
3.88k
    auto field_name_and_contents = TRY(unparsed_user_comment.split_limit('=', 2));
34
35
3.88k
    if (field_name_and_contents.size() != 2)
36
550
        return Error::from_string_literal("User comment does not contain '='");
37
3.33k
    auto contents = field_name_and_contents.take_last();
38
3.33k
    auto field_name = TRY(field_name_and_contents.take_first().to_uppercase());
39
40
    // Some of these are taken from https://age.hobba.nl/audio/tag_frame_reference.html
41
3.33k
    if (field_name == "TITLE"sv) {
42
12
        if (metadata_to_write_into.title.has_value())
43
0
            TRY(metadata_to_write_into.add_miscellaneous(field_name, contents));
44
12
        else
45
12
            metadata_to_write_into.title = contents;
46
3.32k
    } else if (field_name == "VERSION"sv) {
47
0
        if (metadata_to_write_into.subtitle.has_value())
48
0
            TRY(metadata_to_write_into.add_miscellaneous(field_name, contents));
49
0
        else
50
0
            metadata_to_write_into.subtitle = contents;
51
3.32k
    } else if (field_name == "ALBUM"sv) {
52
5
        if (metadata_to_write_into.album.has_value())
53
0
            TRY(metadata_to_write_into.add_miscellaneous(field_name, contents));
54
5
        else
55
5
            metadata_to_write_into.album = contents;
56
3.31k
    } else if (field_name == "COPYRIGHT"sv) {
57
0
        if (metadata_to_write_into.copyright.has_value())
58
0
            TRY(metadata_to_write_into.add_miscellaneous(field_name, contents));
59
0
        else
60
0
            metadata_to_write_into.copyright = contents;
61
3.31k
    } else if (field_name == "ISRC"sv) {
62
0
        if (metadata_to_write_into.isrc.has_value())
63
0
            TRY(metadata_to_write_into.add_miscellaneous(field_name, contents));
64
0
        else
65
0
            metadata_to_write_into.isrc = contents;
66
3.31k
    } else if (field_name == "GENRE"sv) {
67
10
        if (metadata_to_write_into.genre.has_value())
68
0
            TRY(metadata_to_write_into.add_miscellaneous(field_name, contents));
69
10
        else
70
10
            metadata_to_write_into.genre = contents;
71
3.30k
    } else if (field_name == "COMMENT"sv) {
72
0
        if (metadata_to_write_into.comment.has_value())
73
0
            TRY(metadata_to_write_into.add_miscellaneous(field_name, contents));
74
0
        else
75
0
            metadata_to_write_into.comment = contents;
76
3.30k
    } else if (field_name == "TRACKNUMBER"sv) {
77
11
        if (metadata_to_write_into.track_number.has_value())
78
0
            TRY(metadata_to_write_into.add_miscellaneous(field_name, contents));
79
11
        else if (auto maybe_number = contents.to_number<unsigned>(); maybe_number.has_value())
80
2
            metadata_to_write_into.track_number = maybe_number.release_value();
81
9
        else
82
9
            TRY(metadata_to_write_into.add_miscellaneous(field_name, contents));
83
3.29k
    } else if (field_name == "DATE"sv) {
84
4
        if (metadata_to_write_into.unparsed_time.has_value())
85
0
            TRY(metadata_to_write_into.add_miscellaneous(field_name, contents));
86
4
        else
87
4
            metadata_to_write_into.unparsed_time = contents;
88
3.29k
    } else if (field_name == vorbis_field_for_role(Person::Role::Performer)) {
89
0
        TRY(metadata_to_write_into.add_person(Person::Role::Performer, contents));
90
3.29k
    } else if (field_name == vorbis_field_for_role(Person::Role::Artist)) {
91
1
        TRY(metadata_to_write_into.add_person(Person::Role::Artist, contents));
92
3.29k
    } else if (field_name == vorbis_field_for_role(Person::Role::Composer)) {
93
0
        TRY(metadata_to_write_into.add_person(Person::Role::Composer, contents));
94
3.29k
    } else if (field_name == vorbis_field_for_role(Person::Role::Conductor)) {
95
0
        TRY(metadata_to_write_into.add_person(Person::Role::Conductor, contents));
96
3.29k
    } else if (field_name == vorbis_field_for_role(Person::Role::Lyricist)) {
97
0
        TRY(metadata_to_write_into.add_person(Person::Role::Lyricist, contents));
98
3.29k
    } else if (field_name == "ORGANIZATION"sv) {
99
0
        TRY(metadata_to_write_into.add_person(Person::Role::Publisher, contents));
100
3.29k
    } else if (field_name == vorbis_field_for_role(Person::Role::Publisher)) {
101
0
        TRY(metadata_to_write_into.add_person(Person::Role::Publisher, contents));
102
3.29k
    } else if (field_name == vorbis_field_for_role(Person::Role::Engineer)) {
103
0
        TRY(metadata_to_write_into.add_person(Person::Role::Engineer, contents));
104
3.29k
    } else {
105
3.29k
        TRY(metadata_to_write_into.add_miscellaneous(field_name, contents));
106
3.29k
    }
107
108
3.33k
    return {};
109
3.33k
}
110
111
ErrorOr<Metadata, LoaderError> load_vorbis_comment(ByteBuffer const& vorbis_comment)
112
5.41k
{
113
5.41k
    FixedMemoryStream stream { vorbis_comment };
114
5.41k
    auto vendor_length = TRY(stream.read_value<LittleEndian<u32>>());
115
5.38k
    Vector<u8> raw_vendor_string;
116
5.38k
    TRY(raw_vendor_string.try_resize(vendor_length));
117
5.38k
    TRY(stream.read_until_filled(raw_vendor_string));
118
5.03k
    auto vendor_string = TRY(String::from_utf8(StringView { raw_vendor_string.span() }));
119
120
4.90k
    Metadata metadata;
121
4.90k
    metadata.encoder = move(vendor_string);
122
123
4.90k
    auto user_comment_count = TRY(stream.read_value<LittleEndian<u32>>());
124
8.11k
    for (size_t i = 0; i < user_comment_count; ++i) {
125
7.06k
        auto user_comment_length = TRY(stream.read_value<LittleEndian<u32>>());
126
4.62k
        Vector<u8> raw_user_comment;
127
4.62k
        TRY(raw_user_comment.try_resize(user_comment_length));
128
4.62k
        TRY(stream.read_until_filled(raw_user_comment));
129
4.45k
        auto unparsed_user_comment = TRY(String::from_utf8(StringView { raw_user_comment.span() }));
130
3.88k
        TRY(read_vorbis_field(metadata, unparsed_user_comment));
131
3.33k
    }
132
133
4.77k
    return metadata;
134
4.77k
}
135
136
struct VorbisCommentPair {
137
    String field_name;
138
    String contents;
139
};
140
141
static ErrorOr<Vector<VorbisCommentPair>> make_vorbis_user_comments(Metadata const& metadata)
142
0
{
143
0
    Vector<VorbisCommentPair> user_comments;
144
145
0
    auto add_if_present = [&](auto field_name, auto const& value) -> ErrorOr<void> {
146
0
        if (value.has_value())
147
0
            TRY(user_comments.try_append(VorbisCommentPair { field_name, TRY(String::formatted("{}", value.value())) }));
148
0
        return {};
149
0
    };
Unexecuted instantiation: VorbisComment.cpp:AK::ErrorOr<void, AK::Error> Audio::make_vorbis_user_comments(Audio::Metadata const&)::$_0::operator()<AK::String, AK::Optional<AK::String> >(AK::String, AK::Optional<AK::String> const&) const
Unexecuted instantiation: VorbisComment.cpp:AK::ErrorOr<void, AK::Error> Audio::make_vorbis_user_comments(Audio::Metadata const&)::$_0::operator()<AK::String, AK::Optional<unsigned int> >(AK::String, AK::Optional<unsigned int> const&) const
150
151
0
    TRY(add_if_present("TITLE"_string, metadata.title));
152
0
    TRY(add_if_present("VERSION"_string, metadata.subtitle));
153
0
    TRY(add_if_present("ALBUM"_string, metadata.album));
154
0
    TRY(add_if_present("COPYRIGHT"_string, metadata.copyright));
155
0
    TRY(add_if_present("ISRC"_string, metadata.isrc));
156
0
    TRY(add_if_present("GENRE"_string, metadata.genre));
157
0
    TRY(add_if_present("COMMENT"_string, metadata.comment));
158
0
    TRY(add_if_present("TRACKNUMBER"_string, metadata.track_number));
159
0
    TRY(add_if_present("DATE"_string, metadata.unparsed_time));
160
161
0
    for (auto const& person : metadata.people)
162
0
        TRY(user_comments.try_append(VorbisCommentPair { TRY(String::from_utf8(vorbis_field_for_role(person.role))), person.name }));
163
164
0
    for (auto const& field : metadata.miscellaneous) {
165
0
        for (auto const& value : field.value)
166
0
            TRY(user_comments.try_append(VorbisCommentPair { field.key, value }));
167
0
    }
168
169
0
    return user_comments;
170
0
}
171
172
ErrorOr<void> write_vorbis_comment(Metadata const& metadata, Stream& target)
173
0
{
174
0
    auto encoder = metadata.encoder.value_or({}).bytes();
175
0
    TRY(target.write_value<LittleEndian<u32>>(encoder.size()));
176
0
    TRY(target.write_until_depleted(encoder));
177
178
0
    auto vorbis_user_comments = TRY(make_vorbis_user_comments(metadata));
179
0
    TRY(target.write_value<LittleEndian<u32>>(vorbis_user_comments.size()));
180
0
    for (auto const& field : vorbis_user_comments) {
181
0
        auto const serialized_field = TRY(String::formatted("{}={}", field.field_name, field.contents));
182
0
        TRY(target.write_value<LittleEndian<u32>>(serialized_field.bytes().size()));
183
0
        TRY(target.write_until_depleted(serialized_field.bytes()));
184
0
    }
185
186
0
    return {};
187
0
}
188
189
}