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