Bv2.1 8.7: CPLs with encrypted content must be signed.
[libdcp.git] / src / verify.cc
index 2c0da8d7b77eb62f62c23d70c667df8fcae708a3..3f15668fcf88b9a591778b011be73d3c7237c74c 100644 (file)
@@ -47,6 +47,7 @@
 #include "exceptions.h"
 #include "compose.hpp"
 #include "raw_convert.h"
+#include "reel_markers_asset.h"
 #include "smpte_subtitle_asset.h"
 #include <xercesc/util/PlatformUtils.hpp>
 #include <xercesc/parsers/XercesDOMParser.hpp>
@@ -78,6 +79,7 @@ using std::string;
 using std::cout;
 using std::map;
 using std::max;
+using std::set;
 using std::shared_ptr;
 using std::make_shared;
 using boost::optional;
@@ -262,7 +264,7 @@ parse (XercesDOMParser& parser, boost::filesystem::path xml)
 
 
 static void
-parse (XercesDOMParser& parser, std::string xml)
+parse (XercesDOMParser& parser, string xml)
 {
        xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
        parser.parse(buf);
@@ -616,6 +618,13 @@ verify_main_sound_asset (
        stage ("Checking sound asset metadata", asset->file());
 
        verify_language_tag (asset->language(), notes);
+       if (asset->sampling_rate() != 48000) {
+               notes.push_back (
+                       VerificationNote(
+                               VerificationNote::VERIFY_BV21_ERROR, VerificationNote::INVALID_SOUND_FRAME_RATE, *asset->file()
+                               )
+                       );
+       }
 }
 
 
@@ -626,6 +635,12 @@ verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vecto
        if (reel_asset->language()) {
                verify_language_tag (*reel_asset->language(), notes);
        }
+
+       if (!reel_asset->entry_point()) {
+               notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_ENTRY_POINT });
+       } else if (reel_asset->entry_point().get()) {
+               notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_ENTRY_POINT_NON_ZERO });
+       }
 }
 
 
@@ -636,6 +651,12 @@ verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset,
        if (reel_asset->language()) {
                verify_language_tag (*reel_asset->language(), notes);
        }
+
+       if (!reel_asset->entry_point()) {
+               notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_CLOSED_CAPTION_ENTRY_POINT });
+       } else if (reel_asset->entry_point().get()) {
+               notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_ENTRY_POINT_NON_ZERO });
+       }
 }
 
 
