Bv2.1 6.2.1: Check that additional subtitle languages conform to RFC 5646.
[libdcp.git] / test / verify_test.cc
1 /*
2     Copyright (C) 2018-2020 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 "mono_picture_asset_writer.h"
45 #include "interop_subtitle_asset.h"
46 #include "smpte_subtitle_asset.h"
47 #include "reel_closed_caption_asset.h"
48 #include "reel_subtitle_asset.h"
49 #include "compose.hpp"
50 #include "test.h"
51 #include <boost/test/unit_test.hpp>
52 #include <boost/foreach.hpp>
53 #include <boost/algorithm/string.hpp>
54 #include <cstdio>
55 #include <iostream>
56
57 using std::list;
58 using std::pair;
59 using std::string;
60 using std::vector;
61 using std::make_pair;
62 using boost::optional;
63 using std::shared_ptr;
64
65
66 static list<pair<string, optional<boost::filesystem::path> > > stages;
67
68 static void
69 stage (string s, optional<boost::filesystem::path> p)
70 {
71         stages.push_back (make_pair (s, p));
72 }
73
74 static void
75 progress (float)
76 {
77
78 }
79
80 static void
81 prepare_directory (boost::filesystem::path path)
82 {
83         using namespace boost::filesystem;
84         remove_all (path);
85         create_directories (path);
86 }
87
88
89 static vector<boost::filesystem::path>
90 setup (int reference_number, int verify_test_number)
91 {
92         prepare_directory (dcp::String::compose("build/test/verify_test%1", verify_test_number));
93         for (boost::filesystem::directory_iterator i(dcp::String::compose("test/ref/DCP/dcp_test%1", reference_number)); i != boost::filesystem::directory_iterator(); ++i) {
94                 boost::filesystem::copy_file (i->path(), dcp::String::compose("build/test/verify_test%1", verify_test_number) / i->path().filename());
95         }
96
97         vector<boost::filesystem::path> directories;
98         directories.push_back (dcp::String::compose("build/test/verify_test%1", verify_test_number));
99         return directories;
100
101 }
102
103
104 /** Class that can alter a file by searching and replacing strings within it.
105  *  On destruction modifies the file whose name was given to the constructor.
106  */
107 class Editor
108 {
109 public:
110         Editor (boost::filesystem::path path)
111                 : _path(path)
112         {
113                 _content = dcp::file_to_string (_path);
114         }
115
116         ~Editor ()
117         {
118                 FILE* f = fopen(_path.string().c_str(), "w");
119                 BOOST_REQUIRE (f);
120                 fwrite (_content.c_str(), _content.length(), 1, f);
121                 fclose (f);
122         }
123
124         void replace (string a, string b)
125         {
126                 boost::algorithm::replace_all (_content, a, b);
127         }
128
129 private:
130         boost::filesystem::path _path;
131         std::string _content;
132 };
133
134
135 static
136 void
137 dump_notes (list<dcp::VerificationNote> const & notes)
138 {
139         BOOST_FOREACH (dcp::VerificationNote i, notes) {
140                 std::cout << dcp::note_to_string(i) << "\n";
141         }
142 }
143
144 /* Check DCP as-is (should be OK) */
145 BOOST_AUTO_TEST_CASE (verify_test1)
146 {
147         stages.clear ();
148         vector<boost::filesystem::path> directories = setup (1, 1);
149         list<dcp::VerificationNote> notes = dcp::verify (directories, &stage, &progress, xsd_test);
150
151         boost::filesystem::path const cpl_file = "build/test/verify_test1/cpl_81fb54df-e1bf-4647-8788-ea7ba154375b.xml";
152         boost::filesystem::path const pkl_file = "build/test/verify_test1/pkl_cd49971e-bf4c-4594-8474-54ebef09a40c.xml";
153         boost::filesystem::path const assetmap_file = "build/test/verify_test1/ASSETMAP.xml";
154
155         list<pair<string, optional<boost::filesystem::path> > >::const_iterator st = stages.begin();
156         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
157         BOOST_REQUIRE (st->second);
158         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical("build/test/verify_test1"));
159         ++st;
160         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
161         BOOST_REQUIRE (st->second);
162         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical(cpl_file));
163         ++st;
164         BOOST_CHECK_EQUAL (st->first, "Checking reel");
165         BOOST_REQUIRE (!st->second);
166         ++st;
167         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
168         BOOST_REQUIRE (st->second);
169         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical("build/test/verify_test1/video.mxf"));
170         ++st;
171         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
172         BOOST_REQUIRE (st->second);
173         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical("build/test/verify_test1/video.mxf"));
174         ++st;
175         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
176         BOOST_REQUIRE (st->second);
177         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical("build/test/verify_test1/audio.mxf"));
178         ++st;
179         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
180         BOOST_REQUIRE (st->second);
181         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical("build/test/verify_test1/audio.mxf"));
182         ++st;
183         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
184         BOOST_REQUIRE (st->second);
185         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical(pkl_file));
186         ++st;
187         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
188         BOOST_REQUIRE (st->second);
189         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical(assetmap_file));
190         ++st;
191         BOOST_REQUIRE (st == stages.end());
192
193         BOOST_CHECK_EQUAL (notes.size(), 0);
194 }
195
196 /* Corrupt the MXFs and check that this is spotted */
197 BOOST_AUTO_TEST_CASE (verify_test2)
198 {
199         vector<boost::filesystem::path> directories = setup (1, 2);
200
201         FILE* mod = fopen("build/test/verify_test2/video.mxf", "r+b");
202         BOOST_REQUIRE (mod);
203         fseek (mod, 4096, SEEK_SET);
204         int x = 42;
205         fwrite (&x, sizeof(x), 1, mod);
206         fclose (mod);
207
208         mod = fopen("build/test/verify_test2/audio.mxf", "r+b");
209         BOOST_REQUIRE (mod);
210         BOOST_REQUIRE_EQUAL (fseek(mod, -64, SEEK_END), 0);
211         BOOST_REQUIRE (fwrite (&x, sizeof(x), 1, mod) == 1);
212         fclose (mod);
213
214         list<dcp::VerificationNote> notes;
215         {
216                 dcp::ASDCPErrorSuspender sus;
217                 notes = dcp::verify (directories, &stage, &progress, xsd_test);
218         }
219
220         BOOST_REQUIRE_EQUAL (notes.size(), 2);
221         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::VERIFY_ERROR);
222         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::PICTURE_HASH_INCORRECT);
223         BOOST_CHECK_EQUAL (notes.back().type(), dcp::VerificationNote::VERIFY_ERROR);
224         BOOST_CHECK_EQUAL (notes.back().code(), dcp::VerificationNote::SOUND_HASH_INCORRECT);
225 }
226
227 /* Corrupt the hashes in the PKL and check that the disagreement between CPL and PKL is spotted */
228 BOOST_AUTO_TEST_CASE (verify_test3)
229 {
230         vector<boost::filesystem::path> directories = setup (1, 3);
231
232         {
233                 Editor e ("build/test/verify_test3/pkl_cd49971e-bf4c-4594-8474-54ebef09a40c.xml");
234                 e.replace ("<Hash>", "<Hash>x");
235         }
236
237         list<dcp::VerificationNote> notes = dcp::verify (directories, &stage, &progress, xsd_test);
238
239         BOOST_REQUIRE_EQUAL (notes.size(), 6);
240         list<dcp::VerificationNote>::const_iterator i = notes.begin();
241         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::VERIFY_ERROR);
242         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::CPL_HASH_INCORRECT);
243         ++i;
244         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::VERIFY_ERROR);
245         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE);
246         ++i;
247         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::VERIFY_ERROR);
248         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE);
249         ++i;
250         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::VERIFY_ERROR);
251         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::XML_VALIDATION_ERROR);
252         ++i;
253         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::VERIFY_ERROR);
254         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::XML_VALIDATION_ERROR);
255         ++i;
256         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::VERIFY_ERROR);
257         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::XML_VALIDATION_ERROR);
258         ++i;
259 }
260
261 /* Corrupt the ContentKind in the CPL */
262 BOOST_AUTO_TEST_CASE (verify_test4)
263 {
264         vector<boost::filesystem::path> directories = setup (1, 4);
265
266         {
267                 Editor e ("build/test/verify_test4/cpl_81fb54df-e1bf-4647-8788-ea7ba154375b.xml");
268                 e.replace ("<ContentKind>", "<ContentKind>x");
269         }
270
271         list<dcp::VerificationNote> notes = dcp::verify (directories, &stage, &progress, xsd_test);
272
273         BOOST_REQUIRE_EQUAL (notes.size(), 1);
274         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::GENERAL_READ);
275         BOOST_CHECK_EQUAL (*notes.front().note(), "Bad content kind 'xfeature'");
276 }
277
278 static
279 boost::filesystem::path
280 cpl (int n)
281 {
282         return dcp::String::compose("build/test/verify_test%1/cpl_81fb54df-e1bf-4647-8788-ea7ba154375b.xml", n);
283 }
284
285 static
286 boost::filesystem::path
287 pkl (int n)
288 {
289         return dcp::String::compose("build/test/verify_test%1/pkl_cd49971e-bf4c-4594-8474-54ebef09a40c.xml", n);
290 }
291
292 static
293 boost::filesystem::path
294 asset_map (int n)
295 {
296         return dcp::String::compose("build/test/verify_test%1/ASSETMAP.xml", n);
297 }
298
299 static
300 void check_after_replace (int n, boost::function<boost::filesystem::path (int)> file, string from, string to, dcp::VerificationNote::Code code1)
301 {
302         vector<boost::filesystem::path> directories = setup (1, n);
303
304         {
305                 Editor e (file(n));
306                 e.replace (from, to);
307         }
308
309         list<dcp::VerificationNote> notes = dcp::verify (directories, &stage, &progress, xsd_test);
310
311         BOOST_REQUIRE_EQUAL (notes.size(), 1);
312         BOOST_CHECK_EQUAL (notes.front().code(), code1);
313 }
314
315 static
316 void check_after_replace (int n, boost::function<boost::filesystem::path (int)> file, string from, string to, dcp::VerificationNote::Code code1, dcp::VerificationNote::Code code2)
317 {
318         vector<boost::filesystem::path> directories = setup (1, n);
319
320         {
321                 Editor e (file(n));
322                 e.replace (from, to);
323         }
324
325         list<dcp::VerificationNote> notes = dcp::verify (directories, &stage, &progress, xsd_test);
326
327         BOOST_REQUIRE_EQUAL (notes.size(), 2);
328         BOOST_CHECK_EQUAL (notes.front().code(), code1);
329         BOOST_CHECK_EQUAL (notes.back().code(), code2);
330 }
331
332 static
333 void check_after_replace (
334         int n, boost::function<boost::filesystem::path (int)> file,
335         string from,
336         string to,
337         dcp::VerificationNote::Code code1,
338         dcp::VerificationNote::Code code2,
339         dcp::VerificationNote::Code code3
340         )
341 {
342         vector<boost::filesystem::path> directories = setup (1, n);
343
344         {
345                 Editor e (file(n));
346                 e.replace (from, to);
347         }
348
349         list<dcp::VerificationNote> notes = dcp::verify (directories, &stage, &progress, xsd_test);
350
351         BOOST_REQUIRE_EQUAL (notes.size(), 3);
352         list<dcp::VerificationNote>::const_iterator i = notes.begin ();
353         BOOST_CHECK_EQUAL (i->code(), code1);
354         ++i;
355         BOOST_CHECK_EQUAL (i->code(), code2);
356         ++i;
357         BOOST_CHECK_EQUAL (i->code(), code3);
358 }
359
360 /* FrameRate */
361 BOOST_AUTO_TEST_CASE (verify_test5)
362 {
363         check_after_replace (
364                         5, &cpl,
365                         "<FrameRate>24 1", "<FrameRate>99 1",
366                         dcp::VerificationNote::CPL_HASH_INCORRECT,
367                         dcp::VerificationNote::INVALID_PICTURE_FRAME_RATE
368                         );
369 }
370
371 /* Missing asset */
372 BOOST_AUTO_TEST_CASE (verify_test6)
373 {
374         vector<boost::filesystem::path> directories = setup (1, 6);
375
376         boost::filesystem::remove ("build/test/verify_test6/video.mxf");
377         list<dcp::VerificationNote> notes = dcp::verify (directories, &stage, &progress, xsd_test);
378
379         BOOST_REQUIRE_EQUAL (notes.size(), 1);
380         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::VERIFY_ERROR);
381         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::MISSING_ASSET);
382 }
383
384 static
385 boost::filesystem::path
386 assetmap (int n)
387 {
388         return dcp::String::compose("build/test/verify_test%1/ASSETMAP.xml", n);
389 }
390
391 /* Empty asset filename in ASSETMAP */
392 BOOST_AUTO_TEST_CASE (verify_test7)
393 {
394         check_after_replace (
395                         7, &assetmap,
396                         "<Path>video.mxf</Path>", "<Path></Path>",
397                         dcp::VerificationNote::EMPTY_ASSET_PATH
398                         );
399 }
400
401 /* Mismatched standard */
402 BOOST_AUTO_TEST_CASE (verify_test8)
403 {
404         check_after_replace (
405                         8, &cpl,
406                         "http://www.smpte-ra.org/schemas/429-7/2006/CPL", "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#",
407                         dcp::VerificationNote::MISMATCHED_STANDARD,
408                         dcp::VerificationNote::XML_VALIDATION_ERROR,
409                         dcp::VerificationNote::CPL_HASH_INCORRECT
410                         );
411 }
412
413 /* Badly formatted <Id> in CPL */
414 BOOST_AUTO_TEST_CASE (verify_test9)
415 {
416         /* There's no CPL_HASH_INCORRECT error here because it can't find the correct hash by ID (since the ID is wrong) */
417         check_after_replace (
418                         9, &cpl,
419                         "<Id>urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375b", "<Id>urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375",
420                         dcp::VerificationNote::XML_VALIDATION_ERROR
421                         );
422 }
423
424 /* Badly formatted <IssueDate> in CPL */
425 BOOST_AUTO_TEST_CASE (verify_test10)
426 {
427         check_after_replace (
428                         10, &cpl,
429                         "<IssueDate>", "<IssueDate>x",
430                         dcp::VerificationNote::XML_VALIDATION_ERROR,
431                         dcp::VerificationNote::CPL_HASH_INCORRECT
432                         );
433 }
434
435 /* Badly-formatted <Id> in PKL */
436 BOOST_AUTO_TEST_CASE (verify_test11)
437 {
438         check_after_replace (
439                 11, &pkl,
440                 "<Id>urn:uuid:cd4", "<Id>urn:uuid:xd4",
441                 dcp::VerificationNote::XML_VALIDATION_ERROR
442                 );
443 }
444
445 /* Badly-formatted <Id> in ASSETMAP */
446 BOOST_AUTO_TEST_CASE (verify_test12)
447 {
448         check_after_replace (
449                 12, &asset_map,
450                 "<Id>urn:uuid:63c", "<Id>urn:uuid:x3c",
451                 dcp::VerificationNote::XML_VALIDATION_ERROR
452                 );
453 }
454
455 /* Basic test of an Interop DCP */
456 BOOST_AUTO_TEST_CASE (verify_test13)
457 {
458         stages.clear ();
459         vector<boost::filesystem::path> directories = setup (3, 13);
460         list<dcp::VerificationNote> notes = dcp::verify (directories, &stage, &progress, xsd_test);
461
462         boost::filesystem::path const cpl_file = "build/test/verify_test13/cpl_cbfd2bc0-21cf-4a8f-95d8-9cddcbe51296.xml";
463         boost::filesystem::path const pkl_file = "build/test/verify_test13/pkl_d87a950c-bd6f-41f6-90cc-56ccd673e131.xml";
464         boost::filesystem::path const assetmap_file = "build/test/verify_test13/ASSETMAP";
465
466         list<pair<string, optional<boost::filesystem::path> > >::const_iterator st = stages.begin();
467         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
468         BOOST_REQUIRE (st->second);
469         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical("build/test/verify_test13"));
470         ++st;
471         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
472         BOOST_REQUIRE (st->second);
473         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical(cpl_file));
474         ++st;
475         BOOST_CHECK_EQUAL (st->first, "Checking reel");
476         BOOST_REQUIRE (!st->second);
477         ++st;
478         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
479         BOOST_REQUIRE (st->second);
480         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical("build/test/verify_test13/j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
481         ++st;
482         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
483         BOOST_REQUIRE (st->second);
484         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical("build/test/verify_test13/j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
485         ++st;
486         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
487         BOOST_REQUIRE (st->second);
488         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical("build/test/verify_test13/pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
489         ++st;
490         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
491         BOOST_REQUIRE (st->second);
492         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical("build/test/verify_test13/pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
493         ++st;
494         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
495         BOOST_REQUIRE (st->second);
496         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical(pkl_file));
497         ++st;
498         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
499         BOOST_REQUIRE (st->second);
500         BOOST_CHECK_EQUAL (st->second.get(), boost::filesystem::canonical(assetmap_file));
501         ++st;
502         BOOST_REQUIRE (st == stages.end());
503
504         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
505         list<dcp::VerificationNote>::const_iterator i = notes.begin ();
506         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::VERIFY_BV21_ERROR);
507         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::NOT_SMPTE);
508 }
509
510 /* DCP with a short asset */
511 BOOST_AUTO_TEST_CASE (verify_test14)
512 {
513         vector<boost::filesystem::path> directories = setup (8, 14);
514         list<dcp::VerificationNote> notes = dcp::verify (directories, &stage, &progress, xsd_test);
515
516         BOOST_REQUIRE_EQUAL (notes.size(), 5);
517         list<dcp::VerificationNote>::const_iterator i = notes.begin ();
518         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::NOT_SMPTE);
519         ++i;
520         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::DURATION_TOO_SMALL);
521         ++i;
522         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::INTRINSIC_DURATION_TOO_SMALL);
523         ++i;
524         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::DURATION_TOO_SMALL);
525         ++i;
526         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::INTRINSIC_DURATION_TOO_SMALL);
527         ++i;
528 }
529
530
531 static
532 void
533 dcp_from_frame (dcp::ArrayData const& frame, boost::filesystem::path dir)
534 {
535         shared_ptr<dcp::MonoPictureAsset> asset(new dcp::MonoPictureAsset(dcp::Fraction(24, 1), dcp::SMPTE));
536         boost::filesystem::create_directories (dir);
537         shared_ptr<dcp::PictureAssetWriter> writer = asset->start_write (dir / "pic.mxf", true);
538         for (int i = 0; i < 24; ++i) {
539                 writer->write (frame.data(), frame.size());
540         }
541         writer->finalize ();
542
543         shared_ptr<dcp::ReelAsset> reel_asset(new dcp::ReelMonoPictureAsset(asset, 0));
544         shared_ptr<dcp::Reel> reel(new dcp::Reel());
545         reel->add (reel_asset);
546         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
547         cpl->add (reel);
548         shared_ptr<dcp::DCP> dcp(new dcp::DCP(dir));
549         dcp->add (cpl);
550         dcp->write_xml (dcp::SMPTE);
551 }
552
553
554 /* DCP with an over-sized JPEG2000 frame */
555 BOOST_AUTO_TEST_CASE (verify_test15)
556 {
557         int const too_big = 1302083 * 2;
558
559         /* Compress a black image */
560         shared_ptr<dcp::OpenJPEGImage> image = black_image ();
561         dcp::ArrayData frame = dcp::compress_j2k (image, 100000000, 24, false, false);
562         BOOST_REQUIRE (frame.size() < too_big);
563
564         /* Place it in a bigger block with some zero padding at the end */
565         dcp::ArrayData oversized_frame(too_big);
566         memcpy (oversized_frame.data(), frame.data(), frame.size());
567         memset (oversized_frame.data() + frame.size(), 0, too_big - frame.size());
568
569         boost::filesystem::path const dir("build/test/verify_test15");
570         boost::filesystem::remove_all (dir);
571         dcp_from_frame (oversized_frame, dir);
572
573         vector<boost::filesystem::path> dirs;
574         dirs.push_back (dir);
575         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
576         BOOST_REQUIRE_EQUAL (notes.size(), 1);
577         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::PICTURE_FRAME_TOO_LARGE);
578 }
579
580
581 /* DCP with a nearly over-sized JPEG2000 frame */
582 BOOST_AUTO_TEST_CASE (verify_test16)
583 {
584         int const nearly_too_big = 1302083 * 0.98;
585
586         /* Compress a black image */
587         shared_ptr<dcp::OpenJPEGImage> image = black_image ();
588         dcp::ArrayData frame = dcp::compress_j2k (image, 100000000, 24, false, false);
589         BOOST_REQUIRE (frame.size() < nearly_too_big);
590
591         /* Place it in a bigger block with some zero padding at the end */
592         dcp::ArrayData oversized_frame(nearly_too_big);
593         memcpy (oversized_frame.data(), frame.data(), frame.size());
594         memset (oversized_frame.data() + frame.size(), 0, nearly_too_big - frame.size());
595
596         boost::filesystem::path const dir("build/test/verify_test16");
597         boost::filesystem::remove_all (dir);
598         dcp_from_frame (oversized_frame, dir);
599
600         vector<boost::filesystem::path> dirs;
601         dirs.push_back (dir);
602         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
603         BOOST_REQUIRE_EQUAL (notes.size(), 1);
604         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::PICTURE_FRAME_NEARLY_TOO_LARGE);
605 }
606
607
608 /* DCP with a within-range JPEG2000 frame */
609 BOOST_AUTO_TEST_CASE (verify_test17)
610 {
611         /* Compress a black image */
612         shared_ptr<dcp::OpenJPEGImage> image = black_image ();
613         dcp::ArrayData frame = dcp::compress_j2k (image, 100000000, 24, false, false);
614         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
615
616         boost::filesystem::path const dir("build/test/verify_test17");
617         boost::filesystem::remove_all (dir);
618         dcp_from_frame (frame, dir);
619
620         vector<boost::filesystem::path> dirs;
621         dirs.push_back (dir);
622         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
623         BOOST_REQUIRE_EQUAL (notes.size(), 0);
624 }
625
626
627 /* DCP with valid Interop subtitles */
628 BOOST_AUTO_TEST_CASE (verify_test18)
629 {
630         boost::filesystem::path const dir("build/test/verify_test18");
631         prepare_directory (dir);
632         boost::filesystem::copy_file ("test/data/subs1.xml", dir / "subs.xml");
633         shared_ptr<dcp::InteropSubtitleAsset> asset(new dcp::InteropSubtitleAsset(dir / "subs.xml"));
634         shared_ptr<dcp::ReelAsset> reel_asset(new dcp::ReelSubtitleAsset(asset, dcp::Fraction(24, 1), 16 * 24, 0));
635         shared_ptr<dcp::Reel> reel(new dcp::Reel());
636         reel->add (reel_asset);
637         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
638         cpl->add (reel);
639         shared_ptr<dcp::DCP> dcp(new dcp::DCP(dir));
640         dcp->add (cpl);
641         dcp->write_xml (dcp::INTEROP);
642
643         vector<boost::filesystem::path> dirs;
644         dirs.push_back (dir);
645         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
646         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
647         list<dcp::VerificationNote>::const_iterator i = notes.begin ();
648         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::NOT_SMPTE);
649 }
650
651
652 /* DCP with broken Interop subtitles */
653 BOOST_AUTO_TEST_CASE (verify_test19)
654 {
655         boost::filesystem::path const dir("build/test/verify_test19");
656         prepare_directory (dir);
657         boost::filesystem::copy_file ("test/data/subs1.xml", dir / "subs.xml");
658         shared_ptr<dcp::InteropSubtitleAsset> asset(new dcp::InteropSubtitleAsset(dir / "subs.xml"));
659         shared_ptr<dcp::ReelAsset> reel_asset(new dcp::ReelSubtitleAsset(asset, dcp::Fraction(24, 1), 16 * 24, 0));
660         shared_ptr<dcp::Reel> reel(new dcp::Reel());
661         reel->add (reel_asset);
662         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
663         cpl->add (reel);
664         shared_ptr<dcp::DCP> dcp(new dcp::DCP(dir));
665         dcp->add (cpl);
666         dcp->write_xml (dcp::INTEROP);
667
668         {
669                 Editor e (dir / "subs.xml");
670                 e.replace ("</ReelNumber>", "</ReelNumber><Foo></Foo>");
671         }
672
673         vector<boost::filesystem::path> dirs;
674         dirs.push_back (dir);
675         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
676         BOOST_REQUIRE_EQUAL (notes.size(), 3);
677         list<dcp::VerificationNote>::const_iterator i = notes.begin ();
678         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::NOT_SMPTE);
679         ++i;
680         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::XML_VALIDATION_ERROR);
681         ++i;
682         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::XML_VALIDATION_ERROR);
683         ++i;
684 }
685
686
687 /* DCP with valid SMPTE subtitles */
688 BOOST_AUTO_TEST_CASE (verify_test20)
689 {
690         boost::filesystem::path const dir("build/test/verify_test20");
691         prepare_directory (dir);
692         boost::filesystem::copy_file ("test/data/subs.mxf", dir / "subs.mxf");
693         shared_ptr<dcp::SMPTESubtitleAsset> asset(new dcp::SMPTESubtitleAsset(dir / "subs.mxf"));
694         shared_ptr<dcp::ReelAsset> reel_asset(new dcp::ReelSubtitleAsset(asset, dcp::Fraction(24, 1), 16 * 24, 0));
695         shared_ptr<dcp::Reel> reel(new dcp::Reel());
696         reel->add (reel_asset);
697         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
698         cpl->add (reel);
699         shared_ptr<dcp::DCP> dcp(new dcp::DCP(dir));
700         dcp->add (cpl);
701         dcp->write_xml (dcp::SMPTE);
702
703         vector<boost::filesystem::path> dirs;
704         dirs.push_back (dir);
705         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
706         dump_notes (notes);
707         BOOST_REQUIRE_EQUAL (notes.size(), 0);
708 }
709
710
711 /* DCP with broken SMPTE subtitles */
712 BOOST_AUTO_TEST_CASE (verify_test21)
713 {
714         boost::filesystem::path const dir("build/test/verify_test21");
715         prepare_directory (dir);
716         boost::filesystem::copy_file ("test/data/broken_smpte.mxf", dir / "subs.mxf");
717         shared_ptr<dcp::SMPTESubtitleAsset> asset(new dcp::SMPTESubtitleAsset(dir / "subs.mxf"));
718         shared_ptr<dcp::ReelAsset> reel_asset(new dcp::ReelSubtitleAsset(asset, dcp::Fraction(24, 1), 16 * 24, 0));
719         shared_ptr<dcp::Reel> reel(new dcp::Reel());
720         reel->add (reel_asset);
721         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
722         cpl->add (reel);
723         shared_ptr<dcp::DCP> dcp(new dcp::DCP(dir));
724         dcp->add (cpl);
725         dcp->write_xml (dcp::SMPTE);
726
727         vector<boost::filesystem::path> dirs;
728         dirs.push_back (dir);
729         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
730         BOOST_REQUIRE_EQUAL (notes.size(), 2);
731         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::XML_VALIDATION_ERROR);
732         BOOST_CHECK_EQUAL (notes.back().code(), dcp::VerificationNote::XML_VALIDATION_ERROR);
733 }
734
735
736 /* VF */
737 BOOST_AUTO_TEST_CASE (verify_test22)
738 {
739         boost::filesystem::path const ov_dir("build/test/verify_test22_ov");
740         prepare_directory (ov_dir);
741
742         shared_ptr<dcp::OpenJPEGImage> image = black_image ();
743         dcp::ArrayData frame = dcp::compress_j2k (image, 100000000, 24, false, false);
744         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
745         dcp_from_frame (frame, ov_dir);
746
747         dcp::DCP ov (ov_dir);
748         ov.read ();
749
750         boost::filesystem::path const vf_dir("build/test/verify_test22_vf");
751         prepare_directory (vf_dir);
752
753         shared_ptr<dcp::Reel> reel(new dcp::Reel());
754         reel->add (ov.cpls().front()->reels().front()->main_picture());
755         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
756         cpl->add (reel);
757         dcp::DCP vf (vf_dir);
758         vf.add (cpl);
759         vf.write_xml (dcp::SMPTE);
760
761         vector<boost::filesystem::path> dirs;
762         dirs.push_back (vf_dir);
763         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
764         BOOST_REQUIRE_EQUAL (notes.size(), 1);
765         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::EXTERNAL_ASSET);
766 }
767
768
769 /* DCP with valid CompositionMetadataAsset */
770 BOOST_AUTO_TEST_CASE (verify_test23)
771 {
772         boost::filesystem::path const dir("build/test/verify_test23");
773         prepare_directory (dir);
774
775         boost::filesystem::copy_file ("test/data/subs.mxf", dir / "subs.mxf");
776         shared_ptr<dcp::SMPTESubtitleAsset> asset(new dcp::SMPTESubtitleAsset(dir / "subs.mxf"));
777         shared_ptr<dcp::ReelAsset> reel_asset(new dcp::ReelSubtitleAsset(asset, dcp::Fraction(24, 1), 16 * 24, 0));
778
779         shared_ptr<dcp::Reel> reel(new dcp::Reel());
780         reel->add (reel_asset);
781         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
782         cpl->add (reel);
783         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
784         cpl->set_main_sound_sample_rate (48000);
785         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
786         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
787
788         dcp::DCP dcp (dir);
789         dcp.add (cpl);
790         dcp.write_xml (dcp::SMPTE);
791
792         vector<boost::filesystem::path> dirs;
793         dirs.push_back (dir);
794         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
795 }
796
797
798 boost::filesystem::path find_cpl (boost::filesystem::path dir)
799 {
800         for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator(dir); i != boost::filesystem::directory_iterator(); i++) {
801                 if (boost::starts_with(i->path().filename().string(), "cpl_")) {
802                         return i->path();
803                 }
804         }
805
806         BOOST_REQUIRE (false);
807         return boost::filesystem::path();
808 }
809
810
811 /* DCP with invalid CompositionMetadataAsset */
812 BOOST_AUTO_TEST_CASE (verify_test24)
813 {
814         boost::filesystem::path const dir("build/test/verify_test24");
815         prepare_directory (dir);
816
817         shared_ptr<dcp::Reel> reel(new dcp::Reel());
818         reel->add (black_picture_asset(dir));
819         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
820         cpl->add (reel);
821         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
822         cpl->set_main_sound_sample_rate (48000);
823         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
824         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
825
826         dcp::DCP dcp (dir);
827         dcp.add (cpl);
828         dcp.write_xml (dcp::SMPTE);
829
830         {
831                 Editor e (find_cpl("build/test/verify_test24"));
832                 e.replace ("MainSound", "MainSoundX");
833         }
834
835         vector<boost::filesystem::path> dirs;
836         dirs.push_back (dir);
837         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
838         BOOST_REQUIRE_EQUAL (notes.size(), 4);
839
840         list<dcp::VerificationNote>::const_iterator i = notes.begin ();
841         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::XML_VALIDATION_ERROR);
842         ++i;
843         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::XML_VALIDATION_ERROR);
844         ++i;
845         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::XML_VALIDATION_ERROR);
846         ++i;
847         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::CPL_HASH_INCORRECT);
848         ++i;
849 }
850
851
852 /* DCP with invalid CompositionMetadataAsset */
853 BOOST_AUTO_TEST_CASE (verify_test25)
854 {
855         boost::filesystem::path const dir("build/test/verify_test25");
856         prepare_directory (dir);
857
858         boost::filesystem::copy_file ("test/data/subs.mxf", dir / "subs.mxf");
859         shared_ptr<dcp::SMPTESubtitleAsset> asset(new dcp::SMPTESubtitleAsset(dir / "subs.mxf"));
860         shared_ptr<dcp::ReelAsset> reel_asset(new dcp::ReelSubtitleAsset(asset, dcp::Fraction(24, 1), 16 * 24, 0));
861
862         shared_ptr<dcp::Reel> reel(new dcp::Reel());
863         reel->add (reel_asset);
864         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
865         cpl->add (reel);
866         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
867         cpl->set_main_sound_sample_rate (48000);
868         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
869         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
870
871         dcp::DCP dcp (dir);
872         dcp.add (cpl);
873         dcp.write_xml (dcp::SMPTE);
874
875         {
876                 Editor e (find_cpl("build/test/verify_test25"));
877                 e.replace ("</MainPictureActiveArea>", "</MainPictureActiveArea><BadTag></BadTag>");
878         }
879
880         vector<boost::filesystem::path> dirs;
881         dirs.push_back (dir);
882         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
883 }
884
885
886 /* SMPTE DCP with invalid <Language> in the MainSubtitle reel and also in the XML within the MXF */
887 BOOST_AUTO_TEST_CASE (verify_test26)
888 {
889         boost::filesystem::path const dir("build/test/verify_test26");
890         prepare_directory (dir);
891         boost::filesystem::copy_file ("test/data/subs.mxf", dir / "subs.mxf");
892         shared_ptr<dcp::SMPTESubtitleAsset> asset(new dcp::SMPTESubtitleAsset(dir / "subs.mxf"));
893         asset->_language = "wrong-andbad";
894         asset->write (dir / "subs.mxf");
895         shared_ptr<dcp::ReelSubtitleAsset> reel_asset(new dcp::ReelSubtitleAsset(asset, dcp::Fraction(24, 1), 16 * 24, 0));
896         reel_asset->_language = "badlang";
897         shared_ptr<dcp::Reel> reel(new dcp::Reel());
898         reel->add (reel_asset);
899         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
900         cpl->add (reel);
901         shared_ptr<dcp::DCP> dcp(new dcp::DCP(dir));
902         dcp->add (cpl);
903         dcp->write_xml (dcp::SMPTE);
904
905         vector<boost::filesystem::path> dirs;
906         dirs.push_back (dir);
907         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
908         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
909         list<dcp::VerificationNote>::const_iterator i = notes.begin ();
910         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::BAD_LANGUAGE);
911         BOOST_REQUIRE (i->note());
912         BOOST_CHECK_EQUAL (*i->note(), "badlang");
913         i++;
914         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::BAD_LANGUAGE);
915         BOOST_REQUIRE (i->note());
916         BOOST_CHECK_EQUAL (*i->note(), "wrong-andbad");
917 }
918
919
920 /* SMPTE DCP with invalid <Language> in the MainClosedCaption reel and also in the XML within the MXF */
921 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_languages)
922 {
923         boost::filesystem::path const dir("build/test/verify_invalid_closed_caption_languages");
924         prepare_directory (dir);
925         boost::filesystem::copy_file ("test/data/subs.mxf", dir / "subs.mxf");
926         shared_ptr<dcp::SMPTESubtitleAsset> asset(new dcp::SMPTESubtitleAsset(dir / "subs.mxf"));
927         asset->_language = "wrong-andbad";
928         asset->write (dir / "subs.mxf");
929         shared_ptr<dcp::ReelClosedCaptionAsset> reel_asset(new dcp::ReelClosedCaptionAsset(asset, dcp::Fraction(24, 1), 16 * 24, 0));
930         reel_asset->_language = "badlang";
931         shared_ptr<dcp::Reel> reel(new dcp::Reel());
932         reel->add (reel_asset);
933         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
934         cpl->add (reel);
935         shared_ptr<dcp::DCP> dcp(new dcp::DCP(dir));
936         dcp->add (cpl);
937         dcp->write_xml (dcp::SMPTE);
938
939         vector<boost::filesystem::path> dirs;
940         dirs.push_back (dir);
941         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
942         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
943         list<dcp::VerificationNote>::const_iterator i = notes.begin ();
944         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::BAD_LANGUAGE);
945         BOOST_REQUIRE (i->note());
946         BOOST_CHECK_EQUAL (*i->note(), "badlang");
947         i++;
948         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::BAD_LANGUAGE);
949         BOOST_REQUIRE (i->note());
950         BOOST_CHECK_EQUAL (*i->note(), "wrong-andbad");
951 }
952
953
954 /* SMPTE DCP with invalid <Language> in the MainSound reel and in the CPL additional subtitles languages */
955 BOOST_AUTO_TEST_CASE (verify_invalid_sound_reel_and_additional_language)
956 {
957         boost::filesystem::path const dir("build/test/verify_invalid_sound_reel_and_additional_language");
958         prepare_directory (dir);
959
960         shared_ptr<dcp::MonoPictureAsset> picture = simple_picture (dir, "foo");
961         shared_ptr<dcp::ReelPictureAsset> reel_picture(new dcp::ReelMonoPictureAsset(picture, 0));
962         shared_ptr<dcp::Reel> reel(new dcp::Reel());
963         reel->add (reel_picture);
964         shared_ptr<dcp::SoundAsset> sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "frobozz");
965         shared_ptr<dcp::ReelSoundAsset> reel_sound(new dcp::ReelSoundAsset(sound, 0));
966         reel->add (reel_sound);
967         shared_ptr<dcp::CPL> cpl(new dcp::CPL("hello", dcp::FEATURE));
968         cpl->add (reel);
969         cpl->_additional_subtitle_languages.push_back("this-is-wrong");
970         cpl->_additional_subtitle_languages.push_back("andso-is-this");
971         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
972         cpl->set_main_sound_sample_rate (48000);
973         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
974         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
975         shared_ptr<dcp::DCP> dcp(new dcp::DCP(dir));
976         dcp->add (cpl);
977         dcp->write_xml (dcp::SMPTE);
978
979         vector<boost::filesystem::path> dirs;
980         dirs.push_back (dir);
981         list<dcp::VerificationNote> notes = dcp::verify (dirs, &stage, &progress, xsd_test);
982         BOOST_REQUIRE_EQUAL (notes.size(), 3U);
983         list<dcp::VerificationNote>::const_iterator i = notes.begin ();
984         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::BAD_LANGUAGE);
985         BOOST_REQUIRE (i->note());
986         BOOST_CHECK_EQUAL (*i->note(), "this-is-wrong");
987         ++i;
988         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::BAD_LANGUAGE);
989         BOOST_REQUIRE (i->note());
990         BOOST_CHECK_EQUAL (*i->note(), "andso-is-this");
991         ++i;
992         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::BAD_LANGUAGE);
993         BOOST_REQUIRE (i->note());
994         BOOST_CHECK_EQUAL (*i->note(), "frobozz");
995         ++i;
996 }
997