1 /*
2    mkvmerge -- utility for splicing together matroska files
3    from component media subtypes
4 
5    Distributed under the GPL v2
6    see the file COPYING for details
7    or visit https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
8 
9    tag helper functions
10 
11    Written by Moritz Bunkus <moritz@bunkus.org>.
12 */
13 
14 #include "common/common_pch.h"
15 
16 #include <set>
17 
18 #include "common/chapters/chapters.h"
19 #include "common/ebml.h"
20 #include "common/strings/editing.h"
21 #include "common/strings/formatting.h"
22 #include "common/strings/utf8.h"
23 #include "common/tags/tags.h"
24 #include "common/tags/target_type.h"
25 
26 #include <matroska/KaxTags.h>
27 #include <matroska/KaxTag.h>
28 
29 using namespace libebml;
30 using namespace libmatroska;
31 
32 namespace mtx::tags {
33 
34 KaxTags *
select_for_chapters(KaxTags & tags,KaxChapters & chapters)35 select_for_chapters(KaxTags &tags,
36                     KaxChapters &chapters) {
37   KaxTags *new_tags = nullptr;
38 
39   for (auto tag_child : tags) {
40     auto tag = dynamic_cast<KaxTag *>(tag_child);
41     if (!tag)
42       continue;
43 
44     bool copy              = true;
45     KaxTagTargets *targets = FindChild<KaxTagTargets>(tag);
46 
47     if (targets) {
48       for (auto child : *targets) {
49         auto t_euid = dynamic_cast<KaxTagEditionUID *>(child);
50         if (t_euid && !mtx::chapters::find_edition_with_uid(chapters, t_euid->GetValue())) {
51           copy = false;
52           break;
53         }
54 
55         auto t_cuid = dynamic_cast<KaxTagChapterUID *>(child);
56         if (t_cuid && !mtx::chapters::find_chapter_with_uid(chapters, t_cuid->GetValue())) {
57           copy = false;
58           break;
59         }
60       }
61     }
62 
63     if (!copy)
64       continue;
65 
66     if (!new_tags)
67       new_tags = new KaxTags;
68     new_tags->PushElement(*(tag->Clone()));
69   }
70 
71   return new_tags;
72 }
73 
74 KaxTagSimple &
find_simple(const std::string & name,EbmlMaster & m)75 find_simple(const std::string &name,
76             EbmlMaster &m) {
77   return find_simple(to_utfstring(name), m);
78 }
79 
80 KaxTagSimple &
find_simple(const UTFstring & name,EbmlMaster & m)81 find_simple(const UTFstring &name,
82             EbmlMaster &m) {
83   if (Is<KaxTagSimple>(&m)) {
84     KaxTagName *tname = FindChild<KaxTagName>(&m);
85     if (tname && (name == UTFstring(*tname)))
86       return *static_cast<KaxTagSimple *>(&m);
87   }
88 
89   size_t i;
90   for (i = 0; i < m.ListSize(); i++)
91     if (Is<KaxTag, KaxTagSimple>(m[i]))
92       try {
93         return find_simple(name, *static_cast<EbmlMaster *>(m[i]));
94       } catch (...) {
95       }
96 
97   throw false;
98 }
99 
100 std::string
get_simple_value(const std::string & name,EbmlMaster & m)101 get_simple_value(const std::string &name,
102                  EbmlMaster &m) {
103   try {
104     auto tvalue = FindChild<KaxTagString>(&find_simple(name, m));
105     if (tvalue)
106       return tvalue->GetValueUTF8();
107   } catch (...) {
108   }
109 
110   return "";
111 }
112 
113 std::string
get_simple_name(const KaxTagSimple & tag)114 get_simple_name(const KaxTagSimple &tag) {
115   KaxTagName *tname = FindChild<KaxTagName>(&tag);
116   return tname ? tname->GetValueUTF8() : ""s;
117 }
118 
119 std::string
get_simple_value(const KaxTagSimple & tag)120 get_simple_value(const KaxTagSimple &tag) {
121   KaxTagString *tstring = FindChild<KaxTagString>(&tag);
122   return tstring ? tstring->GetValueUTF8() : ""s;
123 }
124 
125 void
set_simple_name(KaxTagSimple & tag,const std::string & name)126 set_simple_name(KaxTagSimple &tag,
127                 const std::string &name) {
128   GetChild<KaxTagName>(tag).SetValueUTF8(name);
129 }
130 
131 void
set_simple_value(KaxTagSimple & tag,const std::string & value)132 set_simple_value(KaxTagSimple &tag,
133                  const std::string &value) {
134   GetChild<KaxTagString>(tag).SetValueUTF8(value);
135 }
136 
137 void
set_simple(KaxTagSimple & tag,const std::string & name,const std::string & value)138 set_simple(KaxTagSimple &tag,
139            const std::string &name,
140            const std::string &value) {
141   set_simple_name(tag, name);
142   set_simple_value(tag, value);
143 }
144 
145 int64_t
get_tuid(const KaxTag & tag)146 get_tuid(const KaxTag &tag) {
147   auto targets = FindChild<KaxTagTargets>(&tag);
148   if (!targets)
149     return -1;
150 
151   auto tuid = FindChild<KaxTagTrackUID>(targets);
152   if (!tuid)
153     return -1;
154 
155   return tuid->GetValue();
156 }
157 
158 int64_t
get_cuid(const KaxTag & tag)159 get_cuid(const KaxTag &tag) {
160   auto targets = FindChild<KaxTagTargets>(&tag);
161   if (!targets)
162     return -1;
163 
164   auto cuid = FindChild<KaxTagChapterUID>(targets);
165   if (!cuid)
166     return -1;
167 
168   return cuid->GetValue();
169 }
170 
171 /** \brief Convert older tags to current specs
172 */
173 void
convert_old(KaxTags & tags)174 convert_old(KaxTags &tags) {
175   bool has_level_type = false;
176   try {
177     find_simple("LEVEL_TYPE", tags);
178     has_level_type = true;
179   } catch (...) {
180   }
181 
182   size_t tags_idx;
183   for (tags_idx = 0; tags_idx < tags.ListSize(); tags_idx++) {
184     if (!Is<KaxTag>(tags[tags_idx]))
185       continue;
186 
187     KaxTag &tag = *static_cast<KaxTag *>(tags[tags_idx]);
188 
189     auto target_type_value = Track;
190     size_t tag_idx         = 0;
191     while (tag_idx < tag.ListSize()) {
192       tag_idx++;
193       if (!Is<KaxTagSimple>(tag[tag_idx - 1]))
194         continue;
195 
196       KaxTagSimple &simple = *static_cast<KaxTagSimple *>(tag[tag_idx - 1]);
197 
198       std::string name  = get_simple_name(simple);
199       std::string value = get_simple_value(simple);
200 
201       if (name == "CATALOG")
202         set_simple_name(simple, "CATALOG_NUMBER");
203 
204       else if (name == "DATE")
205         set_simple_name(simple, "DATE_RELEASED");
206 
207       else if (name == "LEVEL_TYPE") {
208         if (value == "MEDIA")
209           target_type_value = Album;
210         tag_idx--;
211         delete tag[tag_idx];
212         tag.Remove(tag_idx);
213       }
214     }
215 
216     if (!has_level_type)
217       continue;
218 
219     auto targets = FindChild<KaxTagTargets>(&tag);
220     if (targets)
221       GetChild<KaxTagTargetTypeValue>(*targets).SetValue(target_type_value);
222   }
223 }
224 
225 int
count_simple(EbmlMaster & master)226 count_simple(EbmlMaster &master) {
227   int count = 0;
228 
229   for (auto child : master)
230     if (Is<KaxTagSimple>(child))
231       ++count;
232 
233     else if (dynamic_cast<EbmlMaster *>(child))
234       count += count_simple(*static_cast<EbmlMaster *>(child));
235 
236   return count;
237 }
238 
239 void
remove_track_uid_targets(EbmlMaster * tag)240 remove_track_uid_targets(EbmlMaster *tag) {
241   for (auto el : *tag) {
242     if (!Is<KaxTagTargets>(el))
243       continue;
244 
245     KaxTagTargets *targets = static_cast<KaxTagTargets *>(el);
246     size_t idx_target      = 0;
247 
248     while (targets->ListSize() > idx_target) {
249       EbmlElement *uid_el = (*targets)[idx_target];
250       if (Is<KaxTagTrackUID>(uid_el)) {
251         targets->Remove(idx_target);
252         delete uid_el;
253 
254       } else
255         ++idx_target;
256     }
257   }
258 }
259 
260 void
set_simple(KaxTag & tag,std::string const & name,std::string const & value,mtx::bcp47::language_c const & language)261 set_simple(KaxTag &tag,
262            std::string const &name,
263            std::string const &value,
264            mtx::bcp47::language_c const &language) {
265   KaxTagSimple *k_simple_tag = nullptr;
266 
267   for (auto const &element : tag) {
268     auto s_tag = dynamic_cast<KaxTagSimple *>(element);
269     if (!s_tag || (to_utf8(FindChildValue<KaxTagName>(s_tag)) != name))
270       continue;
271 
272     k_simple_tag = s_tag;
273     break;
274   }
275 
276   if (!k_simple_tag) {
277     k_simple_tag = static_cast<KaxTagSimple *>(empty_ebml_master(new KaxTagSimple));
278     tag.PushElement(*k_simple_tag);
279   }
280 
281   GetChild<KaxTagName>(k_simple_tag).SetValueUTF8(name);
282   GetChild<KaxTagString>(k_simple_tag).SetValueUTF8(value);
283 
284   if (!language.is_valid())
285     return;
286 
287   if (language.has_valid_iso639_2_code())
288     GetChild<KaxTagLangue>(k_simple_tag).SetValue(language.get_iso639_alpha_3_code());
289 
290   if (!mtx::bcp47::language_c::is_disabled())
291     GetChild<KaxTagLanguageIETF>(k_simple_tag).SetValue(language.format());
292 }
293 
294 void
set_target_type(KaxTag & tag,target_type_e target_type_value,std::string const & target_type)295 set_target_type(KaxTag &tag,
296                 target_type_e target_type_value,
297                 std::string const &target_type) {
298   auto &targets = GetChild<KaxTagTargets>(tag);
299 
300   GetChild<KaxTagTargetTypeValue>(targets).SetValue(target_type_value);
301   GetChild<KaxTagTargetType>(targets).SetValue(target_type);
302 }
303 
304 void
remove_elements_unsupported_by_webm(EbmlMaster & master)305 remove_elements_unsupported_by_webm(EbmlMaster &master) {
306   static auto s_supported_elements = std::map<uint32_t, bool>{};
307 
308   if (s_supported_elements.empty()) {
309     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTags))               ] = true;
310     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTag))                ] = true;
311     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTagTargets))         ] = true;
312     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTagTargetTypeValue)) ] = true;
313     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTagTargetType))      ] = true;
314     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTagTrackUID))        ] = true;
315     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTagSimple))          ] = true;
316     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTagName))            ] = true;
317     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTagLangue))          ] = true;
318     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTagLanguageIETF))    ] = true;
319     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTagDefault))         ] = true;
320     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTagString))          ] = true;
321     s_supported_elements[ EBML_ID_VALUE(EBML_ID(KaxTagBinary))          ] = true;
322   }
323 
324   auto is_simple = Is<KaxTagSimple>(master);
325   auto idx       = 0u;
326 
327   while (idx < master.ListSize()) {
328     auto e = master[idx];
329 
330     if (e && s_supported_elements[ EBML_ID_VALUE(EbmlId(*e)) ] && !(is_simple && Is<KaxTagSimple>(e))) {
331       ++idx;
332 
333       auto sub_master = dynamic_cast<EbmlMaster *>(e);
334       if (sub_master)
335         remove_elements_unsupported_by_webm(*sub_master);
336 
337     } else {
338       delete master[idx];
339       master.Remove(idx);
340     }
341   }
342 }
343 
344 bool
remove_track_statistics(KaxTags * tags,std::optional<uint64_t> track_uid)345 remove_track_statistics(KaxTags *tags,
346                         std::optional<uint64_t> track_uid) {
347   if (!tags)
348     return false;
349 
350   auto tags_to_discard = std::set<std::string>{
351     "_STATISTICS_TAGS",
352     "_STATISTICS_WRITING_APP",
353     "_STATISTICS_WRITING_DATE_UTC",
354   };
355 
356   auto const wanted_target_type = static_cast<unsigned int>(mtx::tags::Movie);
357 
358   for (auto const &tag_elt : *tags) {
359     auto tag = dynamic_cast<KaxTag *>(tag_elt);
360     if (!tag)
361       continue;
362 
363     auto targets = FindChild<KaxTagTargets>(tag);
364     if (!targets || (FindChildValue<KaxTagTargetTypeValue>(targets, wanted_target_type) != wanted_target_type))
365       continue;
366 
367     for (auto const &simple_tag_elt : *tag) {
368       auto simple_tag = dynamic_cast<KaxTagSimple *>(simple_tag_elt);
369       if (!simple_tag)
370         continue;
371 
372       auto simple_tag_name = mtx::tags::get_simple_name(*simple_tag);
373       if (simple_tag_name != "_STATISTICS_TAGS")
374         continue;
375 
376       auto all_to_discard = mtx::string::split(mtx::tags::get_simple_value(*simple_tag), QRegularExpression{"\\s+"});
377       for (auto const &to_discard : all_to_discard)
378         tags_to_discard.insert(to_discard);
379     }
380   }
381 
382   auto removed_something = false;
383 
384   for (auto const &tag_name : tags_to_discard) {
385     auto removed_something_here = mtx::tags::remove_simple_tags_for<KaxTagTrackUID>(*tags, track_uid, tag_name);
386     removed_something           = removed_something || removed_something_here;
387   }
388 
389   return removed_something;
390 }
391 
392 namespace {
393 std::string
convert_tagets_to_index(KaxTagTargets const & targets)394 convert_tagets_to_index(KaxTagTargets const &targets) {
395   std::vector<std::string> properties;
396 
397   properties.emplace_back(fmt::format("TargetTypeValue:{}", FindChildValue<KaxTagTargetTypeValue>(targets, 50)));
398   properties.emplace_back(fmt::format("TargetType:{}",      FindChildValue<KaxTagTargetType>(     targets, ""s)));
399 
400   for (auto const &child : targets)
401     if (dynamic_cast<EbmlUInteger *>(child) && !dynamic_cast<KaxTagTargetTypeValue *>(child))
402       properties.emplace_back(fmt::format("{}:{}", EBML_NAME(child), static_cast<EbmlUInteger *>(child)->GetValue()));
403 
404   std::sort(properties.begin(), properties.end());
405 
406   return mtx::string::join(properties, " ");
407 }
408 
409 }
410 
411 std::shared_ptr<libmatroska::KaxTags>
merge(std::shared_ptr<libmatroska::KaxTags> const & t1,std::shared_ptr<libmatroska::KaxTags> const & t2)412 merge(std::shared_ptr<libmatroska::KaxTags> const &t1,
413       std::shared_ptr<libmatroska::KaxTags> const &t2) {
414   if (!t1 && !t2)
415     return {};
416 
417   std::map<std::string, KaxTag *> tag_by_target_type;
418   auto dest = std::make_shared<libmatroska::KaxTags>();
419 
420   for (auto const &src : std::vector<std::shared_ptr<libmatroska::KaxTags>>{t1, t2}) {
421     if (!src)
422       continue;
423 
424     for (auto src_child : *src) {
425       if (!dynamic_cast<KaxTag *>(src_child)) {
426         delete src_child;
427         continue;
428       }
429 
430       auto tag              = static_cast<KaxTag *>(src_child);
431       auto &targets         = GetChild<KaxTagTargets>(*tag);
432       auto idx              = convert_tagets_to_index(targets);
433       auto existing_tag_itr = tag_by_target_type.find(idx);
434 
435       if (existing_tag_itr == tag_by_target_type.end()) {
436         dest->PushElement(*tag);
437         tag_by_target_type[idx] = tag;
438         continue;
439       }
440 
441       for (auto const &tag_child : *tag) {
442         if (dynamic_cast<KaxTagTargets *>(tag_child))
443           delete tag_child;
444         else
445           existing_tag_itr->second->PushElement(*tag_child);
446       }
447 
448       tag->RemoveAll();
449     }
450 
451     src->RemoveAll();
452   }
453 
454   return dest;
455 }
456 
457 }
458