@@ -758,7 +779,8 @@ check_text_timing (
        vector<shared_ptr<dcp::Reel>> reels,
        optional<int> picture_frame_rate,
        vector<VerificationNote>& notes,
-       std::function<std::string (shared_ptr<dcp::Reel>)> xml,
+       std::function<bool (shared_ptr<dcp::Reel>)> check,
+       std::function<string (shared_ptr<dcp::Reel>)> xml,
        std::function<int64_t (shared_ptr<dcp::Reel>)> duration
        )
 {
@@ -798,6 +820,10 @@ check_text_timing (
        };
 
        for (auto i = 0U; i < reels.size(); ++i) {
+               if (!check(reels[i])) {
+                       continue;
+               }
+
                /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
                 * read in by libdcp's parser.
                 */
@@ -835,6 +861,105 @@ check_text_timing (
 }
 
 
+struct LinesCharactersResult
+{
+       bool warning_length_exceeded = false;
+       bool error_length_exceeded = false;
+       bool line_count_exceeded = false;
+};
+
+
+static
+void
+check_text_lines_and_characters (
+       shared_ptr<SubtitleAsset> asset,
+       int warning_length,
+       int error_length,
+       LinesCharactersResult* result
+       )
+{
+       class Event
+       {
+       public:
+               Event (dcp::Time time_, float position_, int characters_)
+                       : time (time_)
+                       , position (position_)
+                       , characters (characters_)
+               {}
+
+               Event (dcp::Time time_, shared_ptr<Event> start_)
+                       : time (time_)
+                       , start (start_)
+               {}
+
+               dcp::Time time;
+               int position; //< position from 0 at top of screen to 100 at bottom
+               int characters;
+               shared_ptr<Event> start;
+       };
+
+       vector<shared_ptr<Event>> events;
+
+       auto position = [](shared_ptr<const SubtitleString> sub) {
+               switch (sub->v_align()) {
+               case VALIGN_TOP:
+                       return lrintf(sub->v_position() * 100);
+               case VALIGN_CENTER:
+                       return lrintf((0.5f + sub->v_position()) * 100);
+               case VALIGN_BOTTOM:
+                       return lrintf((1.0f - sub->v_position()) * 100);
+               }
+
+               return 0L;
+       };
+
+       for (auto j: asset->subtitles()) {
+               auto text = dynamic_pointer_cast<const SubtitleString>(j);
+               if (text) {
+                       auto in = make_shared<Event>(text->in(), position(text), text->text().length());
+                       events.push_back(in);
+                       events.push_back(make_shared<Event>(text->out(), in));
+               }
+       }
+
+       std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
+               return a->time < b->time;
+       });
+
+       map<int, int> current;
+       for (auto i: events) {
+               if (current.size() > 3) {
+                       result->line_count_exceeded = true;
+               }
+               for (auto j: current) {
+                       if (j.second >= warning_length) {
+                               result->warning_length_exceeded = true;
+                       }
+                       if (j.second >= error_length) {
+                               result->error_length_exceeded = true;
+                       }
+               }
+
+               if (i->start) {
+                       /* end of a subtitle */
+                       DCP_ASSERT (current.find(i->start->position) != current.end());
+                       if (current[i->start->position] == i->start->characters) {
+                               current.erase(i->start->position);
+                       } else {
+                               current[i->start->position] -= i->start->characters;
+                       }
+               } else {
+                       /* start of a subtitle */
+                       if (current.find(i->position) == current.end()) {
+                               current[i->position] = i->characters;
+                       } else {
+                               current[i->position] += i->characters;
+                       }
+               }
+       }
+}
+
+
 static
 void
 check_text_timing (vector<shared_ptr<dcp::Reel>> reels, vector<VerificationNote>& notes)
