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 */
157 e.message().find("schema document") != string::npos &&
158 e.message().find("has different target namespace from the one specified in instance document") != string::npos
163 _errors.push_back (e);
166 list<XMLValidationError> _errors;
169 class StringToXMLCh : public boost::noncopyable
172 StringToXMLCh (string a)
174 _buffer = XMLString::transcode(a.c_str());
179 XMLString::release (&_buffer);
182 XMLCh const * get () const {
190 class LocalFileResolver : public EntityResolver
193 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
194 : _xsd_dtd_directory (xsd_dtd_directory)
196 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
197 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
198 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
201 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id)
203 string system_id_str = xml_ch_to_string (system_id);
204 if (_files.find(system_id_str) == _files.end()) {
208 boost::filesystem::path p = _xsd_dtd_directory / _files[system_id_str];
209 StringToXMLCh ch (p.string());
210 return new LocalFileInputSource(ch.get());
214 void add (string uri, string file)
219 std::map<string, string> _files;
220 boost::filesystem::path _xsd_dtd_directory;
225 validate_xml (boost::filesystem::path xml_file, boost::filesystem::path xsd_dtd_directory, list<VerificationNote>& notes)
228 XMLPlatformUtils::Initialize ();
229 } catch (XMLException& e) {
230 throw MiscError ("Failed to initialise xerces library");
233 DCPErrorHandler error_handler;
235 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
237 XercesDOMParser parser;
238 parser.setValidationScheme(XercesDOMParser::Val_Always);
239 parser.setDoNamespaces(true);
240 parser.setDoSchema(true);
242 map<string, string> schema;
243 schema["http://www.w3.org/2000/09/xmldsig#"] = "xmldsig-core-schema.xsd";
244 schema["http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd"] = "xmldsig-core-schema.xsd";
245 schema["http://www.smpte-ra.org/schemas/429-7/2006/CPL"] = "SMPTE-429-7-2006-CPL.xsd";
246 schema["http://www.smpte-ra.org/schemas/429-8/2006/PKL"] = "SMPTE-429-8-2006-PKL.xsd";
247 schema["http://www.smpte-ra.org/schemas/429-9/2007/AM"] = "SMPTE-429-9-2007-AM.xsd";
248 schema["http://www.w3.org/2001/03/xml.xsd"] = "xml.xsd";
251 for (map<string, string>::const_iterator i = schema.begin(); i != schema.end(); ++i) {
252 locations += i->first;
254 boost::filesystem::path p = xsd_dtd_directory / i->second;
255 locations += p.string() + " ";
258 parser.setExternalSchemaLocation(locations.c_str());
259 parser.setValidationSchemaFullChecking(true);
260 parser.setErrorHandler(&error_handler);
262 LocalFileResolver resolver (xsd_dtd_directory);
263 parser.setEntityResolver(&resolver);
266 parser.resetDocumentPool();
267 parser.parse(xml_file.string().c_str());
268 } catch (XMLException& e) {
269 throw MiscError(xml_ch_to_string(e.getMessage()));
270 } catch (DOMException& e) {
271 throw MiscError(xml_ch_to_string(e.getMessage()));
273 throw MiscError("Unknown exception from xerces");
277 XMLPlatformUtils::Terminate ();
279 BOOST_FOREACH (XMLValidationError i, error_handler.errors()) {
282 VerificationNote::VERIFY_ERROR,
283 VerificationNote::XML_VALIDATION_ERROR,
293 verify_asset (shared_ptr<DCP> dcp, shared_ptr<ReelMXF> reel_mxf, function<void (float)> progress)
295 string const actual_hash = reel_mxf->asset_ref()->hash(progress);
297 list<shared_ptr<PKL> > pkls = dcp->pkls();
298 /* We've read this DCP in so it must have at least one PKL */
299 DCP_ASSERT (!pkls.empty());
301 shared_ptr<Asset> asset = reel_mxf->asset_ref().asset();
303 optional<string> pkl_hash;
304 BOOST_FOREACH (shared_ptr<PKL> i, pkls) {
305 pkl_hash = i->hash (reel_mxf->asset_ref()->id());
311 DCP_ASSERT (pkl_hash);
313 optional<string> cpl_hash = reel_mxf->hash();
314 if (cpl_hash && *cpl_hash != *pkl_hash) {
315 return RESULT_CPL_PKL_DIFFER;
318 if (actual_hash != *pkl_hash) {
326 list<VerificationNote>
328 vector<boost::filesystem::path> directories,
329 function<void (string, optional<boost::filesystem::path>)> stage,
330 function<void (float)> progress,
331 boost::filesystem::path xsd_dtd_directory
334 xsd_dtd_directory = boost::filesystem::canonical (xsd_dtd_directory);
336 list<VerificationNote> notes;
338 list<shared_ptr<DCP> > dcps;
339 BOOST_FOREACH (boost::filesystem::path i, directories) {
340 dcps.push_back (shared_ptr<DCP> (new DCP (i)));
343 BOOST_FOREACH (shared_ptr<DCP> dcp, dcps) {
344 stage ("Checking DCP", dcp->directory());
347 } catch (DCPReadError& e) {
348 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
349 } catch (XMLError& e) {
350 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
353 BOOST_FOREACH (shared_ptr<CPL> cpl, dcp->cpls()) {
354 stage ("Checking CPL", cpl->file());
355 validate_xml (cpl->file().get(), xsd_dtd_directory, notes);
357 /* Check that the CPL's hash corresponds to the PKL */
358 BOOST_FOREACH (shared_ptr<PKL> i, dcp->pkls()) {
359 optional<string> h = i->hash(cpl->id());
360 if (h && make_digest(Data(*cpl->file())) != *h) {
361 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::CPL_HASH_INCORRECT));
365 BOOST_FOREACH (shared_ptr<Reel> reel, cpl->reels()) {
366 stage ("Checking reel", optional<boost::filesystem::path>());
367 if (reel->main_picture()) {
368 /* Check reel stuff */
369 Fraction const frame_rate = reel->main_picture()->frame_rate();
370 if (frame_rate.denominator != 1 ||
371 (frame_rate.numerator != 24 &&
372 frame_rate.numerator != 25 &&
373 frame_rate.numerator != 30 &&
374 frame_rate.numerator != 48 &&
375 frame_rate.numerator != 50 &&
376 frame_rate.numerator != 60 &&
377 frame_rate.numerator != 96)) {
378 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INVALID_PICTURE_FRAME_RATE));
381 if (reel->main_picture()->asset_ref().resolved()) {
382 stage ("Checking picture asset hash", reel->main_picture()->asset()->file());
383 Result const r = verify_asset (dcp, reel->main_picture(), progress);
388 VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_HASH_INCORRECT, *reel->main_picture()->asset()->file()
392 case RESULT_CPL_PKL_DIFFER:
395 VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE, *reel->main_picture()->asset()->file()
404 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
405 stage ("Checking sound asset hash", reel->main_sound()->asset()->file());
406 Result const r = verify_asset (dcp, reel->main_sound(), progress);
411 VerificationNote::VERIFY_ERROR, VerificationNote::SOUND_HASH_INCORRECT, *reel->main_sound()->asset()->file()
415 case RESULT_CPL_PKL_DIFFER:
418 VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE, *reel->main_sound()->asset()->file()
429 BOOST_FOREACH (shared_ptr<PKL> pkl, dcp->pkls()) {
430 stage ("Checking PKL", pkl->file());
431 validate_xml (pkl->file().get(), xsd_dtd_directory, notes);
434 stage ("Checking ASSETMAP", dcp->asset_map_path().get());
435 validate_xml (dcp->asset_map_path().get(), xsd_dtd_directory, notes);
443 dcp::note_to_string (dcp::VerificationNote note)
445 switch (note.code()) {
446 case dcp::VerificationNote::GENERAL_READ:
448 case dcp::VerificationNote::CPL_HASH_INCORRECT:
449 return "The hash of the CPL in the PKL does not agree with the CPL file";
450 case dcp::VerificationNote::INVALID_PICTURE_FRAME_RATE:
451 return "The picture in a reel has an invalid frame rate";
452 case dcp::VerificationNote::PICTURE_HASH_INCORRECT:
453 return dcp::String::compose("The hash of the picture asset %1 does not agree with the PKL file", note.file()->filename());
454 case dcp::VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE:
455 return dcp::String::compose("The PKL and CPL hashes disagree for the picture asset %1", note.file()->filename());
456 case dcp::VerificationNote::SOUND_HASH_INCORRECT:
457 return dcp::String::compose("The hash of the sound asset %1 does not agree with the PKL file", note.file()->filename());
458 case dcp::VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE:
459 return dcp::String::compose("The PKL and CPL hashes disagree for the sound asset %1", note.file()->filename());
460 case dcp::VerificationNote::EMPTY_ASSET_PATH:
461 return "The asset map contains an empty asset path.";
462 case dcp::VerificationNote::MISSING_ASSET:
463 return String::compose("The file for an asset in the asset map cannot be found; missing file is %1.", note.file()->filename());
464 case dcp::VerificationNote::MISMATCHED_STANDARD:
465 return "The DCP contains both SMPTE and Interop parts.";
466 case dcp::VerificationNote::XML_VALIDATION_ERROR:
467 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());