Add verify check for empty <Text> nodes in timed text.
[libdcp.git] / test / verify_test.cc
1 /*
2     Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     libdcp is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34 #include "compose.hpp"
35 #include "cpl.h"
36 #include "dcp.h"
37 #include "interop_subtitle_asset.h"
38 #include "j2k_transcode.h"
39 #include "mono_picture_asset.h"
40 #include "mono_picture_asset_writer.h"
41 #include "openjpeg_image.h"
42 #include "raw_convert.h"
43 #include "reel.h"
44 #include "reel_interop_closed_caption_asset.h"
45 #include "reel_interop_subtitle_asset.h"
46 #include "reel_markers_asset.h"
47 #include "reel_mono_picture_asset.h"
48 #include "reel_sound_asset.h"
49 #include "reel_stereo_picture_asset.h"
50 #include "reel_smpte_closed_caption_asset.h"
51 #include "reel_smpte_subtitle_asset.h"
52 #include "smpte_subtitle_asset.h"
53 #include "stereo_picture_asset.h"
54 #include "stream_operators.h"
55 #include "test.h"
56 #include "util.h"
57 #include "verify.h"
58 #include "verify_j2k.h"
59 #include <boost/test/unit_test.hpp>
60 #include <boost/algorithm/string.hpp>
61 #include <cstdio>
62 #include <iostream>
63
64
65 using std::list;
66 using std::pair;
67 using std::string;
68 using std::vector;
69 using std::make_pair;
70 using std::make_shared;
71 using boost::optional;
72 using namespace boost::filesystem;
73 using std::shared_ptr;
74
75
76 static list<pair<string, optional<path>>> stages;
77
78 static string filename_to_id(boost::filesystem::path path)
79 {
80         return path.string().substr(4, path.string().length() - 8);
81 }
82
83 static boost::filesystem::path const dcp_test1_pkl = find_file("test/ref/DCP/dcp_test1", "pkl_").filename();
84 static string const dcp_test1_pkl_id = filename_to_id(dcp_test1_pkl);
85
86 static boost::filesystem::path const dcp_test1_cpl = find_file("test/ref/DCP/dcp_test1", "cpl_").filename();
87 static string const dcp_test1_cpl_id = filename_to_id(dcp_test1_cpl);
88
89 static string const dcp_test1_asset_map_id = "5d51e8a1-b2a5-4da6-9b66-4615c3609440";
90
91 static boost::filesystem::path const encryption_test_cpl = find_file("test/ref/DCP/encryption_test", "cpl_").filename();
92 static string const encryption_test_cpl_id = filename_to_id(encryption_test_cpl);
93
94 static boost::filesystem::path const encryption_test_pkl = find_file("test/ref/DCP/encryption_test", "pkl_").filename();
95 static string const encryption_test_pkl_id = filename_to_id(encryption_test_pkl);
96
97 static void
98 stage (string s, optional<path> p)
99 {
100         stages.push_back (make_pair (s, p));
101 }
102
103 static void
104 progress (float)
105 {
106
107 }
108
109 static void
110 prepare_directory (path path)
111 {
112         using namespace boost::filesystem;
113         remove_all (path);
114         create_directories (path);
115 }
116
117
118 static path
119 setup (int reference_number, string verify_test_suffix)
120 {
121         auto const dir = dcp::String::compose("build/test/verify_test%1", verify_test_suffix);
122         prepare_directory (dir);
123         for (auto i: directory_iterator(dcp::String::compose("test/ref/DCP/dcp_test%1", reference_number))) {
124                 copy_file (i.path(), dir / i.path().filename());
125         }
126
127         return dir;
128 }
129
130
131 static
132 shared_ptr<dcp::CPL>
133 write_dcp_with_single_asset (path dir, shared_ptr<dcp::ReelAsset> reel_asset, dcp::Standard standard = dcp::Standard::SMPTE)
134 {
135         auto reel = make_shared<dcp::Reel>();
136         reel->add (reel_asset);
137         reel->add (simple_markers());
138
139         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, standard);
140         cpl->add (reel);
141         auto dcp = make_shared<dcp::DCP>(dir);
142         dcp->add (cpl);
143         dcp->write_xml (
144                 dcp::String::compose("libdcp %1", dcp::version),
145                 dcp::String::compose("libdcp %1", dcp::version),
146                 dcp::LocalTime().as_string(),
147                 "hello"
148                 );
149
150         return cpl;
151 }
152
153
154 /** Class that can alter a file by searching and replacing strings within it.
155  *  On destruction modifies the file whose name was given to the constructor.
156  */
157 class Editor
158 {
159 public:
160         Editor (path path)
161                 : _path(path)
162         {
163                 _content = dcp::file_to_string (_path);
164         }
165
166         ~Editor ()
167         {
168                 auto f = fopen(_path.string().c_str(), "w");
169                 BOOST_REQUIRE (f);
170                 fwrite (_content.c_str(), _content.length(), 1, f);
171                 fclose (f);
172         }
173
174         void replace (string a, string b)
175         {
176                 auto old_content = _content;
177                 boost::algorithm::replace_all (_content, a, b);
178                 BOOST_REQUIRE (_content != old_content);
179         }
180
181         void delete_first_line_containing (string s)
182         {
183                 vector<string> lines;
184                 boost::algorithm::split (lines, _content, boost::is_any_of("\r\n"), boost::token_compress_on);
185                 auto old_content = _content;
186                 _content = "";
187                 bool done = false;
188                 for (auto i: lines) {
189                         if (i.find(s) == string::npos || done) {
190                                 _content += i + "\n";
191                         } else {
192                                 done = true;
193                         }
194                 }
195                 BOOST_REQUIRE (_content != old_content);
196         }
197
198         void delete_lines (string from, string to)
199         {
200                 vector<string> lines;
201                 boost::algorithm::split (lines, _content, boost::is_any_of("\r\n"), boost::token_compress_on);
202                 bool deleting = false;
203                 auto old_content = _content;
204                 _content = "";
205                 for (auto i: lines) {
206                         if (i.find(from) != string::npos) {
207                                 deleting = true;
208                         }
209                         if (!deleting) {
210                                 _content += i + "\n";
211                         }
212                         if (deleting && i.find(to) != string::npos) {
213                                 deleting = false;
214                         }
215                 }
216                 BOOST_REQUIRE (_content != old_content);
217         }
218
219 private:
220         path _path;
221         std::string _content;
222 };
223
224
225 LIBDCP_DISABLE_WARNINGS
226 static
227 void
228 dump_notes (vector<dcp::VerificationNote> const & notes)
229 {
230         for (auto i: notes) {
231                 std::cout << dcp::note_to_string(i) << "\n";
232         }
233 }
234 LIBDCP_ENABLE_WARNINGS
235
236
237 static
238 void
239 check_verify_result (vector<path> dir, vector<dcp::VerificationNote> test_notes)
240 {
241         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
242         BOOST_REQUIRE_EQUAL (notes.size(), test_notes.size());
243         std::sort (notes.begin(), notes.end());
244         std::sort (test_notes.begin(), test_notes.end());
245         for (auto i = 0U; i < notes.size(); ++i) {
246                 BOOST_REQUIRE_MESSAGE (notes[i] == test_notes[i], "Note from verify:\n" << notes[i] << "\ndoes not match the expected:\n" << test_notes[i]);
247         }
248 }
249
250
251 static
252 void
253 check_verify_result_after_replace (string suffix, boost::function<path (string)> file, string from, string to, vector<dcp::VerificationNote::Code> codes)
254 {
255         auto dir = setup (1, suffix);
256
257         {
258                 Editor e (file(suffix));
259                 e.replace (from, to);
260         }
261
262         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
263
264         BOOST_REQUIRE_EQUAL (notes.size(), codes.size());
265         auto i = notes.begin();
266         auto j = codes.begin();
267         while (i != notes.end()) {
268                 BOOST_CHECK_EQUAL (i->code(), *j);
269                 ++i;
270                 ++j;
271         }
272 }
273
274
275 BOOST_AUTO_TEST_CASE (verify_no_error)
276 {
277         stages.clear ();
278         auto dir = setup (1, "no_error");
279         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
280
281         path const cpl_file = dir / dcp_test1_cpl;
282         path const pkl_file = dir / dcp_test1_pkl;
283         path const assetmap_file = dir / "ASSETMAP.xml";
284
285         auto st = stages.begin();
286         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
287         BOOST_REQUIRE (st->second);
288         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir));
289         ++st;
290         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
291         BOOST_REQUIRE (st->second);
292         BOOST_CHECK_EQUAL (st->second.get(), canonical(cpl_file));
293         ++st;
294         BOOST_CHECK_EQUAL (st->first, "Checking reel");
295         BOOST_REQUIRE (!st->second);
296         ++st;
297         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
298         BOOST_REQUIRE (st->second);
299         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "video.mxf"));
300         ++st;
301         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
302         BOOST_REQUIRE (st->second);
303         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "video.mxf"));
304         ++st;
305         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
306         BOOST_REQUIRE (st->second);
307         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "audio.mxf"));
308         ++st;
309         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
310         BOOST_REQUIRE (st->second);
311         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "audio.mxf"));
312         ++st;
313         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
314         BOOST_REQUIRE (st->second);
315         BOOST_CHECK_EQUAL (st->second.get(), canonical(pkl_file));
316         ++st;
317         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
318         BOOST_REQUIRE (st->second);
319         BOOST_CHECK_EQUAL (st->second.get(), canonical(assetmap_file));
320         ++st;
321         BOOST_REQUIRE (st == stages.end());
322
323         BOOST_CHECK_EQUAL (notes.size(), 0);
324 }
325
326
327 BOOST_AUTO_TEST_CASE (verify_incorrect_picture_sound_hash)
328 {
329         using namespace boost::filesystem;
330
331         auto dir = setup (1, "incorrect_picture_sound_hash");
332
333         auto video_path = path(dir / "video.mxf");
334         auto mod = fopen(video_path.string().c_str(), "r+b");
335         BOOST_REQUIRE (mod);
336         fseek (mod, 4096, SEEK_SET);
337         int x = 42;
338         fwrite (&x, sizeof(x), 1, mod);
339         fclose (mod);
340
341         auto audio_path = path(dir / "audio.mxf");
342         mod = fopen(audio_path.string().c_str(), "r+b");
343         BOOST_REQUIRE (mod);
344         BOOST_REQUIRE_EQUAL (fseek(mod, -64, SEEK_END), 0);
345         BOOST_REQUIRE (fwrite (&x, sizeof(x), 1, mod) == 1);
346         fclose (mod);
347
348         dcp::ASDCPErrorSuspender sus;
349         check_verify_result (
350                 { dir },
351                 {
352                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_PICTURE_HASH, canonical(video_path) },
353                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_SOUND_HASH, canonical(audio_path) },
354                 });
355 }
356
357
358 BOOST_AUTO_TEST_CASE (verify_mismatched_picture_sound_hashes)
359 {
360         using namespace boost::filesystem;
361
362         auto dir = setup (1, "mismatched_picture_sound_hashes");
363
364         {
365                 Editor e (dir / dcp_test1_pkl);
366                 e.replace ("<Hash>", "<Hash>x");
367         }
368
369         check_verify_result (
370                 { dir },
371                 {
372                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id, canonical(dir / dcp_test1_cpl) },
373                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_PICTURE_HASHES, canonical(dir / "video.mxf") },
374                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_SOUND_HASHES, canonical(dir / "audio.mxf") },
375                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xLq7ot/GobgrqUYdlbR8FCD5APqs=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 26 },
376                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xgVKhC9IkWyzQbgzpFcJ1bpqbtwk=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 19 },
377                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xc1DRq6GaSzV2brF0YnSNed46nqk=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 12 }
378                 });
379 }
380
381
382 BOOST_AUTO_TEST_CASE (verify_failed_read_content_kind)
383 {
384         auto dir = setup (1, "failed_read_content_kind");
385
386         {
387                 Editor e (dir / dcp_test1_cpl);
388                 e.replace ("<ContentKind>", "<ContentKind>x");
389         }
390
391         check_verify_result (
392                 { dir },
393                 {{ dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::FAILED_READ, string("Bad content kind 'xtrailer'")}}
394                 );
395 }
396
397
398 static
399 path
400 cpl (string suffix)
401 {
402         return dcp::String::compose("build/test/verify_test%1/%2", suffix, dcp_test1_cpl);
403 }
404
405
406 static
407 path
408 pkl (string suffix)
409 {
410         return dcp::String::compose("build/test/verify_test%1/%2", suffix, dcp_test1_pkl);
411 }
412
413
414 static
415 path
416 asset_map (string suffix)
417 {
418         return dcp::String::compose("build/test/verify_test%1/ASSETMAP.xml", suffix);
419 }
420
421
422 BOOST_AUTO_TEST_CASE (verify_invalid_picture_frame_rate)
423 {
424         check_verify_result_after_replace (
425                         "invalid_picture_frame_rate", &cpl,
426                         "<FrameRate>24 1", "<FrameRate>99 1",
427                         { dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES,
428                           dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE }
429                         );
430 }
431
432 BOOST_AUTO_TEST_CASE (verify_missing_asset)
433 {
434         auto dir = setup (1, "missing_asset");
435         remove (dir / "video.mxf");
436         check_verify_result (
437                 { dir },
438                 {
439                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_ASSET, canonical(dir) / "video.mxf" }
440                 });
441 }
442
443
444 BOOST_AUTO_TEST_CASE (verify_empty_asset_path)
445 {
446         check_verify_result_after_replace (
447                         "empty_asset_path", &asset_map,
448                         "<Path>video.mxf</Path>", "<Path></Path>",
449                         { dcp::VerificationNote::Code::EMPTY_ASSET_PATH }
450                         );
451 }
452
453
454 BOOST_AUTO_TEST_CASE (verify_mismatched_standard)
455 {
456         check_verify_result_after_replace (
457                         "mismatched_standard", &cpl,
458                         "http://www.smpte-ra.org/schemas/429-7/2006/CPL", "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#",
459                         { dcp::VerificationNote::Code::MISMATCHED_STANDARD,
460                           dcp::VerificationNote::Code::INVALID_XML,
461                           dcp::VerificationNote::Code::INVALID_XML,
462                           dcp::VerificationNote::Code::INVALID_XML,
463                           dcp::VerificationNote::Code::INVALID_XML,
464                           dcp::VerificationNote::Code::INVALID_XML,
465                           dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES }
466                         );
467 }
468
469
470 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_id)
471 {
472         /* There's no MISMATCHED_CPL_HASHES error here because it can't find the correct hash by ID (since the ID is wrong) */
473         check_verify_result_after_replace (
474                         "invalid_xml_cpl_id", &cpl,
475                         "<Id>urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375b", "<Id>urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375",
476                         { dcp::VerificationNote::Code::INVALID_XML }
477                         );
478 }
479
480
481 BOOST_AUTO_TEST_CASE (verify_invalid_xml_issue_date)
482 {
483         check_verify_result_after_replace (
484                         "invalid_xml_issue_date", &cpl,
485                         "<IssueDate>", "<IssueDate>x",
486                         { dcp::VerificationNote::Code::INVALID_XML,
487                           dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES }
488                         );
489 }
490
491
492 BOOST_AUTO_TEST_CASE (verify_invalid_xml_pkl_id)
493 {
494         check_verify_result_after_replace (
495                 "invalid_xml_pkl_id", &pkl,
496                 "<Id>urn:uuid:" + dcp_test1_pkl_id.substr(0, 3),
497                 "<Id>urn:uuid:x" + dcp_test1_pkl_id.substr(1, 2),
498                 { dcp::VerificationNote::Code::INVALID_XML }
499                 );
500 }
501
502
503 BOOST_AUTO_TEST_CASE (verify_invalid_xml_asset_map_id)
504 {
505         check_verify_result_after_replace (
506                 "invalix_xml_asset_map_id", &asset_map,
507                 "<Id>urn:uuid:" + dcp_test1_asset_map_id.substr(0, 3),
508                 "<Id>urn:uuid:x" + dcp_test1_asset_map_id.substr(1, 2),
509                 { dcp::VerificationNote::Code::INVALID_XML }
510                 );
511 }
512
513
514 BOOST_AUTO_TEST_CASE (verify_invalid_standard)
515 {
516         stages.clear ();
517         auto dir = setup (3, "verify_invalid_standard");
518         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
519
520         path const cpl_file = dir / "cpl_cbfd2bc0-21cf-4a8f-95d8-9cddcbe51296.xml";
521         path const pkl_file = dir / "pkl_d87a950c-bd6f-41f6-90cc-56ccd673e131.xml";
522         path const assetmap_file = dir / "ASSETMAP";
523
524         auto st = stages.begin();
525         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
526         BOOST_REQUIRE (st->second);
527         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir));
528         ++st;
529         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
530         BOOST_REQUIRE (st->second);
531         BOOST_CHECK_EQUAL (st->second.get(), canonical(cpl_file));
532         ++st;
533         BOOST_CHECK_EQUAL (st->first, "Checking reel");
534         BOOST_REQUIRE (!st->second);
535         ++st;
536         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
537         BOOST_REQUIRE (st->second);
538         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
539         ++st;
540         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
541         BOOST_REQUIRE (st->second);
542         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
543         ++st;
544         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
545         BOOST_REQUIRE (st->second);
546         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
547         ++st;
548         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
549         BOOST_REQUIRE (st->second);
550         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
551         ++st;
552         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
553         BOOST_REQUIRE (st->second);
554         BOOST_CHECK_EQUAL (st->second.get(), canonical(pkl_file));
555         ++st;
556         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
557         BOOST_REQUIRE (st->second);
558         BOOST_CHECK_EQUAL (st->second.get(), canonical(assetmap_file));
559         ++st;
560         BOOST_REQUIRE (st == stages.end());
561
562         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
563         auto i = notes.begin ();
564         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::Type::BV21_ERROR);
565         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::Code::INVALID_STANDARD);
566         ++i;
567         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::Type::BV21_ERROR);
568         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K);
569 }
570
571 /* DCP with a short asset */
572 BOOST_AUTO_TEST_CASE (verify_invalid_duration)
573 {
574         auto dir = setup (8, "invalid_duration");
575         check_verify_result (
576                 { dir },
577                 {
578                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
579                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_DURATION, string("d7576dcb-a361-4139-96b8-267f5f8d7f91") },
580                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION, string("d7576dcb-a361-4139-96b8-267f5f8d7f91") },
581                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_DURATION, string("a2a87f5d-b749-4a7e-8d0c-9d48a4abf626") },
582                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION, string("a2a87f5d-b749-4a7e-8d0c-9d48a4abf626") },
583                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K, string("2") }
584                 });
585 }
586
587
588 static
589 shared_ptr<dcp::CPL>
590 dcp_from_frame (dcp::ArrayData const& frame, path dir)
591 {
592         auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
593         create_directories (dir);
594         auto writer = asset->start_write (dir / "pic.mxf", true);
595         for (int i = 0; i < 24; ++i) {
596                 writer->write (frame.data(), frame.size());
597         }
598         writer->finalize ();
599
600         auto reel_asset = make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
601         return write_dcp_with_single_asset (dir, reel_asset);
602 }
603
604
605 BOOST_AUTO_TEST_CASE (verify_invalid_picture_frame_size_in_bytes)
606 {
607         int const too_big = 1302083 * 2;
608
609         /* Compress a black image */
610         auto image = black_image ();
611         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
612         BOOST_REQUIRE (frame.size() < too_big);
613
614         /* Place it in a bigger block with some zero padding at the end */
615         dcp::ArrayData oversized_frame(too_big);
616         memcpy (oversized_frame.data(), frame.data(), frame.size());
617         memset (oversized_frame.data() + frame.size(), 0, too_big - frame.size());
618
619         path const dir("build/test/verify_invalid_picture_frame_size_in_bytes");
620         prepare_directory (dir);
621         auto cpl = dcp_from_frame (oversized_frame, dir);
622
623         check_verify_result (
624                 { dir },
625                 {
626                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM, string("missing marker start byte") },
627                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(dir / "pic.mxf") },
628                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
629                 });
630 }
631
632
633 BOOST_AUTO_TEST_CASE (verify_nearly_invalid_picture_frame_size_in_bytes)
634 {
635         int const nearly_too_big = 1302083 * 0.98;
636
637         /* Compress a black image */
638         auto image = black_image ();
639         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
640         BOOST_REQUIRE (frame.size() < nearly_too_big);
641
642         /* Place it in a bigger block with some zero padding at the end */
643         dcp::ArrayData oversized_frame(nearly_too_big);
644         memcpy (oversized_frame.data(), frame.data(), frame.size());
645         memset (oversized_frame.data() + frame.size(), 0, nearly_too_big - frame.size());
646
647         path const dir("build/test/verify_nearly_invalid_picture_frame_size_in_bytes");
648         prepare_directory (dir);
649         auto cpl = dcp_from_frame (oversized_frame, dir);
650
651         check_verify_result (
652                 { dir },
653                 {
654                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM, string("missing marker start byte") },
655                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(dir / "pic.mxf") },
656                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
657                 });
658 }
659
660
661 BOOST_AUTO_TEST_CASE (verify_valid_picture_frame_size_in_bytes)
662 {
663         /* Compress a black image */
664         auto image = black_image ();
665         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
666         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
667
668         path const dir("build/test/verify_valid_picture_frame_size_in_bytes");
669         prepare_directory (dir);
670         auto cpl = dcp_from_frame (frame, dir);
671
672         check_verify_result ({ dir }, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
673 }
674
675
676 BOOST_AUTO_TEST_CASE (verify_valid_interop_subtitles)
677 {
678         path const dir("build/test/verify_valid_interop_subtitles");
679         prepare_directory (dir);
680         copy_file ("test/data/subs1.xml", dir / "subs.xml");
681         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
682         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
683         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
684
685         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD }});
686 }
687
688
689 BOOST_AUTO_TEST_CASE (verify_invalid_interop_subtitles)
690 {
691         using namespace boost::filesystem;
692
693         path const dir("build/test/verify_invalid_interop_subtitles");
694         prepare_directory (dir);
695         copy_file ("test/data/subs1.xml", dir / "subs.xml");
696         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
697         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
698         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
699
700         {
701                 Editor e (dir / "subs.xml");
702                 e.replace ("</ReelNumber>", "</ReelNumber><Foo></Foo>");
703         }
704
705         check_verify_result (
706                 { dir },
707                 {
708                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
709                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'Foo'"), path(), 5 },
710                         {
711                                 dcp::VerificationNote::Type::ERROR,
712                                 dcp::VerificationNote::Code::INVALID_XML,
713                                 string("element 'Foo' is not allowed for content model '(SubtitleID,MovieTitle,ReelNumber,Language,LoadFont*,Font*,Subtitle*)'"),
714                                 path(),
715                                 29
716                         }
717                 });
718 }
719
720
721 BOOST_AUTO_TEST_CASE (verify_valid_smpte_subtitles)
722 {
723         path const dir("build/test/verify_valid_smpte_subtitles");
724         prepare_directory (dir);
725         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
726         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
727         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
728         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
729
730         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
731 }
732
733
734 BOOST_AUTO_TEST_CASE (verify_invalid_smpte_subtitles)
735 {
736         using namespace boost::filesystem;
737
738         path const dir("build/test/verify_invalid_smpte_subtitles");
739         prepare_directory (dir);
740         /* This broken_smpte.mxf does not use urn:uuid: for its subtitle ID, which we tolerate (rightly or wrongly) */
741         copy_file ("test/data/broken_smpte.mxf", dir / "subs.mxf");
742         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
743         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
744         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
745
746         check_verify_result (
747                 { dir },
748                 {
749                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'Foo'"), path(), 2 },
750                         {
751                                 dcp::VerificationNote::Type::ERROR,
752                                 dcp::VerificationNote::Code::INVALID_XML,
753                                 string("element 'Foo' is not allowed for content model '(Id,ContentTitleText,AnnotationText?,IssueDate,ReelNumber?,Language?,EditRate,TimeCodeRate,StartTime?,DisplayType?,LoadFont*,SubtitleList)'"),
754                                 path(),
755                                 2
756                         },
757                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
758                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
759                 });
760 }
761
762
763 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles)
764 {
765         path const dir("build/test/verify_empty_text_node_in_subtitles");
766         prepare_directory (dir);
767         copy_file ("test/data/empty_text.mxf", dir / "subs.mxf");
768         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
769         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
770         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
771
772         check_verify_result (
773                 { dir },
774                 {
775                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_TEXT },
776                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
777                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
778                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
779                 });
780 }
781
782
783 /** A <Text> node with no content except some <Font> nodes, which themselves do have content */
784 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles_with_child_nodes)
785 {
786         path const dir("build/test/verify_empty_text_node_in_subtitles_with_child_nodes");
787         prepare_directory (dir);
788         copy_file ("test/data/empty_but_with_children.xml", dir / "subs.xml");
789         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
790         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
791         auto cpl = write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
792
793         check_verify_result (
794                 { dir },
795                 {
796                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
797                 });
798 }
799
800
801 /** A <Text> node with no content except some <Font> nodes, which themselves also have no content */
802 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles_with_empty_child_nodes)
803 {
804         path const dir("build/test/verify_empty_text_node_in_subtitles_with_empty_child_nodes");
805         prepare_directory (dir);
806         copy_file ("test/data/empty_with_empty_children.xml", dir / "subs.xml");
807         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
808         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
809         auto cpl = write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
810
811         check_verify_result (
812                 { dir },
813                 {
814                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
815                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_TEXT },
816                 });
817 }
818
819
820 BOOST_AUTO_TEST_CASE (verify_external_asset)
821 {
822         path const ov_dir("build/test/verify_external_asset");
823         prepare_directory (ov_dir);
824
825         auto image = black_image ();
826         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
827         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
828         dcp_from_frame (frame, ov_dir);
829
830         dcp::DCP ov (ov_dir);
831         ov.read ();
832
833         path const vf_dir("build/test/verify_external_asset_vf");
834         prepare_directory (vf_dir);
835
836         auto picture = ov.cpls()[0]->reels()[0]->main_picture();
837         auto cpl = write_dcp_with_single_asset (vf_dir, picture);
838
839         check_verify_result (
840                 { vf_dir },
841                 {
842                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, picture->asset()->id() },
843                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
844                 });
845 }
846
847
848 BOOST_AUTO_TEST_CASE (verify_valid_cpl_metadata)
849 {
850         path const dir("build/test/verify_valid_cpl_metadata");
851         prepare_directory (dir);
852
853         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
854         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
855         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
856
857         auto reel = make_shared<dcp::Reel>();
858         reel->add (reel_asset);
859
860         reel->add (make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", 16 * 24), 0));
861         reel->add (simple_markers(16 * 24));
862
863         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
864         cpl->add (reel);
865         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
866         cpl->set_main_sound_sample_rate (48000);
867         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
868         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
869         cpl->set_version_number (1);
870
871         dcp::DCP dcp (dir);
872         dcp.add (cpl);
873         dcp.write_xml (
874                 dcp::String::compose("libdcp %1", dcp::version),
875                 dcp::String::compose("libdcp %1", dcp::version),
876                 dcp::LocalTime().as_string(),
877                 "hello"
878                 );
879 }
880
881
882 path find_cpl (path dir)
883 {
884         for (auto i: directory_iterator(dir)) {
885                 if (boost::starts_with(i.path().filename().string(), "cpl_")) {
886                         return i.path();
887                 }
888         }
889
890         BOOST_REQUIRE (false);
891         return {};
892 }
893
894
895 /* DCP with invalid CompositionMetadataAsset */
896 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_bad_tag)
897 {
898         using namespace boost::filesystem;
899
900         path const dir("build/test/verify_invalid_cpl_metadata_bad_tag");
901         prepare_directory (dir);
902
903         auto reel = make_shared<dcp::Reel>();
904         reel->add (black_picture_asset(dir));
905         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
906         cpl->add (reel);
907         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
908         cpl->set_main_sound_sample_rate (48000);
909         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
910         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
911         cpl->set_version_number (1);
912
913         reel->add (simple_markers());
914
915         dcp::DCP dcp (dir);
916         dcp.add (cpl);
917         dcp.write_xml (
918                 dcp::String::compose("libdcp %1", dcp::version),
919                 dcp::String::compose("libdcp %1", dcp::version),
920                 dcp::LocalTime().as_string(),
921                 "hello"
922                 );
923
924         {
925                 Editor e (find_cpl(dir));
926                 e.replace ("MainSound", "MainSoundX");
927         }
928
929         check_verify_result (
930                 { dir },
931                 {
932                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXConfiguration'"), canonical(cpl->file().get()), 54 },
933                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXSampleRate'"), canonical(cpl->file().get()), 55 },
934                         {
935                                 dcp::VerificationNote::Type::ERROR,
936                                 dcp::VerificationNote::Code::INVALID_XML,
937                                 string("element 'meta:MainSoundXConfiguration' is not allowed for content model "
938                                        "'(Id,AnnotationText?,EditRate,IntrinsicDuration,EntryPoint?,Duration?,"
939                                        "FullContentTitleText,ReleaseTerritory?,VersionNumber?,Chain?,Distributor?,"
940                                        "Facility?,AlternateContentVersionList?,Luminance?,MainSoundConfiguration,"
941                                        "MainSoundSampleRate,MainPictureStoredArea,MainPictureActiveArea,MainSubtitleLanguageList?,"
942                                        "ExtensionMetadataList?,)'"),
943                                 canonical(cpl->file().get()),
944                                 75
945                         },
946                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) },
947                 });
948 }
949
950
951 /* DCP with invalid CompositionMetadataAsset */
952 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_missing_tag)
953 {
954         path const dir("build/test/verify_invalid_cpl_metadata_missing_tag");
955         prepare_directory (dir);
956
957         auto reel = make_shared<dcp::Reel>();
958         reel->add (black_picture_asset(dir));
959         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
960         cpl->add (reel);
961         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
962         cpl->set_main_sound_sample_rate (48000);
963         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
964         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
965
966         dcp::DCP dcp (dir);
967         dcp.add (cpl);
968         dcp.write_xml (
969                 dcp::String::compose("libdcp %1", dcp::version),
970                 dcp::String::compose("libdcp %1", dcp::version),
971                 dcp::LocalTime().as_string(),
972                 "hello"
973                 );
974
975         {
976                 Editor e (find_cpl(dir));
977                 e.replace ("meta:Width", "meta:WidthX");
978         }
979
980         check_verify_result (
981                 { dir },
982                 {{ dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::FAILED_READ, string("missing XML tag Width in MainPictureStoredArea") }}
983                 );
984 }
985
986
987 BOOST_AUTO_TEST_CASE (verify_invalid_language1)
988 {
989         path const dir("build/test/verify_invalid_language1");
990         prepare_directory (dir);
991         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
992         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
993         asset->_language = "wrong-andbad";
994         asset->write (dir / "subs.mxf");
995         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
996         reel_asset->_language = "badlang";
997         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
998
999         check_verify_result (
1000                 { dir },
1001                 {
1002                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
1003                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
1004                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1005                 });
1006 }
1007
1008
1009 /* SMPTE DCP with invalid <Language> in the MainClosedCaption reel and also in the XML within the MXF */
1010 BOOST_AUTO_TEST_CASE (verify_invalid_language2)
1011 {
1012         path const dir("build/test/verify_invalid_language2");
1013         prepare_directory (dir);
1014         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
1015         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
1016         asset->_language = "wrong-andbad";
1017         asset->write (dir / "subs.mxf");
1018         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
1019         reel_asset->_language = "badlang";
1020         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1021
1022         check_verify_result (
1023                 {dir},
1024                 {
1025                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
1026                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
1027                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1028                 });
1029 }
1030
1031
1032 /* SMPTE DCP with invalid <Language> in the MainSound reel, the CPL additional subtitles languages and
1033  * the release territory.
1034  */
1035 BOOST_AUTO_TEST_CASE (verify_invalid_language3)
1036 {
1037         path const dir("build/test/verify_invalid_language3");
1038         prepare_directory (dir);
1039
1040         auto picture = simple_picture (dir, "foo");
1041         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
1042         auto reel = make_shared<dcp::Reel>();
1043         reel->add (reel_picture);
1044         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "frobozz");
1045         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
1046         reel->add (reel_sound);
1047         reel->add (simple_markers());
1048
1049         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1050         cpl->add (reel);
1051         cpl->_additional_subtitle_languages.push_back("this-is-wrong");
1052         cpl->_additional_subtitle_languages.push_back("andso-is-this");
1053         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1054         cpl->set_main_sound_sample_rate (48000);
1055         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1056         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1057         cpl->set_version_number (1);
1058         cpl->_release_territory = "fred-jim";
1059         auto dcp = make_shared<dcp::DCP>(dir);
1060         dcp->add (cpl);
1061         dcp->write_xml (
1062                 dcp::String::compose("libdcp %1", dcp::version),
1063                 dcp::String::compose("libdcp %1", dcp::version),
1064                 dcp::LocalTime().as_string(),
1065                 "hello"
1066                 );
1067
1068         check_verify_result (
1069                 { dir },
1070                 {
1071                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("this-is-wrong") },
1072                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("andso-is-this") },
1073                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("fred-jim") },
1074                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("frobozz") },
1075                 });
1076 }
1077
1078
1079 static
1080 vector<dcp::VerificationNote>
1081 check_picture_size (int width, int height, int frame_rate, bool three_d)
1082 {
1083         using namespace boost::filesystem;
1084
1085         path dcp_path = "build/test/verify_picture_test";
1086         prepare_directory (dcp_path);
1087
1088         shared_ptr<dcp::PictureAsset> mp;
1089         if (three_d) {
1090                 mp = make_shared<dcp::StereoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1091         } else {
1092                 mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1093         }
1094         auto picture_writer = mp->start_write (dcp_path / "video.mxf", false);
1095
1096         auto image = black_image (dcp::Size(width, height));
1097         auto j2c = dcp::compress_j2k (image, 100000000, frame_rate, three_d, width > 2048);
1098         int const length = three_d ? frame_rate * 2 : frame_rate;
1099         for (int i = 0; i < length; ++i) {
1100                 picture_writer->write (j2c.data(), j2c.size());
1101         }
1102         picture_writer->finalize ();
1103
1104         auto d = make_shared<dcp::DCP>(dcp_path);
1105         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1106         cpl->set_annotation_text ("A Test DCP");
1107         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
1108         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1109         cpl->set_main_sound_sample_rate (48000);
1110         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1111         cpl->set_main_picture_active_area (dcp::Size(1998, 1080));
1112         cpl->set_version_number (1);
1113
1114         auto reel = make_shared<dcp::Reel>();
1115
1116         if (three_d) {
1117                 reel->add (make_shared<dcp::ReelStereoPictureAsset>(std::dynamic_pointer_cast<dcp::StereoPictureAsset>(mp), 0));
1118         } else {
1119                 reel->add (make_shared<dcp::ReelMonoPictureAsset>(std::dynamic_pointer_cast<dcp::MonoPictureAsset>(mp), 0));
1120         }
1121
1122         reel->add (simple_markers(frame_rate));
1123
1124         cpl->add (reel);
1125
1126         d->add (cpl);
1127         d->write_xml (
1128                 dcp::String::compose("libdcp %1", dcp::version),
1129                 dcp::String::compose("libdcp %1", dcp::version),
1130                 dcp::LocalTime().as_string(),
1131                 "A Test DCP"
1132                 );
1133
1134         return dcp::verify ({dcp_path}, &stage, &progress, xsd_test);
1135 }
1136
1137
1138 static
1139 void
1140 check_picture_size_ok (int width, int height, int frame_rate, bool three_d)
1141 {
1142         auto notes = check_picture_size(width, height, frame_rate, three_d);
1143         BOOST_CHECK_EQUAL (notes.size(), 0U);
1144 }
1145
1146
1147 static
1148 void
1149 check_picture_size_bad_frame_size (int width, int height, int frame_rate, bool three_d)
1150 {
1151         auto notes = check_picture_size(width, height, frame_rate, three_d);
1152         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1153         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1154         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS);
1155 }
1156
1157
1158 static
1159 void
1160 check_picture_size_bad_2k_frame_rate (int width, int height, int frame_rate, bool three_d)
1161 {
1162         auto notes = check_picture_size(width, height, frame_rate, three_d);
1163         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
1164         BOOST_CHECK_EQUAL (notes.back().type(), dcp::VerificationNote::Type::BV21_ERROR);
1165         BOOST_CHECK_EQUAL (notes.back().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K);
1166 }
1167
1168
1169 static
1170 void
1171 check_picture_size_bad_4k_frame_rate (int width, int height, int frame_rate, bool three_d)
1172 {
1173         auto notes = check_picture_size(width, height, frame_rate, three_d);
1174         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1175         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1176         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K);
1177 }
1178
1179
1180 BOOST_AUTO_TEST_CASE (verify_picture_size)
1181 {
1182         using namespace boost::filesystem;
1183
1184         /* 2K scope */
1185         check_picture_size_ok (2048, 858, 24, false);
1186         check_picture_size_ok (2048, 858, 25, false);
1187         check_picture_size_ok (2048, 858, 48, false);
1188         check_picture_size_ok (2048, 858, 24, true);
1189         check_picture_size_ok (2048, 858, 25, true);
1190         check_picture_size_ok (2048, 858, 48, true);
1191
1192         /* 2K flat */
1193         check_picture_size_ok (1998, 1080, 24, false);
1194         check_picture_size_ok (1998, 1080, 25, false);
1195         check_picture_size_ok (1998, 1080, 48, false);
1196         check_picture_size_ok (1998, 1080, 24, true);
1197         check_picture_size_ok (1998, 1080, 25, true);
1198         check_picture_size_ok (1998, 1080, 48, true);
1199
1200         /* 4K scope */
1201         check_picture_size_ok (4096, 1716, 24, false);
1202
1203         /* 4K flat */
1204         check_picture_size_ok (3996, 2160, 24, false);
1205
1206         /* Bad frame size */
1207         check_picture_size_bad_frame_size (2050, 858, 24, false);
1208         check_picture_size_bad_frame_size (2048, 658, 25, false);
1209         check_picture_size_bad_frame_size (1920, 1080, 48, true);
1210         check_picture_size_bad_frame_size (4000, 2000, 24, true);
1211
1212         /* Bad 2K frame rate */
1213         check_picture_size_bad_2k_frame_rate (2048, 858, 26, false);
1214         check_picture_size_bad_2k_frame_rate (2048, 858, 31, false);
1215         check_picture_size_bad_2k_frame_rate (1998, 1080, 50, true);
1216
1217         /* Bad 4K frame rate */
1218         check_picture_size_bad_4k_frame_rate (3996, 2160, 25, false);
1219         check_picture_size_bad_4k_frame_rate (3996, 2160, 48, false);
1220
1221         /* No 4K 3D */
1222         auto notes = check_picture_size(3996, 2160, 24, true);
1223         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1224         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1225         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D);
1226 }
1227
1228
1229 static
1230 void
1231 add_test_subtitle (shared_ptr<dcp::SubtitleAsset> asset, int start_frame, int end_frame, float v_position = 0, string text = "Hello")
1232 {
1233         asset->add (
1234                 make_shared<dcp::SubtitleString>(
1235                         optional<string>(),
1236                         false,
1237                         false,
1238                         false,
1239                         dcp::Colour(),
1240                         42,
1241                         1,
1242                         dcp::Time(start_frame, 24, 24),
1243                         dcp::Time(end_frame, 24, 24),
1244                         0,
1245                         dcp::HAlign::CENTER,
1246                         v_position,
1247                         dcp::VAlign::CENTER,
1248                         dcp::Direction::LTR,
1249                         text,
1250                         dcp::Effect::NONE,
1251                         dcp::Colour(),
1252                         dcp::Time(),
1253                         dcp::Time()
1254                 )
1255         );
1256 }
1257
1258
1259 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
1260 {
1261         path const dir("build/test/verify_invalid_closed_caption_xml_size_in_bytes");
1262         prepare_directory (dir);
1263
1264         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1265         for (int i = 0; i < 2048; ++i) {
1266                 add_test_subtitle (asset, i * 24, i * 24 + 20);
1267         }
1268         asset->set_language (dcp::LanguageTag("de-DE"));
1269         asset->write (dir / "subs.mxf");
1270         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
1271         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1272
1273         check_verify_result (
1274                 { dir },
1275                 {
1276                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1277                         {
1278                                 dcp::VerificationNote::Type::BV21_ERROR,
1279                                 dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES,
1280                                 string("413262"),
1281                                 canonical(dir / "subs.mxf")
1282                         },
1283                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1284                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1285                 });
1286 }
1287
1288
1289 static
1290 shared_ptr<dcp::SMPTESubtitleAsset>
1291 make_large_subtitle_asset (path font_file)
1292 {
1293         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1294         dcp::ArrayData big_fake_font(1024 * 1024);
1295         big_fake_font.write (font_file);
1296         for (int i = 0; i < 116; ++i) {
1297                 asset->add_font (dcp::String::compose("big%1", i), big_fake_font);
1298         }
1299         return asset;
1300 }
1301
1302
1303 template <class T>
1304 void
1305 verify_timed_text_asset_too_large (string name)
1306 {
1307         auto const dir = path("build/test") / name;
1308         prepare_directory (dir);
1309         auto asset = make_large_subtitle_asset (dir / "font.ttf");
1310         add_test_subtitle (asset, 0, 240);
1311         asset->set_language (dcp::LanguageTag("de-DE"));
1312         asset->write (dir / "subs.mxf");
1313
1314         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), 240, 0);
1315         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1316
1317         check_verify_result (
1318                 { dir },
1319                 {
1320                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121696411"), canonical(dir / "subs.mxf") },
1321                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, string("121634816"), canonical(dir / "subs.mxf") },
1322                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1323                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1324                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1325                 });
1326 }
1327
1328
1329 BOOST_AUTO_TEST_CASE (verify_subtitle_asset_too_large)
1330 {
1331         verify_timed_text_asset_too_large<dcp::ReelSMPTESubtitleAsset>("verify_subtitle_asset_too_large");
1332         verify_timed_text_asset_too_large<dcp::ReelSMPTEClosedCaptionAsset>("verify_closed_caption_asset_too_large");
1333 }
1334
1335
1336 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_language)
1337 {
1338         path dir = "build/test/verify_missing_subtitle_language";
1339         prepare_directory (dir);
1340         auto dcp = make_simple (dir, 1, 106);
1341
1342         string const xml =
1343                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1344                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1345                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1346                 "<ContentTitleText>Content</ContentTitleText>"
1347                 "<AnnotationText>Annotation</AnnotationText>"
1348                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1349                 "<ReelNumber>1</ReelNumber>"
1350                 "<EditRate>24 1</EditRate>"
1351                 "<TimeCodeRate>24</TimeCodeRate>"
1352                 "<StartTime>00:00:00:00</StartTime>"
1353                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1354                 "<SubtitleList>"
1355                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1356                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1357                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1358                 "</Subtitle>"
1359                 "</Font>"
1360                 "</SubtitleList>"
1361                 "</SubtitleReel>";
1362
1363         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1364         BOOST_REQUIRE (xml_file);
1365         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1366         fclose (xml_file);
1367         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1368         subs->write (dir / "subs.mxf");
1369
1370         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1371         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1372         dcp->write_xml (
1373                 dcp::String::compose("libdcp %1", dcp::version),
1374                 dcp::String::compose("libdcp %1", dcp::version),
1375                 dcp::LocalTime().as_string(),
1376                 "A Test DCP"
1377                 );
1378
1379         check_verify_result (
1380                 { dir },
1381                 {
1382                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
1383                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1384                 });
1385 }
1386
1387
1388 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_languages)
1389 {
1390         path path ("build/test/verify_mismatched_subtitle_languages");
1391         auto constexpr reel_length = 192;
1392         auto dcp = make_simple (path, 2, reel_length);
1393         auto cpl = dcp->cpls()[0];
1394
1395         {
1396                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1397                 subs->set_language (dcp::LanguageTag("de-DE"));
1398                 subs->add (simple_subtitle());
1399                 subs->write (path / "subs1.mxf");
1400                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1401                 cpl->reels()[0]->add(reel_subs);
1402         }
1403
1404         {
1405                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1406                 subs->set_language (dcp::LanguageTag("en-US"));
1407                 subs->add (simple_subtitle());
1408                 subs->write (path / "subs2.mxf");
1409                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1410                 cpl->reels()[1]->add(reel_subs);
1411         }
1412
1413         dcp->write_xml (
1414                 dcp::String::compose("libdcp %1", dcp::version),
1415                 dcp::String::compose("libdcp %1", dcp::version),
1416                 dcp::LocalTime().as_string(),
1417                 "A Test DCP"
1418                 );
1419
1420         check_verify_result (
1421                 { path },
1422                 {
1423                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1424                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") },
1425                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES }
1426                 });
1427 }
1428
1429
1430 BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
1431 {
1432         path path ("build/test/verify_multiple_closed_caption_languages_allowed");
1433         auto constexpr reel_length = 192;
1434         auto dcp = make_simple (path, 2, reel_length);
1435         auto cpl = dcp->cpls()[0];
1436
1437         {
1438                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1439                 ccaps->set_language (dcp::LanguageTag("de-DE"));
1440                 ccaps->add (simple_subtitle());
1441                 ccaps->write (path / "subs1.mxf");
1442                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1443                 cpl->reels()[0]->add(reel_ccaps);
1444         }
1445
1446         {
1447                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1448                 ccaps->set_language (dcp::LanguageTag("en-US"));
1449                 ccaps->add (simple_subtitle());
1450                 ccaps->write (path / "subs2.mxf");
1451                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1452                 cpl->reels()[1]->add(reel_ccaps);
1453         }
1454
1455         dcp->write_xml (
1456                 dcp::String::compose("libdcp %1", dcp::version),
1457                 dcp::String::compose("libdcp %1", dcp::version),
1458                 dcp::LocalTime().as_string(),
1459                 "A Test DCP"
1460                 );
1461
1462         check_verify_result (
1463                 { path },
1464                 {
1465                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1466                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") }
1467                 });
1468 }
1469
1470
1471 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_start_time)
1472 {
1473         path dir = "build/test/verify_missing_subtitle_start_time";
1474         prepare_directory (dir);
1475         auto dcp = make_simple (dir, 1, 106);
1476
1477         string const xml =
1478                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1479                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1480                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1481                 "<ContentTitleText>Content</ContentTitleText>"
1482                 "<AnnotationText>Annotation</AnnotationText>"
1483                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1484                 "<ReelNumber>1</ReelNumber>"
1485                 "<Language>de-DE</Language>"
1486                 "<EditRate>24 1</EditRate>"
1487                 "<TimeCodeRate>24</TimeCodeRate>"
1488                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1489                 "<SubtitleList>"
1490                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1491                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1492                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1493                 "</Subtitle>"
1494                 "</Font>"
1495                 "</SubtitleList>"
1496                 "</SubtitleReel>";
1497
1498         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1499         BOOST_REQUIRE (xml_file);
1500         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1501         fclose (xml_file);
1502         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1503         subs->write (dir / "subs.mxf");
1504
1505         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1506         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1507         dcp->write_xml (
1508                 dcp::String::compose("libdcp %1", dcp::version),
1509                 dcp::String::compose("libdcp %1", dcp::version),
1510                 dcp::LocalTime().as_string(),
1511                 "A Test DCP"
1512                 );
1513
1514         check_verify_result (
1515                 { dir },
1516                 {
1517                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1518                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1519                 });
1520 }
1521
1522
1523 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_start_time)
1524 {
1525         path dir = "build/test/verify_invalid_subtitle_start_time";
1526         prepare_directory (dir);
1527         auto dcp = make_simple (dir, 1, 106);
1528
1529         string const xml =
1530                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1531                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1532                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1533                 "<ContentTitleText>Content</ContentTitleText>"
1534                 "<AnnotationText>Annotation</AnnotationText>"
1535                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1536                 "<ReelNumber>1</ReelNumber>"
1537                 "<Language>de-DE</Language>"
1538                 "<EditRate>24 1</EditRate>"
1539                 "<TimeCodeRate>24</TimeCodeRate>"
1540                 "<StartTime>00:00:02:00</StartTime>"
1541                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1542                 "<SubtitleList>"
1543                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1544                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1545                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1546                 "</Subtitle>"
1547                 "</Font>"
1548                 "</SubtitleList>"
1549                 "</SubtitleReel>";
1550
1551         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1552         BOOST_REQUIRE (xml_file);
1553         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1554         fclose (xml_file);
1555         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1556         subs->write (dir / "subs.mxf");
1557
1558         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1559         dcp->cpls().front()->reels().front()->add(reel_subs);
1560         dcp->write_xml (
1561                 dcp::String::compose("libdcp %1", dcp::version),
1562                 dcp::String::compose("libdcp %1", dcp::version),
1563                 dcp::LocalTime().as_string(),
1564                 "A Test DCP"
1565                 );
1566
1567         check_verify_result (
1568                 { dir },
1569                 {
1570                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1571                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1572                 });
1573 }
1574
1575
1576 class TestText
1577 {
1578 public:
1579         TestText (int in_, int out_, float v_position_ = 0, string text_ = "Hello")
1580                 : in(in_)
1581                 , out(out_)
1582                 , v_position(v_position_)
1583                 , text(text_)
1584         {}
1585
1586         int in;
1587         int out;
1588         float v_position;
1589         string text;
1590 };
1591
1592
1593 template <class T>
1594 shared_ptr<dcp::CPL>
1595 dcp_with_text (path dir, vector<TestText> subs)
1596 {
1597         prepare_directory (dir);
1598         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1599         asset->set_start_time (dcp::Time());
1600         for (auto i: subs) {
1601                 add_test_subtitle (asset, i.in, i.out, i.v_position, i.text);
1602         }
1603         asset->set_language (dcp::LanguageTag("de-DE"));
1604         asset->write (dir / "subs.mxf");
1605
1606         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1607         return write_dcp_with_single_asset (dir, reel_asset);
1608 }
1609
1610
1611 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
1612 {
1613         auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
1614         /* Just too early */
1615         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
1616         check_verify_result (
1617                 { dir },
1618                 {
1619                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1620                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1621                 });
1622
1623 }
1624
1625
1626 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time)
1627 {
1628         auto const dir = path("build/test/verify_valid_subtitle_first_text_time");
1629         /* Just late enough */
1630         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
1631         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1632 }
1633
1634
1635 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time_on_second_reel)
1636 {
1637         auto const dir = path("build/test/verify_valid_subtitle_first_text_time_on_second_reel");
1638         prepare_directory (dir);
1639
1640         auto asset1 = make_shared<dcp::SMPTESubtitleAsset>();
1641         asset1->set_start_time (dcp::Time());
1642         /* Just late enough */
1643         add_test_subtitle (asset1, 4 * 24, 5 * 24);
1644         asset1->set_language (dcp::LanguageTag("de-DE"));
1645         asset1->write (dir / "subs1.mxf");
1646         auto reel_asset1 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset1, dcp::Fraction(24, 1), 5 * 24, 0);
1647         auto reel1 = make_shared<dcp::Reel>();
1648         reel1->add (reel_asset1);
1649         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 5 * 24, 0);
1650         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
1651         reel1->add (markers1);
1652
1653         auto asset2 = make_shared<dcp::SMPTESubtitleAsset>();
1654         asset2->set_start_time (dcp::Time());
1655         /* This would be too early on first reel but should be OK on the second */
1656         add_test_subtitle (asset2, 3, 4 * 24);
1657         asset2->set_language (dcp::LanguageTag("de-DE"));
1658         asset2->write (dir / "subs2.mxf");
1659         auto reel_asset2 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset2, dcp::Fraction(24, 1), 4 * 24, 0);
1660         auto reel2 = make_shared<dcp::Reel>();
1661         reel2->add (reel_asset2);
1662         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 4 * 24, 0);
1663         markers2->set (dcp::Marker::LFOC, dcp::Time(4 * 24 - 1, 24, 24));
1664         reel2->add (markers2);
1665
1666         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1667         cpl->add (reel1);
1668         cpl->add (reel2);
1669         auto dcp = make_shared<dcp::DCP>(dir);
1670         dcp->add (cpl);
1671         dcp->write_xml (
1672                 dcp::String::compose("libdcp %1", dcp::version),
1673                 dcp::String::compose("libdcp %1", dcp::version),
1674                 dcp::LocalTime().as_string(),
1675                 "hello"
1676                 );
1677
1678
1679         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1680 }
1681
1682
1683 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_spacing)
1684 {
1685         auto const dir = path("build/test/verify_invalid_subtitle_spacing");
1686         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1687                 dir,
1688                 {
1689                         { 4 * 24,     5 * 24 },
1690                         { 5 * 24 + 1, 6 * 24 },
1691                 });
1692         check_verify_result (
1693                 {dir},
1694                 {
1695                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING },
1696                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1697                 });
1698 }
1699
1700
1701 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_spacing)
1702 {
1703         auto const dir = path("build/test/verify_valid_subtitle_spacing");
1704         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1705                 dir,
1706                 {
1707                         { 4 * 24,      5 * 24 },
1708                         { 5 * 24 + 16, 8 * 24 },
1709                 });
1710         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1711 }
1712
1713
1714 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_duration)
1715 {
1716         auto const dir = path("build/test/verify_invalid_subtitle_duration");
1717         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
1718         check_verify_result (
1719                 {dir},
1720                 {
1721                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION },
1722                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1723                 });
1724 }
1725
1726
1727 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_duration)
1728 {
1729         auto const dir = path("build/test/verify_valid_subtitle_duration");
1730         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
1731         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1732 }
1733
1734
1735 BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
1736 {
1737         auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
1738         prepare_directory (dir);
1739         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1740         asset->set_start_time (dcp::Time());
1741         add_test_subtitle (asset, 0, 4 * 24);
1742         asset->set_language (dcp::LanguageTag("de-DE"));
1743         asset->write (dir / "subs.mxf");
1744
1745         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
1746         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1747         check_verify_result (
1748                 {dir},
1749                 {
1750                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "72 96", boost::filesystem::canonical(asset->file().get()) },
1751                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1752                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
1753                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1754                 });
1755
1756 }
1757
1758
1759 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
1760 {
1761         auto const dir = path ("build/test/invalid_subtitle_line_count1");
1762         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1763                 dir,
1764                 {
1765                         { 96, 200, 0.0, "We" },
1766                         { 96, 200, 0.1, "have" },
1767                         { 96, 200, 0.2, "four" },
1768                         { 96, 200, 0.3, "lines" }
1769                 });
1770         check_verify_result (
1771                 {dir},
1772                 {
1773                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1774                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1775                 });
1776 }
1777
1778
1779 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
1780 {
1781         auto const dir = path ("build/test/verify_valid_subtitle_line_count1");
1782         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1783                 dir,
1784                 {
1785                         { 96, 200, 0.0, "We" },
1786                         { 96, 200, 0.1, "have" },
1787                         { 96, 200, 0.2, "four" },
1788                 });
1789         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1790 }
1791
1792
1793 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
1794 {
1795         auto const dir = path ("build/test/verify_invalid_subtitle_line_count2");
1796         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1797                 dir,
1798                 {
1799                         { 96, 300, 0.0, "We" },
1800                         { 96, 300, 0.1, "have" },
1801                         { 150, 180, 0.2, "four" },
1802                         { 150, 180, 0.3, "lines" }
1803                 });
1804         check_verify_result (
1805                 {dir},
1806                 {
1807                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1808                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1809                 });
1810 }
1811
1812
1813 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
1814 {
1815         auto const dir = path ("build/test/verify_valid_subtitle_line_count2");
1816         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1817                 dir,
1818                 {
1819                         { 96, 300, 0.0, "We" },
1820                         { 96, 300, 0.1, "have" },
1821                         { 150, 180, 0.2, "four" },
1822                         { 190, 250, 0.3, "lines" }
1823                 });
1824         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1825 }
1826
1827
1828 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
1829 {
1830         auto const dir = path ("build/test/verify_invalid_subtitle_line_length1");
1831         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1832                 dir,
1833                 {
1834                         { 96, 300, 0.0, "012345678901234567890123456789012345678901234567890123" }
1835                 });
1836         check_verify_result (
1837                 {dir},
1838                 {
1839                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH },
1840                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1841                 });
1842 }
1843
1844
1845 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
1846 {
1847         auto const dir = path ("build/test/verify_invalid_subtitle_line_length2");
1848         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1849                 dir,
1850                 {
1851                         { 96, 300, 0.0, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
1852                 });
1853         check_verify_result (
1854                 {dir},
1855                 {
1856                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH },
1857                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1858                 });
1859 }
1860
1861
1862 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
1863 {
1864         auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
1865         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1866                 dir,
1867                 {
1868                         { 96, 200, 0.0, "We" },
1869                         { 96, 200, 0.1, "have" },
1870                         { 96, 200, 0.2, "four" },
1871                         { 96, 200, 0.3, "lines" }
1872                 });
1873         check_verify_result (
1874                 {dir},
1875                 {
1876                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1877                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1878                 });
1879 }
1880
1881
1882 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
1883 {
1884         auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
1885         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1886                 dir,
1887                 {
1888                         { 96, 200, 0.0, "We" },
1889                         { 96, 200, 0.1, "have" },
1890                         { 96, 200, 0.2, "four" },
1891                 });
1892         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1893 }
1894
1895
1896 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
1897 {
1898         auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
1899         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1900                 dir,
1901                 {
1902                         { 96, 300, 0.0, "We" },
1903                         { 96, 300, 0.1, "have" },
1904                         { 150, 180, 0.2, "four" },
1905                         { 150, 180, 0.3, "lines" }
1906                 });
1907         check_verify_result (
1908                 {dir},
1909                 {
1910                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1911                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1912                 });
1913 }
1914
1915
1916 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
1917 {
1918         auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
1919         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1920                 dir,
1921                 {
1922                         { 96, 300, 0.0, "We" },
1923                         { 96, 300, 0.1, "have" },
1924                         { 150, 180, 0.2, "four" },
1925                         { 190, 250, 0.3, "lines" }
1926                 });
1927         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1928 }
1929
1930
1931 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
1932 {
1933         auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
1934         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1935                 dir,
1936                 {
1937                         { 96, 300, 0.0, "0123456789012345678901234567890123" }
1938                 });
1939         check_verify_result (
1940                 {dir},
1941                 {
1942                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH },
1943                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1944                 });
1945 }
1946
1947
1948 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
1949 {
1950         path const dir("build/test/verify_invalid_sound_frame_rate");
1951         prepare_directory (dir);
1952
1953         auto picture = simple_picture (dir, "foo");
1954         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
1955         auto reel = make_shared<dcp::Reel>();
1956         reel->add (reel_picture);
1957         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000);
1958         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
1959         reel->add (reel_sound);
1960         reel->add (simple_markers());
1961         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1962         cpl->add (reel);
1963         auto dcp = make_shared<dcp::DCP>(dir);
1964         dcp->add (cpl);
1965         dcp->write_xml (
1966                 dcp::String::compose("libdcp %1", dcp::version),
1967                 dcp::String::compose("libdcp %1", dcp::version),
1968                 dcp::LocalTime().as_string(),
1969                 "hello"
1970                 );
1971
1972         check_verify_result (
1973                 {dir},
1974                 {
1975                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE, string("96000"), canonical(dir / "audiofoo.mxf") },
1976                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1977                 });
1978 }
1979
1980
1981 BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
1982 {
1983         path const dir("build/test/verify_missing_cpl_annotation_text");
1984         auto dcp = make_simple (dir);
1985         dcp->write_xml (
1986                 dcp::String::compose("libdcp %1", dcp::version),
1987                 dcp::String::compose("libdcp %1", dcp::version),
1988                 dcp::LocalTime().as_string(),
1989                 "A Test DCP"
1990                 );
1991
1992         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
1993
1994         auto const cpl = dcp->cpls()[0];
1995
1996         {
1997                 BOOST_REQUIRE (cpl->file());
1998                 Editor e(cpl->file().get());
1999                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "");
2000         }
2001
2002         check_verify_result (
2003                 {dir},
2004                 {
2005                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2006                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2007                 });
2008 }
2009
2010
2011 BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
2012 {
2013         path const dir("build/test/verify_mismatched_cpl_annotation_text");
2014         auto dcp = make_simple (dir);
2015         dcp->write_xml (
2016                 dcp::String::compose("libdcp %1", dcp::version),
2017                 dcp::String::compose("libdcp %1", dcp::version),
2018                 dcp::LocalTime().as_string(),
2019                 "A Test DCP"
2020                 );
2021
2022         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2023         auto const cpl = dcp->cpls()[0];
2024
2025         {
2026                 BOOST_REQUIRE (cpl->file());
2027                 Editor e(cpl->file().get());
2028                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "<AnnotationText>A Test DCP 1</AnnotationText>");
2029         }
2030
2031         check_verify_result (
2032                 {dir},
2033                 {
2034                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2035                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2036                 });
2037 }
2038
2039
2040 BOOST_AUTO_TEST_CASE (verify_mismatched_asset_duration)
2041 {
2042         path const dir("build/test/verify_mismatched_asset_duration");
2043         prepare_directory (dir);
2044         shared_ptr<dcp::DCP> dcp (new dcp::DCP(dir));
2045         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2046
2047         shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (dir, "", 24);
2048         shared_ptr<dcp::SoundAsset> ms = simple_sound (dir, "", dcp::MXFMetadata(), "en-US", 25);
2049
2050         auto reel = make_shared<dcp::Reel>(
2051                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2052                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2053                 );
2054
2055         reel->add (simple_markers());
2056         cpl->add (reel);
2057
2058         dcp->add (cpl);
2059         dcp->write_xml (
2060                 dcp::String::compose("libdcp %1", dcp::version),
2061                 dcp::String::compose("libdcp %1", dcp::version),
2062                 dcp::LocalTime().as_string(),
2063                 "A Test DCP"
2064                 );
2065
2066         check_verify_result (
2067                 {dir},
2068                 {
2069                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION },
2070                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), canonical(cpl->file().get()) }
2071                 });
2072 }
2073
2074
2075
2076 static
2077 shared_ptr<dcp::CPL>
2078 verify_subtitles_must_be_in_all_reels_check (path dir, bool add_to_reel1, bool add_to_reel2)
2079 {
2080         prepare_directory (dir);
2081         auto dcp = make_shared<dcp::DCP>(dir);
2082         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2083
2084         auto constexpr reel_length = 192;
2085
2086         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2087         subs->set_language (dcp::LanguageTag("de-DE"));
2088         subs->set_start_time (dcp::Time());
2089         subs->add (simple_subtitle());
2090         subs->write (dir / "subs.mxf");
2091         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
2092
2093         auto reel1 = make_shared<dcp::Reel>(
2094                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2095                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2096                 );
2097
2098         if (add_to_reel1) {
2099                 reel1->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2100         }
2101
2102         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2103         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2104         reel1->add (markers1);
2105
2106         cpl->add (reel1);
2107
2108         auto reel2 = make_shared<dcp::Reel>(
2109                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2110                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2111                 );
2112
2113         if (add_to_reel2) {
2114                 reel2->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2115         }
2116
2117         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2118         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2119         reel2->add (markers2);
2120
2121         cpl->add (reel2);
2122
2123         dcp->add (cpl);
2124         dcp->write_xml (
2125                 dcp::String::compose("libdcp %1", dcp::version),
2126                 dcp::String::compose("libdcp %1", dcp::version),
2127                 dcp::LocalTime().as_string(),
2128                 "A Test DCP"
2129                 );
2130
2131         return cpl;
2132 }
2133
2134
2135 BOOST_AUTO_TEST_CASE (verify_missing_main_subtitle_from_some_reels)
2136 {
2137         {
2138                 path dir ("build/test/missing_main_subtitle_from_some_reels");
2139                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, false);
2140                 check_verify_result (
2141                         { dir },
2142                         {
2143                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS },
2144                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2145                         });
2146
2147         }
2148
2149         {
2150                 path dir ("build/test/verify_subtitles_must_be_in_all_reels2");
2151                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, true);
2152                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2153         }
2154
2155         {
2156                 path dir ("build/test/verify_subtitles_must_be_in_all_reels1");
2157                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, false, false);
2158                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2159         }
2160 }
2161
2162
2163 static
2164 shared_ptr<dcp::CPL>
2165 verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1, int caps_in_reel2)
2166 {
2167         prepare_directory (dir);
2168         auto dcp = make_shared<dcp::DCP>(dir);
2169         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2170
2171         auto constexpr reel_length = 192;
2172
2173         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2174         subs->set_language (dcp::LanguageTag("de-DE"));
2175         subs->set_start_time (dcp::Time());
2176         subs->add (simple_subtitle());
2177         subs->write (dir / "subs.mxf");
2178
2179         auto reel1 = make_shared<dcp::Reel>(
2180                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2181                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2182                 );
2183
2184         for (int i = 0; i < caps_in_reel1; ++i) {
2185                 reel1->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2186         }
2187
2188         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2189         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2190         reel1->add (markers1);
2191
2192         cpl->add (reel1);
2193
2194         auto reel2 = make_shared<dcp::Reel>(
2195                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2196                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2197                 );
2198
2199         for (int i = 0; i < caps_in_reel2; ++i) {
2200                 reel2->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2201         }
2202
2203         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2204         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2205         reel2->add (markers2);
2206
2207         cpl->add (reel2);
2208
2209         dcp->add (cpl);
2210         dcp->write_xml (
2211                 dcp::String::compose("libdcp %1", dcp::version),
2212                 dcp::String::compose("libdcp %1", dcp::version),
2213                 dcp::LocalTime().as_string(),
2214                 "A Test DCP"
2215                 );
2216
2217         return cpl;
2218 }
2219
2220
2221 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_asset_counts)
2222 {
2223         {
2224                 path dir ("build/test/mismatched_closed_caption_asset_counts");
2225                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 3, 4);
2226                 check_verify_result (
2227                         {dir},
2228                         {
2229                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS },
2230                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2231                         });
2232         }
2233
2234         {
2235                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels2");
2236                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 4, 4);
2237                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2238         }
2239
2240         {
2241                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels3");
2242                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 0, 0);
2243                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2244         }
2245 }
2246
2247
2248 template <class T>
2249 void
2250 verify_text_entry_point_check (path dir, dcp::VerificationNote::Code code, boost::function<void (shared_ptr<T>)> adjust)
2251 {
2252         prepare_directory (dir);
2253         auto dcp = make_shared<dcp::DCP>(dir);
2254         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2255
2256         auto constexpr reel_length = 192;
2257
2258         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2259         subs->set_language (dcp::LanguageTag("de-DE"));
2260         subs->set_start_time (dcp::Time());
2261         subs->add (simple_subtitle());
2262         subs->write (dir / "subs.mxf");
2263         auto reel_text = make_shared<T>(subs, dcp::Fraction(24, 1), reel_length, 0);
2264         adjust (reel_text);
2265
2266         auto reel = make_shared<dcp::Reel>(
2267                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2268                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2269                 );
2270
2271         reel->add (reel_text);
2272
2273         reel->add (simple_markers(reel_length));
2274
2275         cpl->add (reel);
2276
2277         dcp->add (cpl);
2278         dcp->write_xml (
2279                 dcp::String::compose("libdcp %1", dcp::version),
2280                 dcp::String::compose("libdcp %1", dcp::version),
2281                 dcp::LocalTime().as_string(),
2282                 "A Test DCP"
2283                 );
2284
2285         check_verify_result (
2286                 {dir},
2287                 {
2288                         { dcp::VerificationNote::Type::BV21_ERROR, code, subs->id() },
2289                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2290                 });
2291 }
2292
2293
2294 BOOST_AUTO_TEST_CASE (verify_text_entry_point)
2295 {
2296         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2297                 "build/test/verify_subtitle_entry_point_must_be_present",
2298                 dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT,
2299                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2300                         asset->unset_entry_point ();
2301                         }
2302                 );
2303
2304         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2305                 "build/test/verify_subtitle_entry_point_must_be_zero",
2306                 dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT,
2307                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2308                         asset->set_entry_point (4);
2309                         }
2310                 );
2311
2312         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2313                 "build/test/verify_closed_caption_entry_point_must_be_present",
2314                 dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
2315                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2316                         asset->unset_entry_point ();
2317                         }
2318                 );
2319
2320         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2321                 "build/test/verify_closed_caption_entry_point_must_be_zero",
2322                 dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
2323                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2324                         asset->set_entry_point (9);
2325                         }
2326                 );
2327 }
2328
2329
2330 BOOST_AUTO_TEST_CASE (verify_missing_hash)
2331 {
2332         RNGFixer fix;
2333
2334         path const dir("build/test/verify_missing_hash");
2335         auto dcp = make_simple (dir);
2336         dcp->write_xml (
2337                 dcp::String::compose("libdcp %1", dcp::version),
2338                 dcp::String::compose("libdcp %1", dcp::version),
2339                 dcp::LocalTime().as_string(),
2340                 "A Test DCP"
2341                 );
2342
2343         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2344         auto const cpl = dcp->cpls()[0];
2345         BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
2346         BOOST_REQUIRE (cpl->reels()[0]->main_picture());
2347         auto asset_id = cpl->reels()[0]->main_picture()->id();
2348
2349         {
2350                 BOOST_REQUIRE (cpl->file());
2351                 Editor e(cpl->file().get());
2352                 e.delete_first_line_containing("<Hash>");
2353         }
2354
2355         check_verify_result (
2356                 {dir},
2357                 {
2358                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2359                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
2360                 });
2361 }
2362
2363
2364 static
2365 void
2366 verify_markers_test (
2367         path dir,
2368         vector<pair<dcp::Marker, dcp::Time>> markers,
2369         vector<dcp::VerificationNote> test_notes
2370         )
2371 {
2372         auto dcp = make_simple (dir);
2373         dcp->cpls()[0]->set_content_kind (dcp::ContentKind::FEATURE);
2374         auto markers_asset = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 24, 0);
2375         for (auto const& i: markers) {
2376                 markers_asset->set (i.first, i.second);
2377         }
2378         dcp->cpls()[0]->reels()[0]->add(markers_asset);
2379         dcp->write_xml (
2380                 dcp::String::compose("libdcp %1", dcp::version),
2381                 dcp::String::compose("libdcp %1", dcp::version),
2382                 dcp::LocalTime().as_string(),
2383                 "A Test DCP"
2384                 );
2385
2386         check_verify_result ({dir}, test_notes);
2387 }
2388
2389
2390 BOOST_AUTO_TEST_CASE (verify_markers)
2391 {
2392         verify_markers_test (
2393                 "build/test/verify_markers_all_correct",
2394                 {
2395                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2396                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2397                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2398                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2399                 },
2400                 {}
2401                 );
2402
2403         verify_markers_test (
2404                 "build/test/verify_markers_missing_ffec",
2405                 {
2406                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2407                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2408                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2409                 },
2410                 {
2411                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE }
2412                 });
2413
2414         verify_markers_test (
2415                 "build/test/verify_markers_missing_ffmc",
2416                 {
2417                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2418                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2419                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2420                 },
2421                 {
2422                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
2423                 });
2424
2425         verify_markers_test (
2426                 "build/test/verify_markers_missing_ffoc",
2427                 {
2428                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2429                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2430                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2431                 },
2432                 {
2433                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC}
2434                 });
2435
2436         verify_markers_test (
2437                 "build/test/verify_markers_missing_lfoc",
2438                 {
2439                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2440                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2441                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) }
2442                 },
2443                 {
2444                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC }
2445                 });
2446
2447         verify_markers_test (
2448                 "build/test/verify_markers_incorrect_ffoc",
2449                 {
2450                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2451                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2452                         { dcp::Marker::FFOC, dcp::Time(3, 24, 24) },
2453                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2454                 },
2455                 {
2456                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_FFOC, string("3") }
2457                 });
2458
2459         verify_markers_test (
2460                 "build/test/verify_markers_incorrect_lfoc",
2461                 {
2462                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2463                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2464                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2465                         { dcp::Marker::LFOC, dcp::Time(18, 24, 24) }
2466                 },
2467                 {
2468                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_LFOC, string("18") }
2469                 });
2470 }
2471
2472
2473 BOOST_AUTO_TEST_CASE (verify_missing_cpl_metadata_version_number)
2474 {
2475         path dir = "build/test/verify_missing_cpl_metadata_version_number";
2476         prepare_directory (dir);
2477         auto dcp = make_simple (dir);
2478         auto cpl = dcp->cpls()[0];
2479         cpl->unset_version_number();
2480         dcp->write_xml (
2481                 dcp::String::compose("libdcp %1", dcp::version),
2482                 dcp::String::compose("libdcp %1", dcp::version),
2483                 dcp::LocalTime().as_string(),
2484                 "A Test DCP"
2485                 );
2486
2487         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get() }});
2488 }
2489
2490
2491 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
2492 {
2493         path dir = "build/test/verify_missing_extension_metadata1";
2494         auto dcp = make_simple (dir);
2495         dcp->write_xml (
2496                 dcp::String::compose("libdcp %1", dcp::version),
2497                 dcp::String::compose("libdcp %1", dcp::version),
2498                 dcp::LocalTime().as_string(),
2499                 "A Test DCP"
2500                 );
2501
2502         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2503         auto cpl = dcp->cpls()[0];
2504
2505         {
2506                 Editor e (cpl->file().get());
2507                 e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
2508         }
2509
2510         check_verify_result (
2511                 {dir},
2512                 {
2513                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2514                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2515                 });
2516 }
2517
2518
2519 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
2520 {
2521         path dir = "build/test/verify_missing_extension_metadata2";
2522         auto dcp = make_simple (dir);
2523         dcp->write_xml (
2524                 dcp::String::compose("libdcp %1", dcp::version),
2525                 dcp::String::compose("libdcp %1", dcp::version),
2526                 dcp::LocalTime().as_string(),
2527                 "A Test DCP"
2528                 );
2529
2530         auto cpl = dcp->cpls()[0];
2531
2532         {
2533                 Editor e (cpl->file().get());
2534                 e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
2535         }
2536
2537         check_verify_result (
2538                 {dir},
2539                 {
2540                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2541                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2542                 });
2543 }
2544
2545
2546 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
2547 {
2548         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata3";
2549         auto dcp = make_simple (dir);
2550         dcp->write_xml (
2551                 dcp::String::compose("libdcp %1", dcp::version),
2552                 dcp::String::compose("libdcp %1", dcp::version),
2553                 dcp::LocalTime().as_string(),
2554                 "A Test DCP"
2555                 );
2556
2557         auto const cpl = dcp->cpls()[0];
2558
2559         {
2560                 Editor e (cpl->file().get());
2561                 e.replace ("<meta:Name>A", "<meta:NameX>A");
2562                 e.replace ("n</meta:Name>", "n</meta:NameX>");
2563         }
2564
2565         check_verify_result (
2566                 {dir},
2567                 {
2568                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:NameX'"), cpl->file().get(), 75 },
2569                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:NameX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 82 },
2570                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2571                 });
2572 }
2573
2574
2575 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
2576 {
2577         path dir = "build/test/verify_invalid_extension_metadata1";
2578         auto dcp = make_simple (dir);
2579         dcp->write_xml (
2580                 dcp::String::compose("libdcp %1", dcp::version),
2581                 dcp::String::compose("libdcp %1", dcp::version),
2582                 dcp::LocalTime().as_string(),
2583                 "A Test DCP"
2584                 );
2585
2586         auto cpl = dcp->cpls()[0];
2587
2588         {
2589                 Editor e (cpl->file().get());
2590                 e.replace ("Application", "Fred");
2591         }
2592
2593         check_verify_result (
2594                 {dir},
2595                 {
2596                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2597                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
2598                 });
2599 }
2600
2601
2602 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
2603 {
2604         path dir = "build/test/verify_invalid_extension_metadata2";
2605         auto dcp = make_simple (dir);
2606         dcp->write_xml (
2607                 dcp::String::compose("libdcp %1", dcp::version),
2608                 dcp::String::compose("libdcp %1", dcp::version),
2609                 dcp::LocalTime().as_string(),
2610                 "A Test DCP"
2611                 );
2612
2613         auto cpl = dcp->cpls()[0];
2614
2615         {
2616                 Editor e (cpl->file().get());
2617                 e.replace ("DCP Constraints Profile", "Fred");
2618         }
2619
2620         check_verify_result (
2621                 {dir},
2622                 {
2623                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2624                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
2625                 });
2626 }
2627
2628
2629 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
2630 {
2631         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata6";
2632         auto dcp = make_simple (dir);
2633         dcp->write_xml (
2634                 dcp::String::compose("libdcp %1", dcp::version),
2635                 dcp::String::compose("libdcp %1", dcp::version),
2636                 dcp::LocalTime().as_string(),
2637                 "A Test DCP"
2638                 );
2639
2640         auto const cpl = dcp->cpls()[0];
2641
2642         {
2643                 Editor e (cpl->file().get());
2644                 e.replace ("<meta:Value>", "<meta:ValueX>");
2645                 e.replace ("</meta:Value>", "</meta:ValueX>");
2646         }
2647
2648         check_verify_result (
2649                 {dir},
2650                 {
2651                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 79 },
2652                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:ValueX' is not allowed for content model '(Name,Value)'"), cpl->file().get(), 80 },
2653                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2654                 });
2655 }
2656
2657
2658 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
2659 {
2660         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata7";
2661         auto dcp = make_simple (dir);
2662         dcp->write_xml (
2663                 dcp::String::compose("libdcp %1", dcp::version),
2664                 dcp::String::compose("libdcp %1", dcp::version),
2665                 dcp::LocalTime().as_string(),
2666                 "A Test DCP"
2667                 );
2668
2669         auto const cpl = dcp->cpls()[0];
2670
2671         {
2672                 Editor e (cpl->file().get());
2673                 e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
2674         }
2675
2676         check_verify_result (
2677                 {dir},
2678                 {
2679                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2680                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'"), cpl->file().get() },
2681                 });
2682 }
2683
2684
2685 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
2686 {
2687         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata8";
2688         auto dcp = make_simple (dir);
2689         dcp->write_xml (
2690                 dcp::String::compose("libdcp %1", dcp::version),
2691                 dcp::String::compose("libdcp %1", dcp::version),
2692                 dcp::LocalTime().as_string(),
2693                 "A Test DCP"
2694                 );
2695
2696         auto const cpl = dcp->cpls()[0];
2697
2698         {
2699                 Editor e (cpl->file().get());
2700                 e.replace ("<meta:Property>", "<meta:PropertyX>");
2701                 e.replace ("</meta:Property>", "</meta:PropertyX>");
2702         }
2703
2704         check_verify_result (
2705                 {dir},
2706                 {
2707                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 77 },
2708                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 81 },
2709                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2710                 });
2711 }
2712
2713
2714 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
2715 {
2716         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata9";
2717         auto dcp = make_simple (dir);
2718         dcp->write_xml (
2719                 dcp::String::compose("libdcp %1", dcp::version),
2720                 dcp::String::compose("libdcp %1", dcp::version),
2721                 dcp::LocalTime().as_string(),
2722                 "A Test DCP"
2723                 );
2724
2725         auto const cpl = dcp->cpls()[0];
2726
2727         {
2728                 Editor e (cpl->file().get());
2729                 e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
2730                 e.replace ("</meta:PropertyList>", "</meta:PropertyListX>");
2731         }
2732
2733         check_verify_result (
2734                 {dir},
2735                 {
2736                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 76 },
2737                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyListX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 82 },
2738                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2739                 });
2740 }
2741
2742
2743
2744 BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
2745 {
2746         path dir = "build/test/verify_unsigned_cpl_with_encrypted_content";
2747         prepare_directory (dir);
2748         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2749                 copy_file (i.path(), dir / i.path().filename());
2750         }
2751
2752         path const pkl = dir / ( "pkl_" + encryption_test_pkl_id + ".xml" );
2753         path const cpl = dir / ( "cpl_" + encryption_test_cpl_id + ".xml");
2754
2755         {
2756                 Editor e (cpl);
2757                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2758         }
2759
2760         check_verify_result (
2761                 {dir},
2762                 {
2763                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id, canonical(cpl) },
2764                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl), },
2765                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2766                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2767                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2768                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2769                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2770                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, encryption_test_cpl_id, canonical(cpl) }
2771                 });
2772 }
2773
2774
2775 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_encrypted_content)
2776 {
2777         path dir = "build/test/unsigned_pkl_with_encrypted_content";
2778         prepare_directory (dir);
2779         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2780                 copy_file (i.path(), dir / i.path().filename());
2781         }
2782
2783         path const cpl = dir / ("cpl_" + encryption_test_cpl_id + ".xml");
2784         path const pkl = dir / ("pkl_" + encryption_test_pkl_id + ".xml");
2785         {
2786                 Editor e (pkl);
2787                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2788         }
2789
2790         check_verify_result (
2791                 {dir},
2792                 {
2793                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl) },
2794                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2795                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2796                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2797                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2798                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2799                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, encryption_test_pkl_id, canonical(pkl) },
2800                 });
2801 }
2802
2803
2804 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_unencrypted_content)
2805 {
2806         path dir = "build/test/verify_unsigned_pkl_with_unencrypted_content";
2807         prepare_directory (dir);
2808         for (auto i: directory_iterator("test/ref/DCP/dcp_test1")) {
2809                 copy_file (i.path(), dir / i.path().filename());
2810         }
2811
2812         {
2813                 Editor e (dir / dcp_test1_pkl);
2814                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2815         }
2816
2817         check_verify_result ({dir}, {});
2818 }
2819
2820
2821 BOOST_AUTO_TEST_CASE (verify_partially_encrypted)
2822 {
2823         path dir ("build/test/verify_must_not_be_partially_encrypted");
2824         prepare_directory (dir);
2825
2826         dcp::DCP d (dir);
2827
2828         auto signer = make_shared<dcp::CertificateChain>();
2829         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem")));
2830         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem")));
2831         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem")));
2832         signer->set_key (dcp::file_to_string("test/ref/crypt/leaf.key"));
2833
2834         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2835
2836         dcp::Key key;
2837
2838         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
2839         mp->set_key (key);
2840
2841         auto writer = mp->start_write (dir / "video.mxf", false);
2842         dcp::ArrayData j2c ("test/data/flat_red.j2c");
2843         for (int i = 0; i < 24; ++i) {
2844                 writer->write (j2c.data(), j2c.size());
2845         }
2846         writer->finalize ();
2847
2848         auto ms = simple_sound (dir, "", dcp::MXFMetadata(), "de-DE");
2849
2850         auto reel = make_shared<dcp::Reel>(
2851                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2852                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2853                 );
2854
2855         reel->add (simple_markers());
2856
2857         cpl->add (reel);
2858
2859         cpl->set_content_version (
2860                 {"urn:uri:81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00", "81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00"}
2861                 );
2862         cpl->set_annotation_text ("A Test DCP");
2863         cpl->set_issuer ("OpenDCP 0.0.25");
2864         cpl->set_creator ("OpenDCP 0.0.25");
2865         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
2866         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
2867         cpl->set_main_sound_sample_rate (48000);
2868         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
2869         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
2870         cpl->set_version_number (1);
2871
2872         d.add (cpl);
2873
2874         d.write_xml ("OpenDCP 0.0.25", "OpenDCP 0.0.25", "2012-07-17T04:45:18+00:00", "A Test DCP", signer);
2875
2876         check_verify_result (
2877                 {dir},
2878                 {
2879                         {dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED},
2880                 });
2881 }
2882
2883
2884 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_2k)
2885 {
2886         vector<dcp::VerificationNote> notes;
2887         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV", "j2c.mxf"));
2888         auto reader = picture.start_read ();
2889         auto frame = reader->get_frame (0);
2890         verify_j2k (frame, notes);
2891         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2892 }
2893
2894
2895 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_4k)
2896 {
2897         vector<dcp::VerificationNote> notes;
2898         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "sul", "TLR"));
2899         auto reader = picture.start_read ();
2900         auto frame = reader->get_frame (0);
2901         verify_j2k (frame, notes);
2902         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2903 }
2904
2905
2906 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_libdcp)
2907 {
2908         boost::filesystem::path dir = "build/test/verify_jpeg2000_codestream_libdcp";
2909         prepare_directory (dir);
2910         auto dcp = make_simple (dir);
2911         dcp->write_xml ();
2912         vector<dcp::VerificationNote> notes;
2913         dcp::MonoPictureAsset picture (find_file(dir, "video"));
2914         auto reader = picture.start_read ();
2915         auto frame = reader->get_frame (0);
2916         verify_j2k (frame, notes);
2917         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2918 }
2919
2920
2921 /** Check that ResourceID and the XML ID being different is spotted */
2922 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_resource_id)
2923 {
2924         boost::filesystem::path const dir = "build/test/verify_mismatched_subtitle_resource_id";
2925         prepare_directory (dir);
2926
2927         ASDCP::WriterInfo writer_info;
2928         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
2929
2930         unsigned int c;
2931         auto mxf_id = dcp::make_uuid ();
2932         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
2933         BOOST_REQUIRE (c == Kumu::UUID_Length);
2934
2935         auto resource_id = dcp::make_uuid ();
2936         ASDCP::TimedText::TimedTextDescriptor descriptor;
2937         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
2938         DCP_ASSERT (c == Kumu::UUID_Length);
2939
2940         auto xml_id = dcp::make_uuid ();
2941         ASDCP::TimedText::MXFWriter writer;
2942         auto subs_mxf = dir / "subs.mxf";
2943         auto r = writer.OpenWrite(subs_mxf.c_str(), writer_info, descriptor, 4096);
2944         BOOST_REQUIRE (ASDCP_SUCCESS(r));
2945         writer.WriteTimedTextResource (dcp::String::compose(
2946                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
2947                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
2948                 "<Id>urn:uuid:%1</Id>"
2949                 "<ContentTitleText>Content</ContentTitleText>"
2950                 "<AnnotationText>Annotation</AnnotationText>"
2951                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
2952                 "<ReelNumber>1</ReelNumber>"
2953                 "<Language>en-US</Language>"
2954                 "<EditRate>25 1</EditRate>"
2955                 "<TimeCodeRate>25</TimeCodeRate>"
2956                 "<StartTime>00:00:00:00</StartTime>"
2957                 "<SubtitleList>"
2958                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
2959                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
2960                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
2961                 "</Subtitle>"
2962                 "</Font>"
2963                 "</SubtitleList>"
2964                 "</SubtitleReel>",
2965                 xml_id).c_str());
2966
2967         writer.Finalize();
2968
2969         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
2970         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
2971
2972         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
2973
2974         check_verify_result (
2975                 { dir },
2976                 {
2977                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
2978                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID },
2979                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
2980                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2981                 });
2982 }
2983
2984
2985 /** Check that ResourceID and the MXF ID being the same is spotted */
2986 BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
2987 {
2988         boost::filesystem::path const dir = "build/test/verify_incorrect_timed_text_id";
2989         prepare_directory (dir);
2990
2991         ASDCP::WriterInfo writer_info;
2992         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
2993
2994         unsigned int c;
2995         auto mxf_id = dcp::make_uuid ();
2996         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
2997         BOOST_REQUIRE (c == Kumu::UUID_Length);
2998
2999         auto resource_id = mxf_id;
3000         ASDCP::TimedText::TimedTextDescriptor descriptor;
3001         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3002         DCP_ASSERT (c == Kumu::UUID_Length);
3003
3004         auto xml_id = resource_id;
3005         ASDCP::TimedText::MXFWriter writer;
3006         auto subs_mxf = dir / "subs.mxf";
3007         auto r = writer.OpenWrite(subs_mxf.c_str(), writer_info, descriptor, 4096);
3008         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3009         writer.WriteTimedTextResource (dcp::String::compose(
3010                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3011                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3012                 "<Id>urn:uuid:%1</Id>"
3013                 "<ContentTitleText>Content</ContentTitleText>"
3014                 "<AnnotationText>Annotation</AnnotationText>"
3015                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3016                 "<ReelNumber>1</ReelNumber>"
3017                 "<Language>en-US</Language>"
3018                 "<EditRate>25 1</EditRate>"
3019                 "<TimeCodeRate>25</TimeCodeRate>"
3020                 "<StartTime>00:00:00:00</StartTime>"
3021                 "<SubtitleList>"
3022                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3023                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3024                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3025                 "</Subtitle>"
3026                 "</Font>"
3027                 "</SubtitleList>"
3028                 "</SubtitleReel>",
3029                 xml_id).c_str());
3030
3031         writer.Finalize();
3032
3033         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3034         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3035
3036         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3037
3038         check_verify_result (
3039                 { dir },
3040                 {
3041                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3042                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID },
3043                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3044                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3045                 });
3046 }
3047
3048
3049 /** Check a DCP with a 3D asset marked as 2D */
3050 BOOST_AUTO_TEST_CASE (verify_threed_marked_as_twod)
3051 {
3052         check_verify_result (
3053                 { private_test / "data" / "xm" },
3054                 {
3055                         {
3056                                 dcp::VerificationNote::Type::WARNING,
3057                                 dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, boost::filesystem::canonical(find_file(private_test / "data" / "xm", "j2c"))
3058                         },
3059                         {
3060                                 dcp::VerificationNote::Type::BV21_ERROR,
3061                                 dcp::VerificationNote::Code::INVALID_STANDARD
3062                         },
3063                 });
3064
3065 }
3066