@@ -850,6 +975,9 @@ check_text_timing (vector<shared_ptr<dcp::Reel>> reels, vector<VerificationNote>
 
        if (reels[0]->main_subtitle()) {
                check_text_timing (reels, picture_frame_rate, notes,
+                       [](shared_ptr<dcp::Reel> reel) {
+                               return static_cast<bool>(reel->main_subtitle());
+                       },
                        [](shared_ptr<dcp::Reel> reel) {
                                return reel->main_subtitle()->asset()->raw_xml();
                        },
@@ -861,6 +989,9 @@ check_text_timing (vector<shared_ptr<dcp::Reel>> reels, vector<VerificationNote>
 
        for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
                check_text_timing (reels, picture_frame_rate, notes,
+                       [i](shared_ptr<dcp::Reel> reel) {
+                               return i < reel->closed_captions().size();
+                       },
                        [i](shared_ptr<dcp::Reel> reel) {
                                return reel->closed_captions()[i]->asset()->raw_xml();
                        },
@@ -872,6 +1003,64 @@ check_text_timing (vector<shared_ptr<dcp::Reel>> reels, vector<VerificationNote>
 }
 
 
+void
+check_extension_metadata (shared_ptr<dcp::CPL> cpl, vector<VerificationNote>& notes)
+{
+       DCP_ASSERT (cpl->file());
+       cxml::Document doc ("CompositionPlaylist");
+       doc.read_file (cpl->file().get());
+
+       auto missing = false;
+       string malformed;
+
+       if (auto reel_list = doc.node_child("ReelList")) {
+               auto reels = reel_list->node_children("Reel");
+               if (!reels.empty()) {
+                       if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
+                               if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
+                                       if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
+                                               missing = true;
+                                               for (auto extension: extension_list->node_children("ExtensionMetadata")) {
+                                                       if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
+                                                               continue;
+                                                       }
+                                                       missing = false;
+                                                       if (auto name = extension->optional_node_child("Name")) {
+                                                               if (name->content() != "Application") {
+                                                                       malformed = "<Name> should be 'Application'";
+                                                               }
+                                                       }
+                                                       if (auto property_list = extension->optional_node_child("PropertyList")) {
+                                                               if (auto property = property_list->optional_node_child("Property")) {
+                                                                       if (auto name = property->optional_node_child("Name")) {
+                                                                               if (name->content() != "DCP Constraints Profile") {
+                                                                                       malformed = "<Name> property should be 'DCP Constraints Profile'";
+                                                                               }
+                                                                       }
+                                                                       if (auto value = property->optional_node_child("Value")) {
+                                                                               if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
+                                                                                       malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
+                                                                               }
+                                                                       }
+                                                               }
+                                                       }
+                                               }
+                                       } else {
+                                               missing = true;
+                                       }
+                               }
+                       }
+               }
+       }
+
+       if (missing) {
+               notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_EXTENSION_METADATA});
+       } else if (!malformed.empty()) {
+               notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::INVALID_EXTENSION_METADATA, malformed});
+       }
+}
+
+
 vector<VerificationNote>
 dcp::verify (
        vector<boost::filesystem::path> directories,
@@ -917,7 +1106,25 @@ dcp::verify (
                        }
 
                        if (cpl->release_territory()) {
-                               verify_language_tag (cpl->release_territory().get(), notes);
+                               if (!cpl->release_territory_scope() || cpl->release_territory_scope().get() != "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata#scope/release-territory/UNM49") {
+                                       auto terr = cpl->release_territory().get();
+                                       /* Must be a valid region tag, or "001" */
+                                       try {
+                                               LanguageTag::RegionSubtag test (terr);
+                                       } catch (...) {
+                                               if (terr != "001") {
+                                                       notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::BAD_LANGUAGE, terr});
+                                               }
+                                       }
+                               }
+                       }
+
+                       if (dcp->standard() == dcp::SMPTE) {
+                               if (!cpl->annotation_text()) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_ANNOTATION_TEXT_IN_CPL));
+                               } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::CPL_ANNOTATION_TEXT_DIFFERS_FROM_CONTENT_TITLE_TEXT));
+                               }
                        }
 
                        /* Check that the CPL's hash corresponds to the PKL */
@@ -928,6 +1135,16 @@ dcp::verify (
                                }
                        }
 
+                       /* set to true if any reel has a MainSubtitle */
+                       auto have_main_subtitle = false;
+                       /* set to true if any reel has no MainSubtitle */
+                       auto have_no_main_subtitle = false;
+                       /* fewest number of closed caption assets seen in a reel */
+                       size_t fewest_closed_captions = SIZE_MAX;
+                       /* most number of closed caption assets seen in a reel */
+                       size_t most_closed_captions = 0;
+                       map<Marker, Time> markers_seen;
+
                        for (auto reel: cpl->reels()) {
                                stage ("Checking reel", optional<boost::filesystem::path>());
 
@@ -938,6 +1155,22 @@ dcp::verify (
                                        if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
                                                notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INTRINSIC_DURATION_TOO_SMALL, i->id()));
                                        }
