diff --git a/src/opus.cc b/src/opus.cc index 40da782..36b4772 100644 --- a/src/opus.cc +++ b/src/opus.cc @@ -110,3 +110,54 @@ ot::dynamic_ogg_packet ot::render_tags(const opus_tags& tags) return op; } + +/** + * The METADATA_BLOCK_PICTURE binary data, after base64 decoding, is organized like this: + * + * - 4 bytes for the picture type, + * - 4 + n bytes for the MIME type, + * - 4 + n bytes for the description string, + * - 16 bytes of picture attributes, + * - 4 + n bytes for the picture data. + * + * Integers are all big endian. + */ +ot::picture::picture(std::string block) + : storage(std::move(block)) +{ + auto bytes = reinterpret_cast(storage.data()); + + size_t mime_offset = 4; + if (storage.size() < mime_offset + 4) + throw status { st::invalid_size, "missing MIME type in picture block" }; + uint32_t mime_size = be32toh(*reinterpret_cast(bytes + mime_offset)); + + size_t desc_offset = mime_offset + 4 + mime_size; + if (storage.size() < desc_offset + 4) + throw status { st::invalid_size, "missing description in picture block" }; + uint32_t desc_size = be32toh(*reinterpret_cast(bytes + desc_offset)); + + size_t pic_offset = desc_offset + 4 + desc_size + 16; + if (storage.size() < pic_offset + 4) + throw status { st::invalid_size, "missing picture data in picture block" }; + uint32_t pic_size = be32toh(*reinterpret_cast(bytes + pic_offset)); + + if (storage.size() != pic_offset + 4 + pic_size) + throw status { st::invalid_size, "invalid picture block size" }; + + mime_type = std::string_view(reinterpret_cast(bytes + mime_offset + 4), mime_size); + picture_data = std::string_view(reinterpret_cast(bytes + pic_offset + 4), pic_size); +} + +std::optional ot::extract_cover(const ot::opus_tags& tags) +{ + static const std::string_view prefix = "METADATA_BLOCK_PICTURE="sv; + auto is_cover = [](const std::string& tag) { return tag.starts_with(prefix); }; + auto cover_tag = std::find_if(tags.comments.begin(), tags.comments.end(), is_cover); + if (cover_tag == tags.comments.end()) + return {}; // No cover art. + + std::string_view cover_value = *cover_tag; + cover_value.remove_prefix(prefix.size()); + return picture(decode_base64(cover_value)); +} diff --git a/src/opustags.h b/src/opustags.h index a04bb9c..f18d567 100644 --- a/src/opustags.h +++ b/src/opustags.h @@ -51,6 +51,8 @@ #include #define htole32(x) OSSwapHostToLittleInt32(x) #define le32toh(x) OSSwapLittleToHostInt32(x) +#define htobe32(x) OSSwapHostToBigInt32(x) +#define be32toh(x) OSSwapBigToHostInt32(x) #endif using namespace std::literals; @@ -89,6 +91,7 @@ enum class st { cut_comment_count, cut_comment_length, cut_comment_data, + invalid_size, /* CLI */ bad_arguments, }; @@ -398,6 +401,26 @@ opus_tags parse_tags(const ogg_packet& packet); */ dynamic_ogg_packet render_tags(const opus_tags& tags); +/** + * Extracted data from the METADATA_BLOCK_PICTURE tag. See + * for the full specifications. + * + * It may contain all kinds of metadata but most are not used at all. For now, let’s assume all + * pictures have picture type 3 (front cover), and empty metadata. + */ +struct picture { + /** Extract the picture information from serialized binary data.*/ + picture(std::string block); + std::string_view mime_type; + std::string_view picture_data; + /** To avoid needless copies of the picture data, move the original data block there. The + * string_view attributes will refer to it. */ + std::string storage; +}; + +/** Extract the first picture embedded in the tags, regardless of its type. */ +std::optional extract_cover(const opus_tags& tags); + /** \} */ /***********************************************************************************************//** diff --git a/t/opus.cc b/t/opus.cc index 3988451..cb83bea 100644 --- a/t/opus.cc +++ b/t/opus.cc @@ -135,12 +135,43 @@ static void recode_padding() throw failure("the rendered packet is not what we expected"); } +static void extract_cover() +{ + std::string_view picture_data = ""sv + "\x00\x00\x00\x03" // Picture type 3. + "\x00\x00\x00\x09" "image/foo" // MIME type. + "\x00\x00\x00\x00" "" // Description. + "\x00\x00\x00\x00" // Width. + "\x00\x00\x00\x00" // Height. + "\x00\x00\x00\x00" // Color depth. + "\x00\x00\x00\x00" // Palette size. + "\x00\x00\x00\x0C" "Picture data"; + + ot::opus_tags tags; + tags.comments.push_front("METADATA_BLOCK_PICTURE=" + ot::encode_base64(picture_data)); + std::optional cover = ot::extract_cover(tags); + if (!cover) + throw failure("could not extract the cover"); + if (cover->mime_type != "image/foo") + throw failure("bad extracted MIME type"); + if (cover->picture_data != "Picture data") + throw failure("bad extracted picture data"); + + std::string_view truncated_data = picture_data.substr(0, picture_data.size() - 1); + tags.comments.push_front("METADATA_BLOCK_PICTURE=" + ot::encode_base64(truncated_data)); + try { + ot::extract_cover(tags); + throw failure("accepted a bad picture block"); + } catch (const ot::status& rc) {} +} + int main() { - std::cout << "1..4\n"; + std::cout << "1..5\n"; run(parse_standard, "parse a standard OpusTags packet"); run(parse_corrupted, "correctly reject invalid packets"); run(recode_standard, "recode a standard OpusTags packet"); run(recode_padding, "recode a OpusTags packet with padding"); + run(extract_cover, "extract the cover art"); return 0; }