c8d0e579046db80084384a3aedb01a45b3d48e8d
[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, dcp::VAlign v_align = dcp::VAlign::CENTER, 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                         v_align,
1255                         dcp::Direction::LTR,
1256                         text,
1257                         dcp::Effect::NONE,
1258                         dcp::Colour(),
1259                         dcp::Time(),
1260                         dcp::Time(),
1261                         0
1262                 )
1263         );
1264 }
1265
1266
1267 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
1268 {
1269         path const dir("build/test/verify_invalid_closed_caption_xml_size_in_bytes");
1270         prepare_directory (dir);
1271
1272         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1273         for (int i = 0; i < 2048; ++i) {
1274                 add_test_subtitle (asset, i * 24, i * 24 + 20);
1275         }
1276         asset->set_language (dcp::LanguageTag("de-DE"));
1277         asset->write (dir / "subs.mxf");
1278         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
1279         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1280
1281         check_verify_result (
1282                 { dir },
1283                 {
1284                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1285                         {
1286                                 dcp::VerificationNote::Type::BV21_ERROR,
1287                                 dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES,
1288                                 string("372207"),
1289                                 canonical(dir / "subs.mxf")
1290                         },
1291                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1292                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1293                 });
1294 }
1295
1296
1297 static
1298 shared_ptr<dcp::SMPTESubtitleAsset>
1299 make_large_subtitle_asset (path font_file)
1300 {
1301         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1302         dcp::ArrayData big_fake_font(1024 * 1024);
1303         big_fake_font.write (font_file);
1304         for (int i = 0; i < 116; ++i) {
1305                 asset->add_font (dcp::String::compose("big%1", i), big_fake_font);
1306         }
1307         return asset;
1308 }
1309
1310
1311 template <class T>
1312 void
1313 verify_timed_text_asset_too_large (string name)
1314 {
1315         auto const dir = path("build/test") / name;
1316         prepare_directory (dir);
1317         auto asset = make_large_subtitle_asset (dir / "font.ttf");
1318         add_test_subtitle (asset, 0, 240);
1319         asset->set_language (dcp::LanguageTag("de-DE"));
1320         asset->write (dir / "subs.mxf");
1321
1322         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), 240, 0);
1323         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1324
1325         check_verify_result (
1326                 { dir },
1327                 {
1328                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121695136"), canonical(dir / "subs.mxf") },
1329                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, string("121634816"), canonical(dir / "subs.mxf") },
1330                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1331                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1332                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1333                 });
1334 }
1335
1336
1337 BOOST_AUTO_TEST_CASE (verify_subtitle_asset_too_large)
1338 {
1339         verify_timed_text_asset_too_large<dcp::ReelSMPTESubtitleAsset>("verify_subtitle_asset_too_large");
1340         verify_timed_text_asset_too_large<dcp::ReelSMPTEClosedCaptionAsset>("verify_closed_caption_asset_too_large");
1341 }
1342
1343
1344 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_language)
1345 {
1346         path dir = "build/test/verify_missing_subtitle_language";
1347         prepare_directory (dir);
1348         auto dcp = make_simple (dir, 1, 106);
1349
1350         string const xml =
1351                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1352                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1353                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1354                 "<ContentTitleText>Content</ContentTitleText>"
1355                 "<AnnotationText>Annotation</AnnotationText>"
1356                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1357                 "<ReelNumber>1</ReelNumber>"
1358                 "<EditRate>24 1</EditRate>"
1359                 "<TimeCodeRate>24</TimeCodeRate>"
1360                 "<StartTime>00:00:00:00</StartTime>"
1361                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1362                 "<SubtitleList>"
1363                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1364                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1365                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1366                 "</Subtitle>"
1367                 "</Font>"
1368                 "</SubtitleList>"
1369                 "</SubtitleReel>";
1370
1371         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1372         BOOST_REQUIRE (xml_file);
1373         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1374         fclose (xml_file);
1375         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1376         subs->write (dir / "subs.mxf");
1377
1378         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1379         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1380         dcp->write_xml (
1381                 dcp::String::compose("libdcp %1", dcp::version),
1382                 dcp::String::compose("libdcp %1", dcp::version),
1383                 dcp::LocalTime().as_string(),
1384                 "A Test DCP"
1385                 );
1386
1387         check_verify_result (
1388                 { dir },
1389                 {
1390                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
1391                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1392                 });
1393 }
1394
1395
1396 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_languages)
1397 {
1398         path path ("build/test/verify_mismatched_subtitle_languages");
1399         auto constexpr reel_length = 192;
1400         auto dcp = make_simple (path, 2, reel_length);
1401         auto cpl = dcp->cpls()[0];
1402
1403         {
1404                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1405                 subs->set_language (dcp::LanguageTag("de-DE"));
1406                 subs->add (simple_subtitle());
1407                 subs->write (path / "subs1.mxf");
1408                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1409                 cpl->reels()[0]->add(reel_subs);
1410         }
1411
1412         {
1413                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1414                 subs->set_language (dcp::LanguageTag("en-US"));
1415                 subs->add (simple_subtitle());
1416                 subs->write (path / "subs2.mxf");
1417                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1418                 cpl->reels()[1]->add(reel_subs);
1419         }
1420
1421         dcp->write_xml (
1422                 dcp::String::compose("libdcp %1", dcp::version),
1423                 dcp::String::compose("libdcp %1", dcp::version),
1424                 dcp::LocalTime().as_string(),
1425                 "A Test DCP"
1426                 );
1427
1428         check_verify_result (
1429                 { path },
1430                 {
1431                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1432                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") },
1433                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES }
1434                 });
1435 }
1436
1437
1438 BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
1439 {
1440         path path ("build/test/verify_multiple_closed_caption_languages_allowed");
1441         auto constexpr reel_length = 192;
1442         auto dcp = make_simple (path, 2, reel_length);
1443         auto cpl = dcp->cpls()[0];
1444
1445         {
1446                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1447                 ccaps->set_language (dcp::LanguageTag("de-DE"));
1448                 ccaps->add (simple_subtitle());
1449                 ccaps->write (path / "subs1.mxf");
1450                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1451                 cpl->reels()[0]->add(reel_ccaps);
1452         }
1453
1454         {
1455                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1456                 ccaps->set_language (dcp::LanguageTag("en-US"));
1457                 ccaps->add (simple_subtitle());
1458                 ccaps->write (path / "subs2.mxf");
1459                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1460                 cpl->reels()[1]->add(reel_ccaps);
1461         }
1462
1463         dcp->write_xml (
1464                 dcp::String::compose("libdcp %1", dcp::version),
1465                 dcp::String::compose("libdcp %1", dcp::version),
1466                 dcp::LocalTime().as_string(),
1467                 "A Test DCP"
1468                 );
1469
1470         check_verify_result (
1471                 { path },
1472                 {
1473                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1474                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") }
1475                 });
1476 }
1477
1478
1479 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_start_time)
1480 {
1481         path dir = "build/test/verify_missing_subtitle_start_time";
1482         prepare_directory (dir);
1483         auto dcp = make_simple (dir, 1, 106);
1484
1485         string const xml =
1486                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1487                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1488                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1489                 "<ContentTitleText>Content</ContentTitleText>"
1490                 "<AnnotationText>Annotation</AnnotationText>"
1491                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1492                 "<ReelNumber>1</ReelNumber>"
1493                 "<Language>de-DE</Language>"
1494                 "<EditRate>24 1</EditRate>"
1495                 "<TimeCodeRate>24</TimeCodeRate>"
1496                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1497                 "<SubtitleList>"
1498                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1499                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1500                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1501                 "</Subtitle>"
1502                 "</Font>"
1503                 "</SubtitleList>"
1504                 "</SubtitleReel>";
1505
1506         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1507         BOOST_REQUIRE (xml_file);
1508         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1509         fclose (xml_file);
1510         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1511         subs->write (dir / "subs.mxf");
1512
1513         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1514         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1515         dcp->write_xml (
1516                 dcp::String::compose("libdcp %1", dcp::version),
1517                 dcp::String::compose("libdcp %1", dcp::version),
1518                 dcp::LocalTime().as_string(),
1519                 "A Test DCP"
1520                 );
1521
1522         check_verify_result (
1523                 { dir },
1524                 {
1525                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1526                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1527                 });
1528 }
1529
1530
1531 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_start_time)
1532 {
1533         path dir = "build/test/verify_invalid_subtitle_start_time";
1534         prepare_directory (dir);
1535         auto dcp = make_simple (dir, 1, 106);
1536
1537         string const xml =
1538                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1539                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1540                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1541                 "<ContentTitleText>Content</ContentTitleText>"
1542                 "<AnnotationText>Annotation</AnnotationText>"
1543                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1544                 "<ReelNumber>1</ReelNumber>"
1545                 "<Language>de-DE</Language>"
1546                 "<EditRate>24 1</EditRate>"
1547                 "<TimeCodeRate>24</TimeCodeRate>"
1548                 "<StartTime>00:00:02:00</StartTime>"
1549                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1550                 "<SubtitleList>"
1551                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1552                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1553                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1554                 "</Subtitle>"
1555                 "</Font>"
1556                 "</SubtitleList>"
1557                 "</SubtitleReel>";
1558
1559         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1560         BOOST_REQUIRE (xml_file);
1561         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1562         fclose (xml_file);
1563         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1564         subs->write (dir / "subs.mxf");
1565
1566         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1567         dcp->cpls().front()->reels().front()->add(reel_subs);
1568         dcp->write_xml (
1569                 dcp::String::compose("libdcp %1", dcp::version),
1570                 dcp::String::compose("libdcp %1", dcp::version),
1571                 dcp::LocalTime().as_string(),
1572                 "A Test DCP"
1573                 );
1574
1575         check_verify_result (
1576                 { dir },
1577                 {
1578                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1579                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1580                 });
1581 }
1582
1583
1584 class TestText
1585 {
1586 public:
1587         TestText (int in_, int out_, float v_position_ = 0, dcp::VAlign v_align_ = dcp::VAlign::CENTER, string text_ = "Hello")
1588                 : in(in_)
1589                 , out(out_)
1590                 , v_position(v_position_)
1591                 , v_align(v_align_)
1592                 , text(text_)
1593         {}
1594
1595         int in;
1596         int out;
1597         float v_position;
1598         dcp::VAlign v_align;
1599         string text;
1600 };
1601
1602
1603 template <class T>
1604 shared_ptr<dcp::CPL>
1605 dcp_with_text (path dir, vector<TestText> subs)
1606 {
1607         prepare_directory (dir);
1608         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1609         asset->set_start_time (dcp::Time());
1610         for (auto i: subs) {
1611                 add_test_subtitle (asset, i.in, i.out, i.v_position, i.v_align, i.text);
1612         }
1613         asset->set_language (dcp::LanguageTag("de-DE"));
1614         asset->write (dir / "subs.mxf");
1615
1616         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1617         return write_dcp_with_single_asset (dir, reel_asset);
1618 }
1619
1620
1621 template <class T>
1622 shared_ptr<dcp::CPL>
1623 dcp_with_text_from_file (path dir, boost::filesystem::path subs_xml)
1624 {
1625         prepare_directory (dir);
1626         auto asset = make_shared<dcp::SMPTESubtitleAsset>(subs_xml);
1627         asset->set_start_time (dcp::Time());
1628         asset->set_language (dcp::LanguageTag("de-DE"));
1629
1630         auto subs_mxf = dir / "subs.mxf";
1631         asset->write (subs_mxf);
1632
1633         /* The call to write() puts the asset into the DCP correctly but it will have
1634          * XML re-written by our parser.  Overwrite the MXF using the given file's verbatim
1635          * contents.
1636          */
1637         ASDCP::TimedText::MXFWriter writer;
1638         ASDCP::WriterInfo writer_info;
1639         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
1640         unsigned int c;
1641         Kumu::hex2bin (asset->id().c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
1642         DCP_ASSERT (c == Kumu::UUID_Length);
1643         ASDCP::TimedText::TimedTextDescriptor descriptor;
1644         descriptor.ContainerDuration = asset->intrinsic_duration();
1645         Kumu::hex2bin (asset->xml_id()->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
1646         DCP_ASSERT (c == Kumu::UUID_Length);
1647         ASDCP::Result_t r = writer.OpenWrite (subs_mxf.string().c_str(), writer_info, descriptor, 16384);
1648         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1649         r = writer.WriteTimedTextResource (dcp::file_to_string(subs_xml));
1650         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1651         writer.Finalize ();
1652
1653         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1654         return write_dcp_with_single_asset (dir, reel_asset);
1655 }
1656
1657
1658 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
1659 {
1660         auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
1661         /* Just too early */
1662         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
1663         check_verify_result (
1664                 { dir },
1665                 {
1666                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1667                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1668                 });
1669
1670 }
1671
1672
1673 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time)
1674 {
1675         auto const dir = path("build/test/verify_valid_subtitle_first_text_time");
1676         /* Just late enough */
1677         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
1678         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1679 }
1680
1681
1682 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time_on_second_reel)
1683 {
1684         auto const dir = path("build/test/verify_valid_subtitle_first_text_time_on_second_reel");
1685         prepare_directory (dir);
1686
1687         auto asset1 = make_shared<dcp::SMPTESubtitleAsset>();
1688         asset1->set_start_time (dcp::Time());
1689         /* Just late enough */
1690         add_test_subtitle (asset1, 4 * 24, 5 * 24);
1691         asset1->set_language (dcp::LanguageTag("de-DE"));
1692         asset1->write (dir / "subs1.mxf");
1693         auto reel_asset1 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset1, dcp::Fraction(24, 1), 5 * 24, 0);
1694         auto reel1 = make_shared<dcp::Reel>();
1695         reel1->add (reel_asset1);
1696         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 5 * 24, 0);
1697         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
1698         reel1->add (markers1);
1699
1700         auto asset2 = make_shared<dcp::SMPTESubtitleAsset>();
1701         asset2->set_start_time (dcp::Time());
1702         /* This would be too early on first reel but should be OK on the second */
1703         add_test_subtitle (asset2, 3, 4 * 24);
1704         asset2->set_language (dcp::LanguageTag("de-DE"));
1705         asset2->write (dir / "subs2.mxf");
1706         auto reel_asset2 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset2, dcp::Fraction(24, 1), 4 * 24, 0);
1707         auto reel2 = make_shared<dcp::Reel>();
1708         reel2->add (reel_asset2);
1709         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 4 * 24, 0);
1710         markers2->set (dcp::Marker::LFOC, dcp::Time(4 * 24 - 1, 24, 24));
1711         reel2->add (markers2);
1712
1713         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1714         cpl->add (reel1);
1715         cpl->add (reel2);
1716         auto dcp = make_shared<dcp::DCP>(dir);
1717         dcp->add (cpl);
1718         dcp->write_xml (
1719                 dcp::String::compose("libdcp %1", dcp::version),
1720                 dcp::String::compose("libdcp %1", dcp::version),
1721                 dcp::LocalTime().as_string(),
1722                 "hello"
1723                 );
1724
1725
1726         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1727 }
1728
1729
1730 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_spacing)
1731 {
1732         auto const dir = path("build/test/verify_invalid_subtitle_spacing");
1733         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1734                 dir,
1735                 {
1736                         { 4 * 24,     5 * 24 },
1737                         { 5 * 24 + 1, 6 * 24 },
1738                 });
1739         check_verify_result (
1740                 {dir},
1741                 {
1742                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING },
1743                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1744                 });
1745 }
1746
1747
1748 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_spacing)
1749 {
1750         auto const dir = path("build/test/verify_valid_subtitle_spacing");
1751         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1752                 dir,
1753                 {
1754                         { 4 * 24,      5 * 24 },
1755                         { 5 * 24 + 16, 8 * 24 },
1756                 });
1757         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1758 }
1759
1760
1761 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_duration)
1762 {
1763         auto const dir = path("build/test/verify_invalid_subtitle_duration");
1764         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
1765         check_verify_result (
1766                 {dir},
1767                 {
1768                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION },
1769                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1770                 });
1771 }
1772
1773
1774 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_duration)
1775 {
1776         auto const dir = path("build/test/verify_valid_subtitle_duration");
1777         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
1778         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1779 }
1780
1781
1782 BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
1783 {
1784         auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
1785         prepare_directory (dir);
1786         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1787         asset->set_start_time (dcp::Time());
1788         add_test_subtitle (asset, 0, 4 * 24);
1789         asset->set_language (dcp::LanguageTag("de-DE"));
1790         asset->write (dir / "subs.mxf");
1791
1792         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
1793         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1794         check_verify_result (
1795                 {dir},
1796                 {
1797                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "72 96", boost::filesystem::canonical(asset->file().get()) },
1798                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1799                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
1800                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1801                 });
1802
1803 }
1804
1805
1806 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
1807 {
1808         auto const dir = path ("build/test/invalid_subtitle_line_count1");
1809         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1810                 dir,
1811                 {
1812                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1813                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1814                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1815                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1816                 });
1817         check_verify_result (
1818                 {dir},
1819                 {
1820                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1821                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1822                 });
1823 }
1824
1825
1826 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
1827 {
1828         auto const dir = path ("build/test/verify_valid_subtitle_line_count1");
1829         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1830                 dir,
1831                 {
1832                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1833                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1834                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1835                 });
1836         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1837 }
1838
1839
1840 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
1841 {
1842         auto const dir = path ("build/test/verify_invalid_subtitle_line_count2");
1843         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1844                 dir,
1845                 {
1846                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1847                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1848                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1849                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
1850                 });
1851         check_verify_result (
1852                 {dir},
1853                 {
1854                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1855                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1856                 });
1857 }
1858
1859
1860 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
1861 {
1862         auto const dir = path ("build/test/verify_valid_subtitle_line_count2");
1863         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1864                 dir,
1865                 {
1866                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1867                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1868                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1869                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
1870                 });
1871         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1872 }
1873
1874
1875 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
1876 {
1877         auto const dir = path ("build/test/verify_invalid_subtitle_line_length1");
1878         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1879                 dir,
1880                 {
1881                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123" }
1882                 });
1883         check_verify_result (
1884                 {dir},
1885                 {
1886                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH },
1887                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1888                 });
1889 }
1890
1891
1892 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
1893 {
1894         auto const dir = path ("build/test/verify_invalid_subtitle_line_length2");
1895         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1896                 dir,
1897                 {
1898                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
1899                 });
1900         check_verify_result (
1901                 {dir},
1902                 {
1903                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH },
1904                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1905                 });
1906 }
1907
1908
1909 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
1910 {
1911         auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
1912         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1913                 dir,
1914                 {
1915                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1916                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1917                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1918                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1919                 });
1920         check_verify_result (
1921                 {dir},
1922                 {
1923                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1924                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1925                 });
1926 }
1927
1928
1929 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
1930 {
1931         auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
1932         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1933                 dir,
1934                 {
1935                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1936                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1937                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1938                 });
1939         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1940 }
1941
1942
1943 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
1944 {
1945         auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
1946         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1947                 dir,
1948                 {
1949                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1950                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1951                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1952                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
1953                 });
1954         check_verify_result (
1955                 {dir},
1956                 {
1957                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1958                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1959                 });
1960 }
1961
1962
1963 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
1964 {
1965         auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
1966         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1967                 dir,
1968                 {
1969                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1970                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1971                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1972                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
1973                 });
1974         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1975 }
1976
1977
1978 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
1979 {
1980         auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
1981         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1982                 dir,
1983                 {
1984                         { 96, 300, 0.0, dcp::VAlign::CENTER, "0123456789012345678901234567890123" }
1985                 });
1986         check_verify_result (
1987                 {dir},
1988                 {
1989                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH },
1990                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1991                 });
1992 }
1993
1994
1995 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign1)
1996 {
1997         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign1");
1998         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1999                 dir,
2000                 {
2001                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2002                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2003                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2004                 });
2005         check_verify_result (
2006                 {dir},
2007                 {
2008                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2009                 });
2010 }
2011
2012
2013 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign2)
2014 {
2015         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign2");
2016         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2017                 dir,
2018                 {
2019                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2020                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2021                         { 96, 300, 0.2, dcp::VAlign::CENTER, "not fine" },
2022                 });
2023         check_verify_result (
2024                 {dir},
2025                 {
2026                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN },
2027                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2028                 });
2029 }
2030
2031
2032 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering1)
2033 {
2034         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering1");
2035         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2036                 dir,
2037                 {
2038                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2039                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2040                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2041                 });
2042         check_verify_result (
2043                 {dir},
2044                 {
2045                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2046                 });
2047 }
2048
2049
2050 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering2)
2051 {
2052         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering2");
2053         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2054                 dir,
2055                 {
2056                         { 96, 300, 0.2, dcp::VAlign::BOTTOM, "This" },
2057                         { 96, 300, 0.1, dcp::VAlign::BOTTOM, "is" },
2058                         { 96, 300, 0.0, dcp::VAlign::BOTTOM, "also fine" },
2059                 });
2060         check_verify_result (
2061                 {dir},
2062                 {
2063                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2064                 });
2065 }
2066
2067
2068 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering3)
2069 {
2070         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering3");
2071         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering3.xml");
2072         check_verify_result (
2073                 {dir},
2074                 {
2075                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING },
2076                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2077                 });
2078 }
2079
2080
2081 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering4)
2082 {
2083         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering4");
2084         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering4.xml");
2085         check_verify_result (
2086                 {dir},
2087                 {
2088                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2089                 });
2090 }
2091
2092
2093
2094 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
2095 {
2096         path const dir("build/test/verify_invalid_sound_frame_rate");
2097         prepare_directory (dir);
2098
2099         auto picture = simple_picture (dir, "foo");
2100         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
2101         auto reel = make_shared<dcp::Reel>();
2102         reel->add (reel_picture);
2103         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000, boost::none);
2104         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
2105         reel->add (reel_sound);
2106         reel->add (simple_markers());
2107         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2108         cpl->add (reel);
2109         auto dcp = make_shared<dcp::DCP>(dir);
2110         dcp->add (cpl);
2111         dcp->write_xml (
2112                 dcp::String::compose("libdcp %1", dcp::version),
2113                 dcp::String::compose("libdcp %1", dcp::version),
2114                 dcp::LocalTime().as_string(),
2115                 "hello"
2116                 );
2117
2118         check_verify_result (
2119                 {dir},
2120                 {
2121                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE, string("96000"), canonical(dir / "audiofoo.mxf") },
2122                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2123                 });
2124 }
2125
2126
2127 BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
2128 {
2129         path const dir("build/test/verify_missing_cpl_annotation_text");
2130         auto dcp = make_simple (dir);
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         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2139
2140         auto const cpl = dcp->cpls()[0];
2141
2142         {
2143                 BOOST_REQUIRE (cpl->file());
2144                 Editor e(cpl->file().get());
2145                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "");
2146         }
2147
2148         check_verify_result (
2149                 {dir},
2150                 {
2151                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2152                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2153                 });
2154 }
2155
2156
2157 BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
2158 {
2159         path const dir("build/test/verify_mismatched_cpl_annotation_text");
2160         auto dcp = make_simple (dir);
2161         dcp->write_xml (
2162                 dcp::String::compose("libdcp %1", dcp::version),
2163                 dcp::String::compose("libdcp %1", dcp::version),
2164                 dcp::LocalTime().as_string(),
2165                 "A Test DCP"
2166                 );
2167
2168         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2169         auto const cpl = dcp->cpls()[0];
2170
2171         {
2172                 BOOST_REQUIRE (cpl->file());
2173                 Editor e(cpl->file().get());
2174                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "<AnnotationText>A Test DCP 1</AnnotationText>");
2175         }
2176
2177         check_verify_result (
2178                 {dir},
2179                 {
2180                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2181                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2182                 });
2183 }
2184
2185
2186 BOOST_AUTO_TEST_CASE (verify_mismatched_asset_duration)
2187 {
2188         path const dir("build/test/verify_mismatched_asset_duration");
2189         prepare_directory (dir);
2190         shared_ptr<dcp::DCP> dcp (new dcp::DCP(dir));
2191         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2192
2193         shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (dir, "", 24);
2194         shared_ptr<dcp::SoundAsset> ms = simple_sound (dir, "", dcp::MXFMetadata(), "en-US", 25);
2195
2196         auto reel = make_shared<dcp::Reel>(
2197                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2198                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2199                 );
2200
2201         reel->add (simple_markers());
2202         cpl->add (reel);
2203
2204         dcp->add (cpl);
2205         dcp->write_xml (
2206                 dcp::String::compose("libdcp %1", dcp::version),
2207                 dcp::String::compose("libdcp %1", dcp::version),
2208                 dcp::LocalTime().as_string(),
2209                 "A Test DCP"
2210                 );
2211
2212         check_verify_result (
2213                 {dir},
2214                 {
2215                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION },
2216                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), canonical(cpl->file().get()) }
2217                 });
2218 }
2219
2220
2221
2222 static
2223 shared_ptr<dcp::CPL>
2224 verify_subtitles_must_be_in_all_reels_check (path dir, bool add_to_reel1, bool add_to_reel2)
2225 {
2226         prepare_directory (dir);
2227         auto dcp = make_shared<dcp::DCP>(dir);
2228         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2229
2230         auto constexpr reel_length = 192;
2231
2232         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2233         subs->set_language (dcp::LanguageTag("de-DE"));
2234         subs->set_start_time (dcp::Time());
2235         subs->add (simple_subtitle());
2236         subs->write (dir / "subs.mxf");
2237         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
2238
2239         auto reel1 = make_shared<dcp::Reel>(
2240                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2241                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2242                 );
2243
2244         if (add_to_reel1) {
2245                 reel1->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2246         }
2247
2248         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2249         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2250         reel1->add (markers1);
2251
2252         cpl->add (reel1);
2253
2254         auto reel2 = make_shared<dcp::Reel>(
2255                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2256                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2257                 );
2258
2259         if (add_to_reel2) {
2260                 reel2->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2261         }
2262
2263         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2264         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2265         reel2->add (markers2);
2266
2267         cpl->add (reel2);
2268
2269         dcp->add (cpl);
2270         dcp->write_xml (
2271                 dcp::String::compose("libdcp %1", dcp::version),
2272                 dcp::String::compose("libdcp %1", dcp::version),
2273                 dcp::LocalTime().as_string(),
2274                 "A Test DCP"
2275                 );
2276
2277         return cpl;
2278 }
2279
2280
2281 BOOST_AUTO_TEST_CASE (verify_missing_main_subtitle_from_some_reels)
2282 {
2283         {
2284                 path dir ("build/test/missing_main_subtitle_from_some_reels");
2285                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, false);
2286                 check_verify_result (
2287                         { dir },
2288                         {
2289                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS },
2290                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2291                         });
2292
2293         }
2294
2295         {
2296                 path dir ("build/test/verify_subtitles_must_be_in_all_reels2");
2297                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, true);
2298                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2299         }
2300
2301         {
2302                 path dir ("build/test/verify_subtitles_must_be_in_all_reels1");
2303                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, false, false);
2304                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2305         }
2306 }
2307
2308
2309 static
2310 shared_ptr<dcp::CPL>
2311 verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1, int caps_in_reel2)
2312 {
2313         prepare_directory (dir);
2314         auto dcp = make_shared<dcp::DCP>(dir);
2315         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2316
2317         auto constexpr reel_length = 192;
2318
2319         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2320         subs->set_language (dcp::LanguageTag("de-DE"));
2321         subs->set_start_time (dcp::Time());
2322         subs->add (simple_subtitle());
2323         subs->write (dir / "subs.mxf");
2324
2325         auto reel1 = make_shared<dcp::Reel>(
2326                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2327                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2328                 );
2329
2330         for (int i = 0; i < caps_in_reel1; ++i) {
2331                 reel1->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2332         }
2333
2334         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2335         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2336         reel1->add (markers1);
2337
2338         cpl->add (reel1);
2339
2340         auto reel2 = make_shared<dcp::Reel>(
2341                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2342                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2343                 );
2344
2345         for (int i = 0; i < caps_in_reel2; ++i) {
2346                 reel2->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2347         }
2348
2349         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2350         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2351         reel2->add (markers2);
2352
2353         cpl->add (reel2);
2354
2355         dcp->add (cpl);
2356         dcp->write_xml (
2357                 dcp::String::compose("libdcp %1", dcp::version),
2358                 dcp::String::compose("libdcp %1", dcp::version),
2359                 dcp::LocalTime().as_string(),
2360                 "A Test DCP"
2361                 );
2362
2363         return cpl;
2364 }
2365
2366
2367 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_asset_counts)
2368 {
2369         {
2370                 path dir ("build/test/mismatched_closed_caption_asset_counts");
2371                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 3, 4);
2372                 check_verify_result (
2373                         {dir},
2374                         {
2375                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS },
2376                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2377                         });
2378         }
2379
2380         {
2381                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels2");
2382                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 4, 4);
2383                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2384         }
2385
2386         {
2387                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels3");
2388                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 0, 0);
2389                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2390         }
2391 }
2392
2393
2394 template <class T>
2395 void
2396 verify_text_entry_point_check (path dir, dcp::VerificationNote::Code code, boost::function<void (shared_ptr<T>)> adjust)
2397 {
2398         prepare_directory (dir);
2399         auto dcp = make_shared<dcp::DCP>(dir);
2400         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2401
2402         auto constexpr reel_length = 192;
2403
2404         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2405         subs->set_language (dcp::LanguageTag("de-DE"));
2406         subs->set_start_time (dcp::Time());
2407         subs->add (simple_subtitle());
2408         subs->write (dir / "subs.mxf");
2409         auto reel_text = make_shared<T>(subs, dcp::Fraction(24, 1), reel_length, 0);
2410         adjust (reel_text);
2411
2412         auto reel = make_shared<dcp::Reel>(
2413                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2414                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2415                 );
2416
2417         reel->add (reel_text);
2418
2419         reel->add (simple_markers(reel_length));
2420
2421         cpl->add (reel);
2422
2423         dcp->add (cpl);
2424         dcp->write_xml (
2425                 dcp::String::compose("libdcp %1", dcp::version),
2426                 dcp::String::compose("libdcp %1", dcp::version),
2427                 dcp::LocalTime().as_string(),
2428                 "A Test DCP"
2429                 );
2430
2431         check_verify_result (
2432                 {dir},
2433                 {
2434                         { dcp::VerificationNote::Type::BV21_ERROR, code, subs->id() },
2435                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2436                 });
2437 }
2438
2439
2440 BOOST_AUTO_TEST_CASE (verify_text_entry_point)
2441 {
2442         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2443                 "build/test/verify_subtitle_entry_point_must_be_present",
2444                 dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT,
2445                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2446                         asset->unset_entry_point ();
2447                         }
2448                 );
2449
2450         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2451                 "build/test/verify_subtitle_entry_point_must_be_zero",
2452                 dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT,
2453                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2454                         asset->set_entry_point (4);
2455                         }
2456                 );
2457
2458         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2459                 "build/test/verify_closed_caption_entry_point_must_be_present",
2460                 dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
2461                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2462                         asset->unset_entry_point ();
2463                         }
2464                 );
2465
2466         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2467                 "build/test/verify_closed_caption_entry_point_must_be_zero",
2468                 dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
2469                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2470                         asset->set_entry_point (9);
2471                         }
2472                 );
2473 }
2474
2475
2476 BOOST_AUTO_TEST_CASE (verify_missing_hash)
2477 {
2478         RNGFixer fix;
2479
2480         path const dir("build/test/verify_missing_hash");
2481         auto dcp = make_simple (dir);
2482         dcp->write_xml (
2483                 dcp::String::compose("libdcp %1", dcp::version),
2484                 dcp::String::compose("libdcp %1", dcp::version),
2485                 dcp::LocalTime().as_string(),
2486                 "A Test DCP"
2487                 );
2488
2489         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2490         auto const cpl = dcp->cpls()[0];
2491         BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
2492         BOOST_REQUIRE (cpl->reels()[0]->main_picture());
2493         auto asset_id = cpl->reels()[0]->main_picture()->id();
2494
2495         {
2496                 BOOST_REQUIRE (cpl->file());
2497                 Editor e(cpl->file().get());
2498                 e.delete_first_line_containing("<Hash>");
2499         }
2500
2501         check_verify_result (
2502                 {dir},
2503                 {
2504                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2505                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
2506                 });
2507 }
2508
2509
2510 static
2511 void
2512 verify_markers_test (
2513         path dir,
2514         vector<pair<dcp::Marker, dcp::Time>> markers,
2515         vector<dcp::VerificationNote> test_notes
2516         )
2517 {
2518         auto dcp = make_simple (dir);
2519         dcp->cpls()[0]->set_content_kind (dcp::ContentKind::FEATURE);
2520         auto markers_asset = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 24, 0);
2521         for (auto const& i: markers) {
2522                 markers_asset->set (i.first, i.second);
2523         }
2524         dcp->cpls()[0]->reels()[0]->add(markers_asset);
2525         dcp->write_xml (
2526                 dcp::String::compose("libdcp %1", dcp::version),
2527                 dcp::String::compose("libdcp %1", dcp::version),
2528                 dcp::LocalTime().as_string(),
2529                 "A Test DCP"
2530                 );
2531
2532         check_verify_result ({dir}, test_notes);
2533 }
2534
2535
2536 BOOST_AUTO_TEST_CASE (verify_markers)
2537 {
2538         verify_markers_test (
2539                 "build/test/verify_markers_all_correct",
2540                 {
2541                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2542                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2543                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2544                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2545                 },
2546                 {}
2547                 );
2548
2549         verify_markers_test (
2550                 "build/test/verify_markers_missing_ffec",
2551                 {
2552                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2553                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2554                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2555                 },
2556                 {
2557                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE }
2558                 });
2559
2560         verify_markers_test (
2561                 "build/test/verify_markers_missing_ffmc",
2562                 {
2563                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2564                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2565                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2566                 },
2567                 {
2568                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
2569                 });
2570
2571         verify_markers_test (
2572                 "build/test/verify_markers_missing_ffoc",
2573                 {
2574                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2575                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2576                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2577                 },
2578                 {
2579                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC}
2580                 });
2581
2582         verify_markers_test (
2583                 "build/test/verify_markers_missing_lfoc",
2584                 {
2585                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2586                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2587                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) }
2588                 },
2589                 {
2590                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC }
2591                 });
2592
2593         verify_markers_test (
2594                 "build/test/verify_markers_incorrect_ffoc",
2595                 {
2596                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2597                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2598                         { dcp::Marker::FFOC, dcp::Time(3, 24, 24) },
2599                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2600                 },
2601                 {
2602                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_FFOC, string("3") }
2603                 });
2604
2605         verify_markers_test (
2606                 "build/test/verify_markers_incorrect_lfoc",
2607                 {
2608                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2609                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2610                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2611                         { dcp::Marker::LFOC, dcp::Time(18, 24, 24) }
2612                 },
2613                 {
2614                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_LFOC, string("18") }
2615                 });
2616 }
2617
2618
2619 BOOST_AUTO_TEST_CASE (verify_missing_cpl_metadata_version_number)
2620 {
2621         path dir = "build/test/verify_missing_cpl_metadata_version_number";
2622         prepare_directory (dir);
2623         auto dcp = make_simple (dir);
2624         auto cpl = dcp->cpls()[0];
2625         cpl->unset_version_number();
2626         dcp->write_xml (
2627                 dcp::String::compose("libdcp %1", dcp::version),
2628                 dcp::String::compose("libdcp %1", dcp::version),
2629                 dcp::LocalTime().as_string(),
2630                 "A Test DCP"
2631                 );
2632
2633         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get() }});
2634 }
2635
2636
2637 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
2638 {
2639         path dir = "build/test/verify_missing_extension_metadata1";
2640         auto dcp = make_simple (dir);
2641         dcp->write_xml (
2642                 dcp::String::compose("libdcp %1", dcp::version),
2643                 dcp::String::compose("libdcp %1", dcp::version),
2644                 dcp::LocalTime().as_string(),
2645                 "A Test DCP"
2646                 );
2647
2648         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2649         auto cpl = dcp->cpls()[0];
2650
2651         {
2652                 Editor e (cpl->file().get());
2653                 e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
2654         }
2655
2656         check_verify_result (
2657                 {dir},
2658                 {
2659                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2660                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2661                 });
2662 }
2663
2664
2665 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
2666 {
2667         path dir = "build/test/verify_missing_extension_metadata2";
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 cpl = dcp->cpls()[0];
2677
2678         {
2679                 Editor e (cpl->file().get());
2680                 e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
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::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2688                 });
2689 }
2690
2691
2692 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
2693 {
2694         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata3";
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:Name>A", "<meta:NameX>A");
2708                 e.replace ("n</meta:Name>", "n</meta:NameX>");
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:NameX'"), cpl->file().get(), 75 },
2715                         { 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 },
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_extension_metadata1)
2722 {
2723         path dir = "build/test/verify_invalid_extension_metadata1";
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 cpl = dcp->cpls()[0];
2733
2734         {
2735                 Editor e (cpl->file().get());
2736                 e.replace ("Application", "Fred");
2737         }
2738
2739         check_verify_result (
2740                 {dir},
2741                 {
2742                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2743                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
2744                 });
2745 }
2746
2747
2748 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
2749 {
2750         path dir = "build/test/verify_invalid_extension_metadata2";
2751         auto dcp = make_simple (dir);
2752         dcp->write_xml (
2753                 dcp::String::compose("libdcp %1", dcp::version),
2754                 dcp::String::compose("libdcp %1", dcp::version),
2755                 dcp::LocalTime().as_string(),
2756                 "A Test DCP"
2757                 );
2758
2759         auto cpl = dcp->cpls()[0];
2760
2761         {
2762                 Editor e (cpl->file().get());
2763                 e.replace ("DCP Constraints Profile", "Fred");
2764         }
2765
2766         check_verify_result (
2767                 {dir},
2768                 {
2769                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2770                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
2771                 });
2772 }
2773
2774
2775 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
2776 {
2777         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata6";
2778         auto dcp = make_simple (dir);
2779         dcp->write_xml (
2780                 dcp::String::compose("libdcp %1", dcp::version),
2781                 dcp::String::compose("libdcp %1", dcp::version),
2782                 dcp::LocalTime().as_string(),
2783                 "A Test DCP"
2784                 );
2785
2786         auto const cpl = dcp->cpls()[0];
2787
2788         {
2789                 Editor e (cpl->file().get());
2790                 e.replace ("<meta:Value>", "<meta:ValueX>");
2791                 e.replace ("</meta:Value>", "</meta:ValueX>");
2792         }
2793
2794         check_verify_result (
2795                 {dir},
2796                 {
2797                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 79 },
2798                         { 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 },
2799                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2800                 });
2801 }
2802
2803
2804 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
2805 {
2806         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata7";
2807         auto dcp = make_simple (dir);
2808         dcp->write_xml (
2809                 dcp::String::compose("libdcp %1", dcp::version),
2810                 dcp::String::compose("libdcp %1", dcp::version),
2811                 dcp::LocalTime().as_string(),
2812                 "A Test DCP"
2813                 );
2814
2815         auto const cpl = dcp->cpls()[0];
2816
2817         {
2818                 Editor e (cpl->file().get());
2819                 e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
2820         }
2821
2822         check_verify_result (
2823                 {dir},
2824                 {
2825                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2826                         { 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() },
2827                 });
2828 }
2829
2830
2831 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
2832 {
2833         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata8";
2834         auto dcp = make_simple (dir);
2835         dcp->write_xml (
2836                 dcp::String::compose("libdcp %1", dcp::version),
2837                 dcp::String::compose("libdcp %1", dcp::version),
2838                 dcp::LocalTime().as_string(),
2839                 "A Test DCP"
2840                 );
2841
2842         auto const cpl = dcp->cpls()[0];
2843
2844         {
2845                 Editor e (cpl->file().get());
2846                 e.replace ("<meta:Property>", "<meta:PropertyX>");
2847                 e.replace ("</meta:Property>", "</meta:PropertyX>");
2848         }
2849
2850         check_verify_result (
2851                 {dir},
2852                 {
2853                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 77 },
2854                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 81 },
2855                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2856                 });
2857 }
2858
2859
2860 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
2861 {
2862         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata9";
2863         auto dcp = make_simple (dir);
2864         dcp->write_xml (
2865                 dcp::String::compose("libdcp %1", dcp::version),
2866                 dcp::String::compose("libdcp %1", dcp::version),
2867                 dcp::LocalTime().as_string(),
2868                 "A Test DCP"
2869                 );
2870
2871         auto const cpl = dcp->cpls()[0];
2872
2873         {
2874                 Editor e (cpl->file().get());
2875                 e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
2876                 e.replace ("</meta:PropertyList>", "</meta:PropertyListX>");
2877         }
2878
2879         check_verify_result (
2880                 {dir},
2881                 {
2882                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 76 },
2883                         { 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 },
2884                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2885                 });
2886 }
2887
2888
2889
2890 BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
2891 {
2892         path dir = "build/test/verify_unsigned_cpl_with_encrypted_content";
2893         prepare_directory (dir);
2894         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2895                 copy_file (i.path(), dir / i.path().filename());
2896         }
2897
2898         path const pkl = dir / ( "pkl_" + encryption_test_pkl_id + ".xml" );
2899         path const cpl = dir / ( "cpl_" + encryption_test_cpl_id + ".xml");
2900
2901         {
2902                 Editor e (cpl);
2903                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2904         }
2905
2906         check_verify_result (
2907                 {dir},
2908                 {
2909                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id, canonical(cpl) },
2910                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl), },
2911                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2912                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2913                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2914                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2915                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2916                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, encryption_test_cpl_id, canonical(cpl) }
2917                 });
2918 }
2919
2920
2921 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_encrypted_content)
2922 {
2923         path dir = "build/test/unsigned_pkl_with_encrypted_content";
2924         prepare_directory (dir);
2925         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2926                 copy_file (i.path(), dir / i.path().filename());
2927         }
2928
2929         path const cpl = dir / ("cpl_" + encryption_test_cpl_id + ".xml");
2930         path const pkl = dir / ("pkl_" + encryption_test_pkl_id + ".xml");
2931         {
2932                 Editor e (pkl);
2933                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2934         }
2935
2936         check_verify_result (
2937                 {dir},
2938                 {
2939                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl) },
2940                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2941                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2942                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2943                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2944                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2945                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, encryption_test_pkl_id, canonical(pkl) },
2946                 });
2947 }
2948
2949
2950 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_unencrypted_content)
2951 {
2952         path dir = "build/test/verify_unsigned_pkl_with_unencrypted_content";
2953         prepare_directory (dir);
2954         for (auto i: directory_iterator("test/ref/DCP/dcp_test1")) {
2955                 copy_file (i.path(), dir / i.path().filename());
2956         }
2957
2958         {
2959                 Editor e (dir / dcp_test1_pkl);
2960                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2961         }
2962
2963         check_verify_result ({dir}, {});
2964 }
2965
2966
2967 BOOST_AUTO_TEST_CASE (verify_partially_encrypted)
2968 {
2969         path dir ("build/test/verify_must_not_be_partially_encrypted");
2970         prepare_directory (dir);
2971
2972         dcp::DCP d (dir);
2973
2974         auto signer = make_shared<dcp::CertificateChain>();
2975         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem")));
2976         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem")));
2977         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem")));
2978         signer->set_key (dcp::file_to_string("test/ref/crypt/leaf.key"));
2979
2980         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2981
2982         dcp::Key key;
2983
2984         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
2985         mp->set_key (key);
2986
2987         auto writer = mp->start_write (dir / "video.mxf", false);
2988         dcp::ArrayData j2c ("test/data/flat_red.j2c");
2989         for (int i = 0; i < 24; ++i) {
2990                 writer->write (j2c.data(), j2c.size());
2991         }
2992         writer->finalize ();
2993
2994         auto ms = simple_sound (dir, "", dcp::MXFMetadata(), "de-DE");
2995
2996         auto reel = make_shared<dcp::Reel>(
2997                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2998                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2999                 );
3000
3001         reel->add (simple_markers());
3002
3003         cpl->add (reel);
3004
3005         cpl->set_content_version (
3006                 {"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"}
3007                 );
3008         cpl->set_annotation_text ("A Test DCP");
3009         cpl->set_issuer ("OpenDCP 0.0.25");
3010         cpl->set_creator ("OpenDCP 0.0.25");
3011         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
3012         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
3013         cpl->set_main_sound_sample_rate (48000);
3014         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
3015         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
3016         cpl->set_version_number (1);
3017
3018         d.add (cpl);
3019
3020         d.write_xml ("OpenDCP 0.0.25", "OpenDCP 0.0.25", "2012-07-17T04:45:18+00:00", "A Test DCP", signer);
3021
3022         check_verify_result (
3023                 {dir},
3024                 {
3025                         {dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED},
3026                 });
3027 }
3028
3029
3030 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_2k)
3031 {
3032         vector<dcp::VerificationNote> notes;
3033         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"));
3034         auto reader = picture.start_read ();
3035         auto frame = reader->get_frame (0);
3036         verify_j2k (frame, notes);
3037         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3038 }
3039
3040
3041 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_4k)
3042 {
3043         vector<dcp::VerificationNote> notes;
3044         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "sul", "TLR"));
3045         auto reader = picture.start_read ();
3046         auto frame = reader->get_frame (0);
3047         verify_j2k (frame, notes);
3048         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3049 }
3050
3051
3052 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_libdcp)
3053 {
3054         boost::filesystem::path dir = "build/test/verify_jpeg2000_codestream_libdcp";
3055         prepare_directory (dir);
3056         auto dcp = make_simple (dir);
3057         dcp->write_xml ();
3058         vector<dcp::VerificationNote> notes;
3059         dcp::MonoPictureAsset picture (find_file(dir, "video"));
3060         auto reader = picture.start_read ();
3061         auto frame = reader->get_frame (0);
3062         verify_j2k (frame, notes);
3063         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3064 }
3065
3066
3067 /** Check that ResourceID and the XML ID being different is spotted */
3068 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_resource_id)
3069 {
3070         boost::filesystem::path const dir = "build/test/verify_mismatched_subtitle_resource_id";
3071         prepare_directory (dir);
3072
3073         ASDCP::WriterInfo writer_info;
3074         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3075
3076         unsigned int c;
3077         auto mxf_id = dcp::make_uuid ();
3078         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3079         BOOST_REQUIRE (c == Kumu::UUID_Length);
3080
3081         auto resource_id = dcp::make_uuid ();
3082         ASDCP::TimedText::TimedTextDescriptor descriptor;
3083         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3084         DCP_ASSERT (c == Kumu::UUID_Length);
3085
3086         auto xml_id = dcp::make_uuid ();
3087         ASDCP::TimedText::MXFWriter writer;
3088         auto subs_mxf = dir / "subs.mxf";
3089         auto r = writer.OpenWrite(subs_mxf.c_str(), writer_info, descriptor, 4096);
3090         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3091         writer.WriteTimedTextResource (dcp::String::compose(
3092                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3093                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3094                 "<Id>urn:uuid:%1</Id>"
3095                 "<ContentTitleText>Content</ContentTitleText>"
3096                 "<AnnotationText>Annotation</AnnotationText>"
3097                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3098                 "<ReelNumber>1</ReelNumber>"
3099                 "<Language>en-US</Language>"
3100                 "<EditRate>25 1</EditRate>"
3101                 "<TimeCodeRate>25</TimeCodeRate>"
3102                 "<StartTime>00:00:00:00</StartTime>"
3103                 "<SubtitleList>"
3104                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3105                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3106                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3107                 "</Subtitle>"
3108                 "</Font>"
3109                 "</SubtitleList>"
3110                 "</SubtitleReel>",
3111                 xml_id).c_str());
3112
3113         writer.Finalize();
3114
3115         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3116         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3117
3118         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3119
3120         check_verify_result (
3121                 { dir },
3122                 {
3123                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3124                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID },
3125                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3126                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3127                 });
3128 }
3129
3130
3131 /** Check that ResourceID and the MXF ID being the same is spotted */
3132 BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
3133 {
3134         boost::filesystem::path const dir = "build/test/verify_incorrect_timed_text_id";
3135         prepare_directory (dir);
3136
3137         ASDCP::WriterInfo writer_info;
3138         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3139
3140         unsigned int c;
3141         auto mxf_id = dcp::make_uuid ();
3142         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3143         BOOST_REQUIRE (c == Kumu::UUID_Length);
3144
3145         auto resource_id = mxf_id;
3146         ASDCP::TimedText::TimedTextDescriptor descriptor;
3147         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3148         DCP_ASSERT (c == Kumu::UUID_Length);
3149
3150         auto xml_id = resource_id;
3151         ASDCP::TimedText::MXFWriter writer;
3152         auto subs_mxf = dir / "subs.mxf";
3153         auto r = writer.OpenWrite(subs_mxf.c_str(), writer_info, descriptor, 4096);
3154         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3155         writer.WriteTimedTextResource (dcp::String::compose(
3156                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3157                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3158                 "<Id>urn:uuid:%1</Id>"
3159                 "<ContentTitleText>Content</ContentTitleText>"
3160                 "<AnnotationText>Annotation</AnnotationText>"
3161                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3162                 "<ReelNumber>1</ReelNumber>"
3163                 "<Language>en-US</Language>"
3164                 "<EditRate>25 1</EditRate>"
3165                 "<TimeCodeRate>25</TimeCodeRate>"
3166                 "<StartTime>00:00:00:00</StartTime>"
3167                 "<SubtitleList>"
3168                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3169                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3170                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3171                 "</Subtitle>"
3172                 "</Font>"
3173                 "</SubtitleList>"
3174                 "</SubtitleReel>",
3175                 xml_id).c_str());
3176
3177         writer.Finalize();
3178
3179         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3180         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3181
3182         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3183
3184         check_verify_result (
3185                 { dir },
3186                 {
3187                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3188                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID },
3189                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3190                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3191                 });
3192 }
3193
3194
3195 /** Check a DCP with a 3D asset marked as 2D */
3196 BOOST_AUTO_TEST_CASE (verify_threed_marked_as_twod)
3197 {
3198         check_verify_result (
3199                 { private_test / "data" / "xm" },
3200                 {
3201                         {
3202                                 dcp::VerificationNote::Type::WARNING,
3203                                 dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, boost::filesystem::canonical(find_file(private_test / "data" / "xm", "j2c"))
3204                         },
3205                         {
3206                                 dcp::VerificationNote::Type::BV21_ERROR,
3207                                 dcp::VerificationNote::Code::INVALID_STANDARD
3208                         },
3209                 });
3210
3211 }
3212