+                                       auto mxf = dynamic_pointer_cast<ReelMXF>(i);
+                                       if (mxf && !mxf->hash()) {
+                                               notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_HASH, i->id()});
+                                       }
+                               }
+
+                               if (dcp->standard() == dcp::SMPTE) {
+                                       boost::optional<int64_t> duration;
+                                       for (auto i: reel->assets()) {
+                                               if (!duration) {
+                                                       duration = i->actual_duration();
+                                               } else if (*duration != i->actual_duration()) {
+                                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISMATCHED_ASSET_DURATION, i->id()));
+                                                       break;
+                                               }
+                                       }
                                }
 
                                if (reel->main_picture()) {
@@ -968,6 +1201,9 @@ dcp::verify (
                                        if (reel->main_subtitle()->asset_ref().resolved()) {
                                                verify_subtitle_asset (reel->main_subtitle()->asset(), stage, xsd_dtd_directory, notes, state);
                                        }
+                                       have_main_subtitle = true;
+                               } else {
+                                       have_no_main_subtitle = true;
                                }
 
                                for (auto i: reel->closed_captions()) {
@@ -976,10 +1212,103 @@ dcp::verify (
                                                verify_closed_caption_asset (i->asset(), stage, xsd_dtd_directory, notes, state);
                                        }
                                }
+
+                               if (reel->main_markers()) {
+                                       for (auto const& i: reel->main_markers()->get()) {
+                                               markers_seen.insert (i);
+                                       }
+                               }
+
+                               fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
+                               most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
                        }
 
                        if (dcp->standard() == dcp::SMPTE) {
+
+                               if (have_main_subtitle && have_no_main_subtitle) {
+                                       notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MAIN_SUBTITLE_NOT_IN_ALL_REELS});
+                               }
+
+                               if (fewest_closed_captions != most_closed_captions) {
+                                       notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_ASSET_COUNTS_DIFFER});
+                               }
+
+                               if (cpl->content_kind() == FEATURE) {
+                                       if (markers_seen.find(dcp::Marker::FFEC) == markers_seen.end()) {
+                                               notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_FFEC_IN_FEATURE});
+                                       }
+                                       if (markers_seen.find(dcp::Marker::FFMC) == markers_seen.end()) {
+                                               notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_FFMC_IN_FEATURE});
+                                       }
+                               }
+
+                               auto ffoc = markers_seen.find(dcp::Marker::FFOC);
+                               if (ffoc == markers_seen.end()) {
+                                       notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::MISSING_FFOC});
+                               } else if (ffoc->second.e != 1) {
+                                       notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::INCORRECT_FFOC});
+                               }
+
+                               auto lfoc = markers_seen.find(dcp::Marker::LFOC);
+                               if (lfoc == markers_seen.end()) {
+                                       notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::MISSING_LFOC});
+                               } else if (lfoc->second.as_editable_units(lfoc->second.tcr) != (cpl->reels().back()->duration() - 1)) {
+                                       notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::INCORRECT_LFOC});
+                               }
+
                                check_text_timing (cpl->reels(), notes);
+
+                               LinesCharactersResult result;
+                               for (auto reel: cpl->reels()) {
+                                       if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
+                                               check_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
+                                       }
+                               }
+
+                               if (result.line_count_exceeded) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::TOO_MANY_SUBTITLE_LINES));
+                               }
+                               if (result.error_length_exceeded) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_TOO_LONG));
+                               } else if (result.warning_length_exceeded) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED));
+                               }
+
+                               result = LinesCharactersResult();
+                               for (auto reel: cpl->reels()) {
+                                       for (auto i: reel->closed_captions()) {
+                                               if (i->asset()) {
+                                                       check_text_lines_and_characters (i->asset(), 32, 32, &result);
+                                               }
+                                       }
+                               }
+
+                               if (result.line_count_exceeded) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES));
+                               }
+                               if (result.error_length_exceeded) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG));
+                               }
+
+                               if (!cpl->full_content_title_text()) {
+                                       /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we
+                                        * can use it as a proxy for CompositionMetadataAsset's existence.
+                                        */
+                                       notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_CPL_METADATA});
+                               } else if (!cpl->version_number()) {
+                                       notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_CPL_METADATA_VERSION_NUMBER});
+                               }
+
+                               check_extension_metadata (cpl, notes);
+
+                               if (cpl->encrypted()) {
+                                       cxml::Document doc ("CompositionPlaylist");
+                                       DCP_ASSERT (cpl->file());
+                                       doc.read_file (cpl->file().get());
+                                       if (!doc.optional_node_child("Signature")) {
+                                               notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CPL_WITH_ENCRYPTED_CONTENT_NOT_SIGNED, cpl->file().get()});
+                                       }
+                               }
                        }
                }
 
@@ -1069,6 +1398,60 @@ dcp::note_to_string (dcp::VerificationNote note)
                return "At least one subtitle is less than the minimum of 15 frames suggested by Bv2.1";
        case dcp::VerificationNote::SUBTITLE_TOO_CLOSE:
                return "At least one pair of subtitles are separated by less than the the minimum of 2 frames suggested by Bv2.1";
