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