2 Copyright (C) 2018-2019 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
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.
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.
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/>.
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
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.
38 #include "reel_picture_asset.h"
39 #include "reel_sound_asset.h"
40 #include "exceptions.h"
41 #include "compose.hpp"
42 #include "raw_convert.h"
43 #include <xercesc/util/PlatformUtils.hpp>
44 #include <xercesc/parsers/XercesDOMParser.hpp>
45 #include <xercesc/parsers/AbstractDOMParser.hpp>
46 #include <xercesc/sax/HandlerBase.hpp>
47 #include <xercesc/dom/DOMImplementation.hpp>
48 #include <xercesc/dom/DOMImplementationLS.hpp>
49 #include <xercesc/dom/DOMImplementationRegistry.hpp>
50 #include <xercesc/dom/DOMLSParser.hpp>
51 #include <xercesc/dom/DOMException.hpp>
52 #include <xercesc/dom/DOMDocument.hpp>
53 #include <xercesc/dom/DOMNodeList.hpp>
54 #include <xercesc/dom/DOMError.hpp>
55 #include <xercesc/dom/DOMLocator.hpp>
56 #include <xercesc/dom/DOMNamedNodeMap.hpp>
57 #include <xercesc/dom/DOMAttr.hpp>
58 #include <xercesc/dom/DOMErrorHandler.hpp>
59 #include <xercesc/framework/LocalFileInputSource.hpp>
60 #include <boost/noncopyable.hpp>
61 #include <boost/foreach.hpp>
62 #include <boost/algorithm/string.hpp>
73 using boost::shared_ptr;
74 using boost::optional;
75 using boost::function;
78 using namespace xercesc;
82 RESULT_CPL_PKL_DIFFER,
88 xml_ch_to_string (XMLCh const * a)
90 char* x = XMLString::transcode(a);
92 XMLString::release(&x);
96 class XMLValidationError
99 XMLValidationError (SAXParseException const & e)
100 : _message (xml_ch_to_string(e.getMessage()))
101 , _line (e.getLineNumber())
102 , _column (e.getColumnNumber())
107 string message () const {
111 uint64_t line () const {
115 uint64_t column () const {
126 class DCPErrorHandler : public ErrorHandler
129 void warning(const SAXParseException& e)
131 maybe_add (XMLValidationError(e));
134 void error(const SAXParseException& e)
136 maybe_add (XMLValidationError(e));
139 void fatalError(const SAXParseException& e)
141 maybe_add (XMLValidationError(e));
148 list<XMLValidationError> errors () const {
153 void maybe_add (XMLValidationError e)
155 /* XXX: nasty hack */
158 "schema document '/home/carl/src/libdcp/xsd/xml.xsd' has different target namespace "
159 "from the one specified in instance document 'http://www.w3.org/2001/03/xml.xsd'" ||
161 "schema document '/home/carl/src/libdcp/xsd/xmldsig-core-schema.xsd' has different target namespace "
162 "from the one specified in instance document 'http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd'" ||
164 "schema document '/home/carl/src/libdcp/xsd/SMPTE-429-8-2006-PKL.xsd' has different target namespace "
165 "from the one specified in instance document 'http://www.smpte-ra.org/schemas/429-8/2006/PKL'"
170 _errors.push_back (e);
173 list<XMLValidationError> _errors;
176 class StringToXMLCh : public boost::noncopyable
179 StringToXMLCh (string a)
181 _buffer = XMLString::transcode(a.c_str());
186 XMLString::release (&_buffer);
189 XMLCh const * get () const {
197 class LocalFileResolver : public EntityResolver
200 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
201 : _xsd_dtd_directory (xsd_dtd_directory)
203 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
204 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
205 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
208 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id)
210 string system_id_str = xml_ch_to_string (system_id);
211 if (_files.find(system_id_str) == _files.end()) {
215 boost::filesystem::path p = _xsd_dtd_directory / _files[system_id_str];
216 StringToXMLCh ch (p.string());
217 return new LocalFileInputSource(ch.get());
221 void add (string uri, string file)
226 std::map<string, string> _files;
227 boost::filesystem::path _xsd_dtd_directory;
232 validate_xml (boost::filesystem::path xml_file, boost::filesystem::path xsd_dtd_directory, list<VerificationNote>& notes)
235 XMLPlatformUtils::Initialize ();
236 } catch (XMLException& e) {
237 throw MiscError ("Failed to initialise xerces library");
240 DCPErrorHandler error_handler;
242 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
244 XercesDOMParser parser;
245 parser.setValidationScheme(XercesDOMParser::Val_Always);
246 parser.setDoNamespaces(true);
247 parser.setDoSchema(true);
249 map<string, string> schema;
250 schema["http://www.w3.org/2000/09/xmldsig#"] = "xmldsig-core-schema.xsd";
251 schema["http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd"] = "xmldsig-core-schema.xsd";
252 schema["http://www.smpte-ra.org/schemas/429-7/2006/CPL"] = "SMPTE-429-7-2006-CPL.xsd";
253 schema["http://www.smpte-ra.org/schemas/429-8/2006/PKL"] = "SMPTE-429-8-2006-PKL.xsd";
254 schema["http://www.smpte-ra.org/schemas/429-9/2007/AM"] = "SMPTE-429-9-2007-AM.xsd";
255 schema["http://www.w3.org/2001/03/xml.xsd"] = "xml.xsd";
258 for (map<string, string>::const_iterator i = schema.begin(); i != schema.end(); ++i) {
259 locations += i->first;
261 boost::filesystem::path p = xsd_dtd_directory / i->second;
262 locations += p.string() + " ";
265 parser.setExternalSchemaLocation(locations.c_str());
266 parser.setValidationSchemaFullChecking(true);
267 parser.setErrorHandler(&error_handler);
269 LocalFileResolver resolver (xsd_dtd_directory);
270 parser.setEntityResolver(&resolver);
273 parser.resetDocumentPool();
274 parser.parse(xml_file.string().c_str());
275 } catch (XMLException& e) {
276 throw MiscError(xml_ch_to_string(e.getMessage()));
277 } catch (DOMException& e) {
278 throw MiscError(xml_ch_to_string(e.getMessage()));
280 throw MiscError("Unknown exception from xerces");
284 XMLPlatformUtils::Terminate ();
286 BOOST_FOREACH (XMLValidationError i, error_handler.errors()) {
289 VerificationNote::VERIFY_ERROR,
290 VerificationNote::Code::XML_VALIDATION_ERROR,
300 verify_asset (shared_ptr<DCP> dcp, shared_ptr<ReelMXF> reel_mxf, function<void (float)> progress)
302 string const actual_hash = reel_mxf->asset_ref()->hash(progress);
304 list<shared_ptr<PKL> > pkls = dcp->pkls();
305 /* We've read this DCP in so it must have at least one PKL */
306 DCP_ASSERT (!pkls.empty());
308 shared_ptr<Asset> asset = reel_mxf->asset_ref().asset();
310 optional<string> pkl_hash;
311 BOOST_FOREACH (shared_ptr<PKL> i, pkls) {
312 pkl_hash = i->hash (reel_mxf->asset_ref()->id());
318 DCP_ASSERT (pkl_hash);
320 optional<string> cpl_hash = reel_mxf->hash();
321 if (cpl_hash && *cpl_hash != *pkl_hash) {
322 return RESULT_CPL_PKL_DIFFER;
325 if (actual_hash != *pkl_hash) {
333 list<VerificationNote>
335 vector<boost::filesystem::path> directories,
336 function<void (string, optional<boost::filesystem::path>)> stage,
337 function<void (float)> progress,
338 boost::filesystem::path xsd_dtd_directory
341 xsd_dtd_directory = boost::filesystem::canonical (xsd_dtd_directory);
343 list<VerificationNote> notes;
345 list<shared_ptr<DCP> > dcps;
346 BOOST_FOREACH (boost::filesystem::path i, directories) {
347 dcps.push_back (shared_ptr<DCP> (new DCP (i)));
350 BOOST_FOREACH (shared_ptr<DCP> dcp, dcps) {
351 stage ("Checking DCP", dcp->directory());
354 } catch (DCPReadError& e) {
355 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::GENERAL_READ, string(e.what())));
356 } catch (XMLError& e) {
357 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::GENERAL_READ, string(e.what())));
360 BOOST_FOREACH (shared_ptr<CPL> cpl, dcp->cpls()) {
361 stage ("Checking CPL", cpl->file());
362 validate_xml (cpl->file().get(), xsd_dtd_directory, notes);
364 /* Check that the CPL's hash corresponds to the PKL */
365 BOOST_FOREACH (shared_ptr<PKL> i, dcp->pkls()) {
366 optional<string> h = i->hash(cpl->id());
367 if (h && make_digest(Data(*cpl->file())) != *h) {
368 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::CPL_HASH_INCORRECT));
372 BOOST_FOREACH (shared_ptr<Reel> reel, cpl->reels()) {
373 stage ("Checking reel", optional<boost::filesystem::path>());
374 if (reel->main_picture()) {
375 /* Check reel stuff */
376 Fraction const frame_rate = reel->main_picture()->frame_rate();
377 if (frame_rate.denominator != 1 ||
378 (frame_rate.numerator != 24 &&
379 frame_rate.numerator != 25 &&
380 frame_rate.numerator != 30 &&
381 frame_rate.numerator != 48 &&
382 frame_rate.numerator != 50 &&
383 frame_rate.numerator != 60 &&
384 frame_rate.numerator != 96)) {
385 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INVALID_PICTURE_FRAME_RATE));
388 if (reel->main_picture()->asset_ref().resolved()) {
389 stage ("Checking picture asset hash", reel->main_picture()->asset()->file());
390 Result const r = verify_asset (dcp, reel->main_picture(), progress);
395 VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_HASH_INCORRECT, *reel->main_picture()->asset()->file()
399 case RESULT_CPL_PKL_DIFFER:
402 VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE, *reel->main_picture()->asset()->file()
411 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
412 stage ("Checking sound asset hash", reel->main_sound()->asset()->file());
413 Result const r = verify_asset (dcp, reel->main_sound(), progress);
418 VerificationNote::VERIFY_ERROR, VerificationNote::SOUND_HASH_INCORRECT, *reel->main_sound()->asset()->file()
422 case RESULT_CPL_PKL_DIFFER:
425 VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE, *reel->main_sound()->asset()->file()
436 BOOST_FOREACH (shared_ptr<PKL> pkl, dcp->pkls()) {
437 stage ("Checking PKL", pkl->file());
438 validate_xml (pkl->file().get(), xsd_dtd_directory, notes);
441 stage ("Checking ASSETMAP", dcp->asset_map_path().get());
442 validate_xml (dcp->asset_map_path().get(), xsd_dtd_directory, notes);
450 dcp::note_to_string (dcp::VerificationNote note)
452 switch (note.code()) {
453 case dcp::VerificationNote::GENERAL_READ:
455 case dcp::VerificationNote::CPL_HASH_INCORRECT:
456 return "The hash of the CPL in the PKL does not agree with the CPL file";
457 case dcp::VerificationNote::INVALID_PICTURE_FRAME_RATE:
458 return "The picture in a reel has an invalid frame rate";
459 case dcp::VerificationNote::PICTURE_HASH_INCORRECT:
460 return dcp::String::compose("The hash of the picture asset %1 does not agree with the PKL file", note.file()->filename());
461 case dcp::VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE:
462 return dcp::String::compose("The PKL and CPL hashes disagree for the picture asset %1", note.file()->filename());
463 case dcp::VerificationNote::SOUND_HASH_INCORRECT:
464 return dcp::String::compose("The hash of the sound asset %1 does not agree with the PKL file", note.file()->filename());
465 case dcp::VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE:
466 return dcp::String::compose("The PKL and CPL hashes disagree for the sound asset %1", note.file()->filename());
467 case dcp::VerificationNote::EMPTY_ASSET_PATH:
468 return "The asset map contains an empty asset path.";
469 case dcp::VerificationNote::MISSING_ASSET:
470 return String::compose("The file for an asset in the asset map cannot be found; missing file is %1.", note.file()->filename());
471 case dcp::VerificationNote::MISMATCHED_STANDARD:
472 return "The DCP contains both SMPTE and Interop parts.";
473 case dcp::VerificationNote::XML_VALIDATION_ERROR:
474 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());