+       case dcp::VerificationNote::TOO_MANY_SUBTITLE_LINES:
+               return "There are more than 3 subtitle lines in at least one place in the DCP, which Bv2.1 advises against.";
+       case dcp::VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED:
+               return "There are more than 52 characters in at least one subtitle line, which Bv2.1 advises against.";
+       case dcp::VerificationNote::SUBTITLE_LINE_TOO_LONG:
+               return "There are more than 79 characters in at least one subtitle line, which Bv2.1 strongly advises against.";
+       case dcp::VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES:
+               return "There are more than 3 closed caption lines in at least one place, which is disallowed by Bv2.1";
+       case dcp::VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG:
+               return "There are more than 32 characters in at least one closed caption line, which is disallowed by Bv2.1";
+       case dcp::VerificationNote::INVALID_SOUND_FRAME_RATE:
+               return "A sound asset has a sampling rate other than 48kHz, which is disallowed by Bv2.1";
+       case dcp::VerificationNote::MISSING_ANNOTATION_TEXT_IN_CPL:
+               return "The CPL has no <AnnotationText> tag, which is required by Bv2.1";
+       case dcp::VerificationNote::CPL_ANNOTATION_TEXT_DIFFERS_FROM_CONTENT_TITLE_TEXT:
+               return "The CPL's <AnnotationText> differs from its <ContentTitleText>, which Bv2.1 advises against.";
+       case dcp::VerificationNote::MISMATCHED_ASSET_DURATION:
+               return "All assets in a reel do not have the same duration, which is required by Bv2.1";
+       case dcp::VerificationNote::MAIN_SUBTITLE_NOT_IN_ALL_REELS:
+               return "At least one reel contains a subtitle asset, but some reel(s) do not";
+       case dcp::VerificationNote::CLOSED_CAPTION_ASSET_COUNTS_DIFFER:
+               return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
+       case dcp::VerificationNote::MISSING_SUBTITLE_ENTRY_POINT:
+               return "Subtitle assets must have an <EntryPoint> tag.";
+       case dcp::VerificationNote::SUBTITLE_ENTRY_POINT_NON_ZERO:
+               return "Subtitle assets must have an <EntryPoint> of 0.";
+       case dcp::VerificationNote::MISSING_CLOSED_CAPTION_ENTRY_POINT:
+               return "Closed caption assets must have an <EntryPoint> tag.";
+       case dcp::VerificationNote::CLOSED_CAPTION_ENTRY_POINT_NON_ZERO:
+               return "Closed caption assets must have an <EntryPoint> of 0.";
+       case dcp::VerificationNote::MISSING_HASH:
+               return String::compose("An asset is missing a <Hash> tag: %1", note.note().get());
+       case dcp::VerificationNote::MISSING_FFEC_IN_FEATURE:
+               return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
+       case dcp::VerificationNote::MISSING_FFMC_IN_FEATURE:
+               return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
+       case dcp::VerificationNote::MISSING_FFOC:
+               return "There should be a FFOC (first frame of content) marker";
+       case dcp::VerificationNote::MISSING_LFOC:
+               return "There should be a LFOC (last frame of content) marker";
+       case dcp::VerificationNote::INCORRECT_FFOC:
+               return "The FFOC marker should bet set to 1";
+       case dcp::VerificationNote::INCORRECT_LFOC:
+               return "The LFOC marker should be set to 1 less than the duration of the last reel";
+       case dcp::VerificationNote::MISSING_CPL_METADATA:
+               return "There should be a <CompositionMetadataAsset> tag";
+       case dcp::VerificationNote::MISSING_CPL_METADATA_VERSION_NUMBER:
+               return "The CPL metadata must contain a <VersionNumber>";
+       case dcp::VerificationNote::MISSING_EXTENSION_METADATA:
+               return "The CPL metadata must contain <ExtensionMetadata>";
+       case dcp::VerificationNote::INVALID_EXTENSION_METADATA:
+               return String::compose("The <ExtensionMetadata> is malformed in some way: %1", note.note().get());
+       case dcp::VerificationNote::CPL_WITH_ENCRYPTED_CONTENT_NOT_SIGNED:
+               return String::compose("The CPL %1, which has encrypted content, is not signed", note.file()->filename());
        }
 
        return "";