From 5b57e219a823d768dd8df3ee4f83dcfe39b5d896 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Wed, 22 Mar 2023 19:43:02 +0400 Subject: [PATCH 01/16] Introduced zim::Metadata --- src/metadata.cpp | 131 +++++++++++++++++++++++++++++++++++ src/metadata.h | 69 ++++++++++++++++++ src/metadata_constraints.cpp | 29 ++++++++ test/meson.build | 2 + test/metadata-test.cpp | 130 ++++++++++++++++++++++++++++++++++ 5 files changed, 361 insertions(+) create mode 100644 src/metadata.cpp create mode 100644 src/metadata.h create mode 100644 src/metadata_constraints.cpp create mode 100644 test/metadata-test.cpp diff --git a/src/metadata.cpp b/src/metadata.cpp new file mode 100644 index 00000000..343066c7 --- /dev/null +++ b/src/metadata.cpp @@ -0,0 +1,131 @@ +/* + * Copyright 2023 Veloman Yunkan <veloman.yunkan@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#include "metadata.h" + +#include <sstream> +#include <regex> + +namespace zim +{ + +namespace +{ + +const bool MANDATORY = true; +const bool OPTIONAL = false; + +const std::string LANGS_REGEXP = "\\w{3}(,\\w{3})*"; +const std::string DATE_REGEXP = R"(\d\d\d\d-\d\d-\d\d)"; +const std::string PNG_REGEXP = "^\x89\x50\x4e\x47\x0d\x0a\x1a\x0a.+"; + +bool matchRegex(const std::string& regexStr, const std::string& text) +{ + const std::regex regex(regexStr); + return std::regex_match(text.begin(), text.end(), regex); +} + + +#include "metadata_constraints.cpp" + +} // unnamed namespace + +const Metadata::ReservedMetadataTable& Metadata::reservedMetadataInfo = reservedMetadataInfoTable; + +const Metadata::ReservedMetadataRecord& +Metadata::getReservedMetadataRecord(const std::string& name) +{ + for ( const auto& x : reservedMetadataInfo ) { + if ( x.name == name ) + return x; + } + + throw std::out_of_range(name + " is not a reserved metadata name"); +} + +bool Metadata::has(const std::string& name) const +{ + return data.find(name) != data.end(); +} + +const std::string& Metadata::operator[](const std::string& name) const +{ + return data.at(name); +} + +void Metadata::set(const std::string& name, const std::string& value) +{ + data[name] = value; +} + +bool Metadata::valid() const +{ + return check().empty(); +} + +Metadata::Errors Metadata::checkMandatoryMetadata() const +{ + Errors errors; + for ( const auto& rmr : reservedMetadataInfo ) { + if ( rmr.isMandatory && data.find(rmr.name) == data.end() ) { + errors.push_back("Missing mandatory metadata: " + rmr.name ); + } + } + + return errors; +} + +Metadata::Errors Metadata::checkSimpleConstraints() const +{ + Errors errors; + for ( const auto& nv : data ) { + const auto& name = nv.first; + const auto& value = nv.second; + try { + const auto& rmr = getReservedMetadataRecord(name); + if ( value.size() < rmr.minLength ) { + std::ostringstream oss; + oss << name << " must be at least " << rmr.minLength << " bytes"; + errors.push_back(oss.str()); + } + if ( rmr.maxLength != 0 && value.size() > rmr.maxLength ) { + std::ostringstream oss; + oss << name << " must be at most " << rmr.maxLength << " bytes"; + errors.push_back(oss.str()); + } + if ( !rmr.regex.empty() && !matchRegex(rmr.regex, value) ) { + errors.push_back(name + " doesn't match regex: " + rmr.regex); + } + } catch ( const std::out_of_range& ) { + // ignore non-reserved metadata + } + } + return errors; +} + +Metadata::Errors Metadata::check() const +{ + Errors e = checkMandatoryMetadata(); + if ( !e.empty() ) + return e; + + return checkSimpleConstraints(); +} + +} // namespace zim diff --git a/src/metadata.h b/src/metadata.h new file mode 100644 index 00000000..ec14cc62 --- /dev/null +++ b/src/metadata.h @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Veloman Yunkan <veloman.yunkan@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#ifndef OPENZIM_METADATA_H +#define OPENZIM_METADATA_H + +#include <string> +#include <vector> +#include <map> + +namespace zim +{ + +class Metadata +{ +public: // types + struct ReservedMetadataRecord + { + const std::string name; + const bool isMandatory; + const size_t minLength; + const size_t maxLength; + const std::string regex; + }; + + typedef std::vector<ReservedMetadataRecord> ReservedMetadataTable; + + typedef std::vector<std::string> Errors; + +public: // data + static const ReservedMetadataTable& reservedMetadataInfo; + +public: // functions + void set(const std::string& name, const std::string& value); + bool has(const std::string& name) const; + const std::string& operator[](const std::string& name) const; + + bool valid() const; + Errors check() const; + + static const ReservedMetadataRecord& getReservedMetadataRecord(const std::string& name); + +private: // functions + Errors checkMandatoryMetadata() const; + Errors checkSimpleConstraints() const; + +private: // data + std::map<std::string, std::string> data; +}; + +} // namespace zim + +#endif // OPENZIM_METADATA_H diff --git a/src/metadata_constraints.cpp b/src/metadata_constraints.cpp new file mode 100644 index 00000000..bee79ab7 --- /dev/null +++ b/src/metadata_constraints.cpp @@ -0,0 +1,29 @@ +const Metadata::ReservedMetadataTable reservedMetadataInfoTable = { + // name isMandatory minLength maxLength regex + { "Name", MANDATORY, 1, 0, "" }, + { "Title", MANDATORY, 1, 30, "" }, + { "Language", MANDATORY, 3, 0, LANGS_REGEXP }, + { "Creator", MANDATORY, 1, 0, "" }, + { "Publisher", MANDATORY, 1, 0, "" }, + { "Date", MANDATORY, 10, 10, DATE_REGEXP }, + { "Description", MANDATORY, 1, 80, "" }, + { "LongDescription", OPTIONAL, 1, 4000, "" }, + { "License", OPTIONAL, 1, 0, "" }, + { "Tags", OPTIONAL, 1, 0, "" }, + { "Relation", OPTIONAL, 1, 0, "" }, + { "Flavour", OPTIONAL, 1, 0, "" }, + { "Source", OPTIONAL, 1, 0, "" }, + { "Counter", OPTIONAL, 1, 0, "" }, + { "Scraper", OPTIONAL, 1, 0, "" }, + + { + "Illustration_48x48@1", + MANDATORY, + 67, // this is the lower limit on a syntactically valid PNG file + // (according to https://github.com/mathiasbynens/small) + 10000, // this is roughly the size of the raw (i.e. without any compression) + // RGBA pixel data of a 48x48 image + // Question: how much PNG metadata shall we allow? + PNG_REGEXP + }, +}; diff --git a/test/meson.build b/test/meson.build index 3669d109..26530901 100644 --- a/test/meson.build +++ b/test/meson.build @@ -1,6 +1,7 @@ gtest_dep = dependency('gtest', main:true, fallback:['gtest', 'gtest_main_dep'], required:false) tests = [ + 'metadata-test', 'tools-test', 'zimwriterfs-zimcreatorfs', 'zimcheck-test' @@ -12,6 +13,7 @@ zimwriter_srcs = [ '../src/zimwriterfs/tools.cpp', tests_src_map = { 'zimcheck-test' : ['../src/zimcheck/zimcheck.cpp', '../src/zimcheck/checks.cpp', '../src/zimcheck/json_tools.cpp', '../src/tools.cpp'], 'tools-test' : zimwriter_srcs, + 'metadata-test' : ['../src/metadata.cpp'], 'zimwriterfs-zimcreatorfs' : zimwriter_srcs } if gtest_dep.found() and not meson.is_cross_build() diff --git a/test/metadata-test.cpp b/test/metadata-test.cpp new file mode 100644 index 00000000..67dba591 --- /dev/null +++ b/test/metadata-test.cpp @@ -0,0 +1,130 @@ +#include "../src/metadata.h" + +#include "gtest/gtest.h" + + +TEST(Metadata, isDefaultConstructible) +{ + zim::Metadata m; + (void)m; // suppress compiler's warning about an unused variable +} + + +TEST(Metadata, detectsAbsenceOfMandatoryEntries) +{ + zim::Metadata m; + + ASSERT_FALSE(m.valid()); + ASSERT_EQ(m.check(), + zim::Metadata::Errors({ + "Missing mandatory metadata: Name", + "Missing mandatory metadata: Title", + "Missing mandatory metadata: Language", + "Missing mandatory metadata: Creator", + "Missing mandatory metadata: Publisher", + "Missing mandatory metadata: Date", + "Missing mandatory metadata: Description", + "Missing mandatory metadata: Illustration_48x48@1", + }) + ); + + m.set("Description", "Any nonsense is better than nothing"); + m.set("Date", "2020-20-20"); + m.set("Creator", "Demiurge"); + m.set("Name", "wikipedia_py_all"); + + ASSERT_FALSE(m.valid()); + ASSERT_EQ(m.check(), + zim::Metadata::Errors({ + "Missing mandatory metadata: Title", + "Missing mandatory metadata: Language", + "Missing mandatory metadata: Publisher", + "Missing mandatory metadata: Illustration_48x48@1", + }) + ); + + m.set("Title", "Chief Executive Officer"); + m.set("Publisher", "Zangak"); + m.set("Language", "py3"); + m.set("Illustration_48x48@1", "\x89PNG\r\n\x1a\n" + std::string(100, 'x')); + + ASSERT_TRUE(m.valid()); + ASSERT_TRUE(m.check().empty()); +} + +zim::Metadata makeValidMetadata() +{ + zim::Metadata m; + + m.set("Description", "Any nonsense is better than nothing"); + m.set("Date", "2020-20-20"); + m.set("Creator", "Demiurge"); + m.set("Name", "wikipedia_py_all"); + m.set("Title", "Chief Executive Officer"); + m.set("Publisher", "Zangak"); + m.set("Language", "py3"); + m.set("Illustration_48x48@1", "\x89PNG\r\n\x1a\n" + std::string(100, 'x')); + + return m; +} + +TEST(Metadata, nonReservedMetadataIsNotAProblem) +{ + zim::Metadata m = makeValidMetadata(); + m.set("NonReservedMetadata", ""); + ASSERT_TRUE(m.valid()); +} + +TEST(Metadata, minSizeConstraints) +{ + zim::Metadata m = makeValidMetadata(); + m.set("Title", ""); + ASSERT_FALSE(m.valid()); + ASSERT_EQ(m.check(), + zim::Metadata::Errors({ + "Title must be at least 1 bytes" + }) + ); + m.set("Title", "t"); + ASSERT_TRUE(m.valid()); +} + +TEST(Metadata, maxSizeConstraints) +{ + zim::Metadata m = makeValidMetadata(); + m.set("Title", std::string(31, 'a')); + ASSERT_FALSE(m.valid()); + ASSERT_EQ(m.check(), + zim::Metadata::Errors({ + "Title must be at most 30 bytes" + }) + ); + m.set("Title", std::string(30, 'a')); + ASSERT_TRUE(m.valid()); +} + +TEST(Metadata, regexpConstraints) +{ + zim::Metadata m = makeValidMetadata(); + m.set("Date", "YYYY-MM-DD"); + ASSERT_FALSE(m.valid()); + ASSERT_EQ(m.check(), + zim::Metadata::Errors({ + "Date doesn't match regex: \\d\\d\\d\\d-\\d\\d-\\d\\d" + }) + ); + m.set("Date", "1234-56-78"); // Yes, such a date is considered valid + // by the current simple regex + ASSERT_TRUE(m.valid()); + + m.set("Language", "fre,"); + ASSERT_FALSE(m.valid()); + ASSERT_EQ(m.check(), + zim::Metadata::Errors({ + "Language doesn't match regex: \\w{3}(,\\w{3})*" + }) + ); + + m.set("Language", "fre,nch"); + ASSERT_TRUE(m.valid()); +} From 7e4249deacf051f4b1c90c7ff575f307fdd9cb88 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Tue, 28 Mar 2023 19:14:12 +0400 Subject: [PATCH 02/16] Metadata length is measured in characters --- src/metadata.cpp | 13 +++++++++---- src/metadata_constraints.cpp | 7 ++----- test/metadata-test.cpp | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/metadata.cpp b/src/metadata.cpp index 343066c7..cb232846 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -21,6 +21,7 @@ #include <sstream> #include <regex> +#include <unicode/unistr.h> namespace zim { @@ -41,6 +42,10 @@ bool matchRegex(const std::string& regexStr, const std::string& text) return std::regex_match(text.begin(), text.end(), regex); } +size_t getTextLength(const std::string& utf8EncodedString) +{ + return icu::UnicodeString::fromUTF8(utf8EncodedString).length(); +} #include "metadata_constraints.cpp" @@ -99,14 +104,14 @@ Metadata::Errors Metadata::checkSimpleConstraints() const const auto& value = nv.second; try { const auto& rmr = getReservedMetadataRecord(name); - if ( value.size() < rmr.minLength ) { + if ( rmr.minLength != 0 && getTextLength(value) < rmr.minLength ) { std::ostringstream oss; - oss << name << " must be at least " << rmr.minLength << " bytes"; + oss << name << " must contain at least " << rmr.minLength << " characters"; errors.push_back(oss.str()); } - if ( rmr.maxLength != 0 && value.size() > rmr.maxLength ) { + if ( rmr.maxLength != 0 && getTextLength(value) > rmr.maxLength ) { std::ostringstream oss; - oss << name << " must be at most " << rmr.maxLength << " bytes"; + oss << name << " must contain at most " << rmr.maxLength << " characters"; errors.push_back(oss.str()); } if ( !rmr.regex.empty() && !matchRegex(rmr.regex, value) ) { diff --git a/src/metadata_constraints.cpp b/src/metadata_constraints.cpp index bee79ab7..8604dc08 100644 --- a/src/metadata_constraints.cpp +++ b/src/metadata_constraints.cpp @@ -19,11 +19,8 @@ const Metadata::ReservedMetadataTable reservedMetadataInfoTable = { { "Illustration_48x48@1", MANDATORY, - 67, // this is the lower limit on a syntactically valid PNG file - // (according to https://github.com/mathiasbynens/small) - 10000, // this is roughly the size of the raw (i.e. without any compression) - // RGBA pixel data of a 48x48 image - // Question: how much PNG metadata shall we allow? + 0, // There are no constraints on the illustration metadata size + 0, // in order to avoid decoding it as UTF-8 encoded text PNG_REGEXP }, }; diff --git a/test/metadata-test.cpp b/test/metadata-test.cpp index 67dba591..078d9a8c 100644 --- a/test/metadata-test.cpp +++ b/test/metadata-test.cpp @@ -82,7 +82,7 @@ TEST(Metadata, minSizeConstraints) ASSERT_FALSE(m.valid()); ASSERT_EQ(m.check(), zim::Metadata::Errors({ - "Title must be at least 1 bytes" + "Title must contain at least 1 characters" }) ); m.set("Title", "t"); @@ -96,7 +96,7 @@ TEST(Metadata, maxSizeConstraints) ASSERT_FALSE(m.valid()); ASSERT_EQ(m.check(), zim::Metadata::Errors({ - "Title must be at most 30 bytes" + "Title must contain at most 30 characters" }) ); m.set("Title", std::string(30, 'a')); From 63d79e4da78eaa6b0d677d4f5e34a1dccd899ef9 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Tue, 28 Mar 2023 19:15:01 +0400 Subject: [PATCH 03/16] Added support for complex metadata checks This commit is intended to demonstrate how complex checks (involving more than one metadata entries) can be added if required. --- src/metadata.cpp | 87 +++++++++++++++++++++++++++++++++++- src/metadata.h | 1 + src/metadata_constraints.cpp | 13 ++++++ test/metadata-test.cpp | 13 ++++++ 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/metadata.cpp b/src/metadata.cpp index cb232846..af803f30 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -47,6 +47,75 @@ size_t getTextLength(const std::string& utf8EncodedString) return icu::UnicodeString::fromUTF8(utf8EncodedString).length(); } +bool checkTextLanguage(const std::string& text, const std::string& langCode) +{ + // TODO: check that text is in langCode's script + return true; +} + +class MetadataComplexCheckBase +{ +public: + const std::string description; + const MetadataComplexCheckBase* const prev; + +public: // functions + explicit MetadataComplexCheckBase(const std::string& desc); + + MetadataComplexCheckBase(const MetadataComplexCheckBase&) = delete; + MetadataComplexCheckBase(MetadataComplexCheckBase&&) = delete; + void operator=(const MetadataComplexCheckBase&) = delete; + void operator=(MetadataComplexCheckBase&&) = delete; + + virtual ~MetadataComplexCheckBase(); + + virtual bool checkMetadata(const Metadata& m) const = 0; + + static const MetadataComplexCheckBase* getLastCheck() { return last; } + +private: // functions + static const MetadataComplexCheckBase* last; +}; + +const MetadataComplexCheckBase* MetadataComplexCheckBase::last = nullptr; + +MetadataComplexCheckBase::MetadataComplexCheckBase(const std::string& desc) + : description(desc) + , prev(last) +{ + last = this; +} + +MetadataComplexCheckBase::~MetadataComplexCheckBase() +{ + // Ideally, we should de-register this object from the list of live objects. + // However, in the current implementation MetadataComplexCheckBase objects + // are only constructed in static storage and the list of active objects + // isn't supposed to be accessed after any MetadataComplexCheckBase object + // has been destroyed as part of program termination clean-up actions. +} + +#define ADD_METADATA_COMPLEX_CHECK(DESC, CLSNAME) \ +class CLSNAME : public MetadataComplexCheckBase \ +{ \ +public: \ + CLSNAME() : MetadataComplexCheckBase(DESC) {} \ + bool checkMetadata(const Metadata& data) const override; \ +}; \ + \ +const CLSNAME CONCAT(obj, CLSNAME); \ + \ +bool CLSNAME::checkMetadata(const Metadata& data) const \ +/* should be followed by the check body */ + + + +#define CONCAT(X, Y) X##Y +#define GENCLSNAME(UUID) CONCAT(MetadataComplexCheck, UUID) + +#define METADATA_ASSERT(DESC) ADD_METADATA_COMPLEX_CHECK(DESC, GENCLSNAME(__LINE__)) + + #include "metadata_constraints.cpp" } // unnamed namespace @@ -124,13 +193,29 @@ Metadata::Errors Metadata::checkSimpleConstraints() const return errors; } +Metadata::Errors Metadata::checkComplexConstraints() const +{ + Errors errors; + const MetadataComplexCheckBase* c = MetadataComplexCheckBase::getLastCheck(); + for ( ; c != nullptr ; c = c->prev ) { + if ( ! c->checkMetadata(*this) ) { + errors.push_back(c->description); + } + } + return errors; +} + Metadata::Errors Metadata::check() const { Errors e = checkMandatoryMetadata(); if ( !e.empty() ) return e; - return checkSimpleConstraints(); + e = checkSimpleConstraints(); + if ( !e.empty() ) + return e; + + return checkComplexConstraints(); } } // namespace zim diff --git a/src/metadata.h b/src/metadata.h index ec14cc62..e782a486 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -59,6 +59,7 @@ class Metadata private: // functions Errors checkMandatoryMetadata() const; Errors checkSimpleConstraints() const; + Errors checkComplexConstraints() const; private: // data std::map<std::string, std::string> data; diff --git a/src/metadata_constraints.cpp b/src/metadata_constraints.cpp index 8604dc08..123c5eca 100644 --- a/src/metadata_constraints.cpp +++ b/src/metadata_constraints.cpp @@ -24,3 +24,16 @@ const Metadata::ReservedMetadataTable reservedMetadataInfoTable = { PNG_REGEXP }, }; + +METADATA_ASSERT("LongDescription shouldn't be shorter than Description") +{ + return !data.has("LongDescription") + || data["LongDescription"].size() >= data["Description"].size(); +} + +METADATA_ASSERT("Description must be in the language of the ZIM file") +{ + const auto lang = data["Language"]; + const auto description = data["Description"]; + return checkTextLanguage(description, lang); +} diff --git a/test/metadata-test.cpp b/test/metadata-test.cpp index 078d9a8c..a46eef4f 100644 --- a/test/metadata-test.cpp +++ b/test/metadata-test.cpp @@ -128,3 +128,16 @@ TEST(Metadata, regexpConstraints) m.set("Language", "fre,nch"); ASSERT_TRUE(m.valid()); } + +TEST(Metadata, complexConstraints) +{ + zim::Metadata m = makeValidMetadata(); + m.set("Description", "Short description"); + m.set("LongDescription", "Long description"); + ASSERT_FALSE(m.valid()); + ASSERT_EQ(m.check(), + zim::Metadata::Errors({ + "LongDescription shouldn't be shorter than Description" + }) + ); +} From 7276c23b996388c16320630148f9f9c3afd49fe1 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Wed, 19 Apr 2023 18:15:41 +0400 Subject: [PATCH 04/16] Removed an unimplemented demo check --- src/metadata.cpp | 6 ------ src/metadata_constraints.cpp | 7 ------- 2 files changed, 13 deletions(-) diff --git a/src/metadata.cpp b/src/metadata.cpp index af803f30..3a6f14e6 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -47,12 +47,6 @@ size_t getTextLength(const std::string& utf8EncodedString) return icu::UnicodeString::fromUTF8(utf8EncodedString).length(); } -bool checkTextLanguage(const std::string& text, const std::string& langCode) -{ - // TODO: check that text is in langCode's script - return true; -} - class MetadataComplexCheckBase { public: diff --git a/src/metadata_constraints.cpp b/src/metadata_constraints.cpp index 123c5eca..ddfef847 100644 --- a/src/metadata_constraints.cpp +++ b/src/metadata_constraints.cpp @@ -30,10 +30,3 @@ METADATA_ASSERT("LongDescription shouldn't be shorter than Description") return !data.has("LongDescription") || data["LongDescription"].size() >= data["Description"].size(); } - -METADATA_ASSERT("Description must be in the language of the ZIM file") -{ - const auto lang = data["Language"]; - const auto description = data["Description"]; - return checkTextLanguage(description, lang); -} From 6705eb7e1fc035d27e5dc2c9701506e1c7ee0609 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Wed, 19 Apr 2023 19:13:28 +0400 Subject: [PATCH 05/16] zimwriterfs sets LongDescription metadata When -L|--longDesciption option was added to zimwriterfs its argument remained unused (participating in constraint checks doesn't count). --- src/zimwriterfs/zimwriterfs.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/zimwriterfs/zimwriterfs.cpp b/src/zimwriterfs/zimwriterfs.cpp index f5ea6948..98e42912 100644 --- a/src/zimwriterfs/zimwriterfs.cpp +++ b/src/zimwriterfs/zimwriterfs.cpp @@ -427,6 +427,7 @@ void create_zim() zimCreator.addMetadata("Creator", creator); zimCreator.addMetadata("Title", title); zimCreator.addMetadata("Description", description); + zimCreator.addMetadata("LongDescription", longDescription); zimCreator.addMetadata("Name", name); zimCreator.addMetadata("Source", source); zimCreator.addMetadata("Flavour", flavour); From 25a33ed26791aad137753bd3a450e90c3fd4042e Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Wed, 19 Apr 2023 19:19:34 +0400 Subject: [PATCH 06/16] Fixed "zimwriterfs -L" -L option of zimwriterfs was not properly registered in the call to `getopt_long()` --- src/zimwriterfs/zimwriterfs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zimwriterfs/zimwriterfs.cpp b/src/zimwriterfs/zimwriterfs.cpp index 98e42912..113a9182 100644 --- a/src/zimwriterfs/zimwriterfs.cpp +++ b/src/zimwriterfs/zimwriterfs.cpp @@ -246,7 +246,7 @@ void parse_args(int argc, char** argv) do { c = getopt_long( - argc, argv, "hVvijxuw:I:t:d:c:l:p:r:e:n:m:J:UB", long_options, &option_index); + argc, argv, "hVvijxuw:I:t:d:c:l:p:r:e:n:m:J:UBL:", long_options, &option_index); if (c != -1) { switch (c) { From 144143917a715fa3e700de5810bb6aaa5eba6e95 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Wed, 19 Apr 2023 19:32:00 +0400 Subject: [PATCH 07/16] Using zim::Metadata in zimwriterfs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For some reason the PNG regex check fails on real data. But at least it uncovered the issue with the error message which reads as bellow: ``` Illustration_48x48@1 doesn't match regex: ^�PNG � .+ ``` --- src/metadata.h | 9 +++- src/metadata_constraints.cpp | 16 +++--- src/zimwriterfs/meson.build | 1 + src/zimwriterfs/zimwriterfs.cpp | 88 ++++++++++++++++++--------------- 4 files changed, 66 insertions(+), 48 deletions(-) diff --git a/src/metadata.h b/src/metadata.h index e782a486..f57655d8 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -29,6 +29,8 @@ namespace zim class Metadata { + typedef std::map<std::string, std::string> KeyValueMap; + public: // types struct ReservedMetadataRecord { @@ -43,6 +45,8 @@ class Metadata typedef std::vector<std::string> Errors; + typedef KeyValueMap::const_iterator Iterator; + public: // data static const ReservedMetadataTable& reservedMetadataInfo; @@ -56,13 +60,16 @@ class Metadata static const ReservedMetadataRecord& getReservedMetadataRecord(const std::string& name); + Iterator begin() const { return data.begin(); } + Iterator end() const { return data.end(); } + private: // functions Errors checkMandatoryMetadata() const; Errors checkSimpleConstraints() const; Errors checkComplexConstraints() const; private: // data - std::map<std::string, std::string> data; + KeyValueMap data; }; } // namespace zim diff --git a/src/metadata_constraints.cpp b/src/metadata_constraints.cpp index ddfef847..48c8e1c4 100644 --- a/src/metadata_constraints.cpp +++ b/src/metadata_constraints.cpp @@ -7,14 +7,14 @@ const Metadata::ReservedMetadataTable reservedMetadataInfoTable = { { "Publisher", MANDATORY, 1, 0, "" }, { "Date", MANDATORY, 10, 10, DATE_REGEXP }, { "Description", MANDATORY, 1, 80, "" }, - { "LongDescription", OPTIONAL, 1, 4000, "" }, - { "License", OPTIONAL, 1, 0, "" }, - { "Tags", OPTIONAL, 1, 0, "" }, - { "Relation", OPTIONAL, 1, 0, "" }, - { "Flavour", OPTIONAL, 1, 0, "" }, - { "Source", OPTIONAL, 1, 0, "" }, - { "Counter", OPTIONAL, 1, 0, "" }, - { "Scraper", OPTIONAL, 1, 0, "" }, + { "LongDescription", OPTIONAL, 0, 4000, "" }, + { "License", OPTIONAL, 0, 0, "" }, + { "Tags", OPTIONAL, 0, 0, "" }, + { "Relation", OPTIONAL, 0, 0, "" }, + { "Flavour", OPTIONAL, 0, 0, "" }, + { "Source", OPTIONAL, 0, 0, "" }, + { "Counter", OPTIONAL, 0, 0, "" }, + { "Scraper", OPTIONAL, 0, 0, "" }, { "Illustration_48x48@1", diff --git a/src/zimwriterfs/meson.build b/src/zimwriterfs/meson.build index 6f746884..a4f44012 100644 --- a/src/zimwriterfs/meson.build +++ b/src/zimwriterfs/meson.build @@ -3,6 +3,7 @@ sources = [ 'zimwriterfs.cpp', 'tools.cpp', '../tools.cpp', + '../metadata.cpp', 'zimcreatorfs.cpp' ] diff --git a/src/zimwriterfs/zimwriterfs.cpp b/src/zimwriterfs/zimwriterfs.cpp index 113a9182..ed6cba6d 100644 --- a/src/zimwriterfs/zimwriterfs.cpp +++ b/src/zimwriterfs/zimwriterfs.cpp @@ -33,6 +33,7 @@ #include <queue> #include "zimcreatorfs.h" +#include "../metadata.h" #include "../tools.h" #include "../version.h" #include "tools.h" @@ -81,30 +82,52 @@ bool thereAreMissingArguments() || illustration.empty(); } -bool checkDescriptionLengths() { - if (description.empty()) { - std::cerr << "Description metadata should not be empty." << std::endl; - return false; +zim::Metadata makeMetadata() { + zim::Metadata metadata; + + metadata.set("Language", language); + metadata.set("Publisher", publisher); + metadata.set("Creator", creator); + metadata.set("Title", title); + metadata.set("Description", description); + metadata.set("LongDescription", longDescription); + metadata.set("Name", name); + metadata.set("Source", source); + metadata.set("Flavour", flavour); + metadata.set("Scraper", scraper); + metadata.set("Tags", tags); + metadata.set("Date", generateDate()); + if ( !illustration.empty() ) { + const auto data = getFileContent(directoryPath + "/" + illustration); + metadata.set("Illustration_48x48@1", data); } - if (!longDescription.empty() && longDescription.length() < description.length()) { - std::cerr << "Long description should not be shorter than the short description." << std::endl; - return false; - } + return metadata; +} - if (description.length() > 80) { - std::cerr << "Description length exceeds the 80 character limit." << std::endl; - return false; - } - if (!longDescription.empty() && longDescription.length() > 4000) { - std::cerr << "Long description length exceeds the 4000 character limit." << std::endl; - return false; +bool checkMetadata(const zim::Metadata& metadata) +{ + const auto errors = metadata.check(); + + if ( !errors.empty() ) { + std::cerr << "Metadata doesn't meet the following requirements:\n"; + for ( const auto& err : errors ) { + std::cerr << " " << err << std::endl; + } } - return true; + return errors.empty(); +} + +void addMetadata(ZimCreatorFS& zimCreator, const zim::Metadata& metadata) +{ + for ( const auto& kv : metadata ) { + zimCreator.addMetadata(kv.first, kv.second); + } } + } // Global flags @@ -328,11 +351,6 @@ void parse_args(int argc, char** argv) } } while (c != -1); - if ( !checkDescriptionLengths() ) { - exit(1); - } - - while (optind < argc) { if (directoryPath.empty()) { directoryPath = argv[optind++]; @@ -390,7 +408,7 @@ void parse_args(int argc, char** argv) } } -void create_zim() +void create_zim(const zim::Metadata& metadata) { ZimCreatorFS zimCreator(directoryPath); zimCreator.configVerbose(isVerbose()) @@ -422,27 +440,12 @@ void create_zim() zimCreator.startZimCreation(zimPath); - zimCreator.addMetadata("Language", language); - zimCreator.addMetadata("Publisher", publisher); - zimCreator.addMetadata("Creator", creator); - zimCreator.addMetadata("Title", title); - zimCreator.addMetadata("Description", description); - zimCreator.addMetadata("LongDescription", longDescription); - zimCreator.addMetadata("Name", name); - zimCreator.addMetadata("Source", source); - zimCreator.addMetadata("Flavour", flavour); - zimCreator.addMetadata("Scraper", scraper); - zimCreator.addMetadata("Tags", tags); - zimCreator.addMetadata("Date", generateDate()); + addMetadata(zimCreator, metadata); if ( !welcome.empty() ) { zimCreator.setMainPath(welcome); } - if ( !illustration.empty() ) { - zimCreator.addIllustration(48, getFileContent(directoryPath + "/" + illustration)); - } - /* Directory visitor */ zimCreator.visitDirectory(directoryPath); @@ -475,7 +478,14 @@ int main(int argc, char** argv) try { parse_args(argc, argv); - create_zim(); + + const zim::Metadata metadata = makeMetadata(); + + if ( !checkMetadata(metadata) ) { + exit(1); + } + + create_zim(metadata); } catch(std::exception &e) { std::cerr << "zimwriterfs: " << e.what() << std::endl; From d296cae135f5f71c0e65a7dd2243207105b982e9 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Thu, 20 Apr 2023 16:43:11 +0400 Subject: [PATCH 08/16] Testing of PNG regex failure --- test/metadata-test.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/metadata-test.cpp b/test/metadata-test.cpp index a46eef4f..ee760ea1 100644 --- a/test/metadata-test.cpp +++ b/test/metadata-test.cpp @@ -127,6 +127,13 @@ TEST(Metadata, regexpConstraints) m.set("Language", "fre,nch"); ASSERT_TRUE(m.valid()); + + m.set("Illustration_48x48@1", "zimdata/favicon.png"); + ASSERT_EQ(m.check(), + zim::Metadata::Errors({ + "Illustration_48x48@1 doesn't match regex: ^\x89PNG\x0d\x0a\x1a\x0a.+" + }) + ); } TEST(Metadata, complexConstraints) From 5cc17ce58d43b7e709c06e90c7988029803e07c7 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Thu, 20 Apr 2023 17:12:52 +0400 Subject: [PATCH 09/16] Fixed non-printable chars in error messages --- src/metadata.cpp | 26 +++++++++++++++++++++++++- test/metadata-test.cpp | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/metadata.cpp b/src/metadata.cpp index 3a6f14e6..af3c5ee1 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -23,6 +23,10 @@ #include <regex> #include <unicode/unistr.h> +#include <cctype> +#include <iomanip> + + namespace zim { @@ -112,6 +116,25 @@ bool CLSNAME::checkMetadata(const Metadata& data) const \ #include "metadata_constraints.cpp" +// This function is intended for pretty printing of regexps with non-printable +// characters. +// In a general purpose/rigorous version we should escape the escape symbol +// (backslash) too, but that doesn't play well with the purpose stated above. +std::string escapeNonPrintableChars(const std::string& s) +{ + std::ostringstream os; + os << std::hex; + for (const char c : s) { + if (std::isprint(c)) { + os << c; + } else { + const unsigned int charVal = static_cast<unsigned char>(c); + os << "\\x" << std::setw(2) << std::setfill('0') << charVal; + } + } + return os.str(); +} + } // unnamed namespace const Metadata::ReservedMetadataTable& Metadata::reservedMetadataInfo = reservedMetadataInfoTable; @@ -178,7 +201,8 @@ Metadata::Errors Metadata::checkSimpleConstraints() const errors.push_back(oss.str()); } if ( !rmr.regex.empty() && !matchRegex(rmr.regex, value) ) { - errors.push_back(name + " doesn't match regex: " + rmr.regex); + const std::string regex = escapeNonPrintableChars(rmr.regex); + errors.push_back(name + " doesn't match regex: " + regex); } } catch ( const std::out_of_range& ) { // ignore non-reserved metadata diff --git a/test/metadata-test.cpp b/test/metadata-test.cpp index ee760ea1..7ab4bc52 100644 --- a/test/metadata-test.cpp +++ b/test/metadata-test.cpp @@ -131,7 +131,7 @@ TEST(Metadata, regexpConstraints) m.set("Illustration_48x48@1", "zimdata/favicon.png"); ASSERT_EQ(m.check(), zim::Metadata::Errors({ - "Illustration_48x48@1 doesn't match regex: ^\x89PNG\x0d\x0a\x1a\x0a.+" + "Illustration_48x48@1 doesn't match regex: ^\\x89PNG\\x0d\\x0a\\x1a\\x0a.+" }) ); } From 012ec004da80ec007826c08a00a40bca0d518c3a Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Thu, 20 Apr 2023 17:50:51 +0400 Subject: [PATCH 10/16] Fixed PNG regexp Regexp is not the best tool for use with binary data where NUL characters may occur. --- src/metadata.cpp | 6 +++++- test/metadata-test.cpp | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/metadata.cpp b/src/metadata.cpp index af3c5ee1..617a133f 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -38,7 +38,11 @@ const bool OPTIONAL = false; const std::string LANGS_REGEXP = "\\w{3}(,\\w{3})*"; const std::string DATE_REGEXP = R"(\d\d\d\d-\d\d-\d\d)"; -const std::string PNG_REGEXP = "^\x89\x50\x4e\x47\x0d\x0a\x1a\x0a.+"; + +// PNG regexp has to be defined in such a tricky way because it includes +// a NUL character +const char PNG_REGEXP_DATA[] = "^\x89\x50\x4e\x47\x0d\x0a\x1a\x0a(.|\\s|\0)+"; +const std::string PNG_REGEXP(PNG_REGEXP_DATA, sizeof(PNG_REGEXP_DATA)-1); bool matchRegex(const std::string& regexStr, const std::string& text) { diff --git a/test/metadata-test.cpp b/test/metadata-test.cpp index 7ab4bc52..903e0a7c 100644 --- a/test/metadata-test.cpp +++ b/test/metadata-test.cpp @@ -131,11 +131,30 @@ TEST(Metadata, regexpConstraints) m.set("Illustration_48x48@1", "zimdata/favicon.png"); ASSERT_EQ(m.check(), zim::Metadata::Errors({ - "Illustration_48x48@1 doesn't match regex: ^\\x89PNG\\x0d\\x0a\\x1a\\x0a.+" + "Illustration_48x48@1 doesn't match regex: ^\\x89PNG\\x0d\\x0a\\x1a\\x0a(.|\\s|\\x00)+" }) ); } +TEST(Metadata, pngRegexp) +{ + const std::string PNG_HEADER = "\x89PNG\r\n\x1a\n"; + zim::Metadata m = makeValidMetadata(); + { + m.set("Illustration_48x48@1", PNG_HEADER + 'A'); + ASSERT_TRUE(m.valid()); + } + { + m.set("Illustration_48x48@1", PNG_HEADER + '\n'); + ASSERT_TRUE(m.valid()); + } + { + m.set("Illustration_48x48@1", PNG_HEADER + '\0'); + ASSERT_TRUE(m.valid()); + } +} + + TEST(Metadata, complexConstraints) { zim::Metadata m = makeValidMetadata(); From da72df70879ac2433c7c6a1b1bf45dd208d62120 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Thu, 20 Apr 2023 18:17:16 +0400 Subject: [PATCH 11/16] Preparing zimcheck for switching to zim::Metadata --- src/zimcheck/checks.cpp | 7 ++++--- src/zimcheck/checks.h | 2 +- test/zimcheck-test.cpp | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/zimcheck/checks.cpp b/src/zimcheck/checks.cpp index ab48330c..acea0f38 100644 --- a/src/zimcheck/checks.cpp +++ b/src/zimcheck/checks.cpp @@ -49,7 +49,7 @@ std::unordered_map<TestType, std::pair<LogTag, std::string>> errormapping = { { TestType::CHECKSUM, {LogTag::ERROR, "Invalid checksum"}}, { TestType::INTEGRITY, {LogTag::ERROR, "Invalid low-level structure"}}, { TestType::EMPTY, {LogTag::ERROR, "Empty articles"}}, - { TestType::METADATA, {LogTag::ERROR, "Missing metadata entries"}}, + { TestType::METADATA, {LogTag::ERROR, "Metadata errors"}}, { TestType::FAVICON, {LogTag::ERROR, "Favicon"}}, { TestType::MAIN_PAGE, {LogTag::ERROR, "Missing mainpage"}}, { TestType::REDUNDANT, {LogTag::WARNING, "Redundant data found"}}, @@ -73,7 +73,7 @@ std::unordered_map<MsgId, MsgInfo> msgTable = { { MsgId::DANGLING_LINKS, { TestType::URL_INTERNAL, "The following links:\n{{#links}}- {{&value}}\n{{/links}}({{&normalized_link}}) were not found in article {{&path}}" } }, { MsgId::EXTERNAL_LINK, { TestType::URL_EXTERNAL, "{{&link}} is an external dependence in article {{&path}}" } }, { MsgId::REDUNDANT_ITEMS, { TestType::REDUNDANT, "{{&path1}} and {{&path2}}" } }, - { MsgId::MISSING_METADATA, { TestType::METADATA, "{{&metadata_type}}" } }, + { MsgId::METADATA, { TestType::METADATA, "{{&error}}" } }, { MsgId::REDIRECT_LOOP, { TestType::REDIRECT, "Redirect loop exists from entry {{&entry_path}}\n" } }, { MsgId::MISSING_FAVICON, { TestType::FAVICON, "Favicon is missing" } } }; @@ -279,7 +279,8 @@ void test_metadata(const zim::Archive& archive, ErrorLogger& reporter) { auto end = existing_metadata.end(); for (auto &meta : test_meta) { if (std::find(begin, end, meta) == end) { - reporter.addMsg(MsgId::MISSING_METADATA, {{"metadata_type", meta}}); + const std::string error = std::string("Missing ") + meta; + reporter.addMsg(MsgId::METADATA, {{"error", error}}); } } } diff --git a/src/zimcheck/checks.h b/src/zimcheck/checks.h index 14ef33e9..f5942fed 100644 --- a/src/zimcheck/checks.h +++ b/src/zimcheck/checks.h @@ -52,7 +52,7 @@ enum class MsgId { CHECKSUM, MAIN_PAGE, - MISSING_METADATA, + METADATA, EMPTY_ENTRY, OUTOFBOUNDS_LINK, EMPTY_LINKS, diff --git a/test/zimcheck-test.cpp b/test/zimcheck-test.cpp index 1005655c..3ba90c3b 100644 --- a/test/zimcheck-test.cpp +++ b/test/zimcheck-test.cpp @@ -528,9 +528,9 @@ TEST(zimcheck, metadata_poorzimfile) "[INFO] Checking zim file data/zimfiles/poor.zim" "\n" "[INFO] Zimcheck version is " VERSION "\n" "[INFO] Searching for metadata entries..." "\n" - "[ERROR] Missing metadata entries:" "\n" - " Title" "\n" - " Description" "\n" + "[ERROR] Metadata errors:" "\n" + " Missing Title" "\n" + " Missing Description" "\n" "[INFO] Overall Test Status: Fail" "\n" "[INFO] Total time taken by zimcheck: <3 seconds." "\n" ); @@ -718,9 +718,9 @@ const std::string ALL_CHECKS_OUTPUT_ON_POORZIMFILE( "[INFO] Checking for redirect loops..." "\n" "[ERROR] Empty articles:" "\n" " Entry empty.html is empty" "\n" - "[ERROR] Missing metadata entries:" "\n" - " Title" "\n" - " Description" "\n" + "[ERROR] Metadata errors:" "\n" + " Missing Title" "\n" + " Missing Description" "\n" "[ERROR] Favicon:" "\n" " Favicon is missing" "\n" "[ERROR] Missing mainpage:" "\n" @@ -832,14 +832,14 @@ TEST(zimcheck, json_poorzimfile) " {" "\n" " \"check\" : \"metadata\"," "\n" " \"level\" : \"ERROR\"," "\n" - " \"message\" : \"Title\"," "\n" - " \"metadata_type\" : \"Title\"" "\n" + " \"message\" : \"Missing Title\"," "\n" + " \"error\" : \"Missing Title\"" "\n" " }," "\n" " {" "\n" " \"check\" : \"metadata\"," "\n" " \"level\" : \"ERROR\"," "\n" - " \"message\" : \"Description\"," "\n" - " \"metadata_type\" : \"Description\"" "\n" + " \"message\" : \"Missing Description\"," "\n" + " \"error\" : \"Missing Description\"" "\n" " }," "\n" " {" "\n" " \"check\" : \"favicon\"," "\n" From 28b17bb26934e3077f8673784fe0b007c29ebf8d Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Thu, 20 Apr 2023 18:44:39 +0400 Subject: [PATCH 12/16] Used zim::Metadata in zimcheck Note that metadata constraints were relaxed in order to let the zimcheck unittests pass without updating/regenerating the unittest ZIM data. --- src/metadata.cpp | 2 +- src/metadata_constraints.cpp | 6 +++--- src/zimcheck/checks.cpp | 24 ++++++++---------------- src/zimcheck/meson.build | 1 + test/meson.build | 2 +- test/metadata-test.cpp | 5 +---- test/zimcheck-test.cpp | 30 +++++++++++++++--------------- 7 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/metadata.cpp b/src/metadata.cpp index 617a133f..ed7ca02e 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -36,7 +36,7 @@ namespace const bool MANDATORY = true; const bool OPTIONAL = false; -const std::string LANGS_REGEXP = "\\w{3}(,\\w{3})*"; +const std::string LANGS_REGEXP = "\\w{2,3}(,\\w{2,3})*"; const std::string DATE_REGEXP = R"(\d\d\d\d-\d\d-\d\d)"; // PNG regexp has to be defined in such a tricky way because it includes diff --git a/src/metadata_constraints.cpp b/src/metadata_constraints.cpp index 48c8e1c4..d13eb88a 100644 --- a/src/metadata_constraints.cpp +++ b/src/metadata_constraints.cpp @@ -1,8 +1,8 @@ const Metadata::ReservedMetadataTable reservedMetadataInfoTable = { // name isMandatory minLength maxLength regex - { "Name", MANDATORY, 1, 0, "" }, + { "Name", OPTIONAL, 1, 0, "" }, { "Title", MANDATORY, 1, 30, "" }, - { "Language", MANDATORY, 3, 0, LANGS_REGEXP }, + { "Language", MANDATORY, 2, 0, LANGS_REGEXP }, { "Creator", MANDATORY, 1, 0, "" }, { "Publisher", MANDATORY, 1, 0, "" }, { "Date", MANDATORY, 10, 10, DATE_REGEXP }, @@ -18,7 +18,7 @@ const Metadata::ReservedMetadataTable reservedMetadataInfoTable = { { "Illustration_48x48@1", - MANDATORY, + OPTIONAL, 0, // There are no constraints on the illustration metadata size 0, // in order to avoid decoding it as UTF-8 encoded text PNG_REGEXP diff --git a/src/zimcheck/checks.cpp b/src/zimcheck/checks.cpp index acea0f38..d2abf2bf 100644 --- a/src/zimcheck/checks.cpp +++ b/src/zimcheck/checks.cpp @@ -2,6 +2,7 @@ #include "checks.h" #include "../tools.h" #include "../concurrent_cache.h" +#include "../metadata.h" #include <cassert> #include <map> @@ -266,22 +267,13 @@ void test_integrity(const std::string& filename, ErrorLogger& reporter) { void test_metadata(const zim::Archive& archive, ErrorLogger& reporter) { - reporter.infoMsg("[INFO] Searching for metadata entries..."); - static const char* const test_meta[] = { - "Title", - "Creator", - "Publisher", - "Date", - "Description", - "Language"}; - auto existing_metadata = archive.getMetadataKeys(); - auto begin = existing_metadata.begin(); - auto end = existing_metadata.end(); - for (auto &meta : test_meta) { - if (std::find(begin, end, meta) == end) { - const std::string error = std::string("Missing ") + meta; - reporter.addMsg(MsgId::METADATA, {{"error", error}}); - } + reporter.infoMsg("[INFO] Checking metadata..."); + zim::Metadata metadata; + for ( const auto& key : archive.getMetadataKeys() ) { + metadata.set(key, archive.getMetadata(key)); + } + for (const auto &error : metadata.check()) { + reporter.addMsg(MsgId::METADATA, {{"error", error}}); } } diff --git a/src/zimcheck/meson.build b/src/zimcheck/meson.build index c08af030..08c6789e 100644 --- a/src/zimcheck/meson.build +++ b/src/zimcheck/meson.build @@ -21,6 +21,7 @@ executable('zimcheck', 'checks.cpp', 'json_tools.cpp', '../tools.cpp', + '../metadata.cpp', include_directories : inc, dependencies: [libzim_dep, thread_dep], install: true) diff --git a/test/meson.build b/test/meson.build index 26530901..07eeb99f 100644 --- a/test/meson.build +++ b/test/meson.build @@ -11,7 +11,7 @@ zimwriter_srcs = [ '../src/zimwriterfs/tools.cpp', '../src/zimwriterfs/zimcreatorfs.cpp', '../src/tools.cpp'] -tests_src_map = { 'zimcheck-test' : ['../src/zimcheck/zimcheck.cpp', '../src/zimcheck/checks.cpp', '../src/zimcheck/json_tools.cpp', '../src/tools.cpp'], +tests_src_map = { 'zimcheck-test' : ['../src/zimcheck/zimcheck.cpp', '../src/zimcheck/checks.cpp', '../src/zimcheck/json_tools.cpp', '../src/tools.cpp', '../src/metadata.cpp'], 'tools-test' : zimwriter_srcs, 'metadata-test' : ['../src/metadata.cpp'], 'zimwriterfs-zimcreatorfs' : zimwriter_srcs } diff --git a/test/metadata-test.cpp b/test/metadata-test.cpp index 903e0a7c..e2315a29 100644 --- a/test/metadata-test.cpp +++ b/test/metadata-test.cpp @@ -17,14 +17,12 @@ TEST(Metadata, detectsAbsenceOfMandatoryEntries) ASSERT_FALSE(m.valid()); ASSERT_EQ(m.check(), zim::Metadata::Errors({ - "Missing mandatory metadata: Name", "Missing mandatory metadata: Title", "Missing mandatory metadata: Language", "Missing mandatory metadata: Creator", "Missing mandatory metadata: Publisher", "Missing mandatory metadata: Date", "Missing mandatory metadata: Description", - "Missing mandatory metadata: Illustration_48x48@1", }) ); @@ -39,7 +37,6 @@ TEST(Metadata, detectsAbsenceOfMandatoryEntries) "Missing mandatory metadata: Title", "Missing mandatory metadata: Language", "Missing mandatory metadata: Publisher", - "Missing mandatory metadata: Illustration_48x48@1", }) ); @@ -121,7 +118,7 @@ TEST(Metadata, regexpConstraints) ASSERT_FALSE(m.valid()); ASSERT_EQ(m.check(), zim::Metadata::Errors({ - "Language doesn't match regex: \\w{3}(,\\w{3})*" + "Language doesn't match regex: \\w{2,3}(,\\w{2,3})*" }) ); diff --git a/test/zimcheck-test.cpp b/test/zimcheck-test.cpp index 3ba90c3b..6bb2a355 100644 --- a/test/zimcheck-test.cpp +++ b/test/zimcheck-test.cpp @@ -244,9 +244,9 @@ void test_zimcheck_single_option(std::vector<const char*> optionAliases, CapturedStdout zimcheck_output; CapturedStderr zimcheck_stderr; const CmdLine cmdline{"zimcheck", opt, zimfile}; - ASSERT_EQ(expected_exit_code, zimcheck(cmdline)) << cmdline; - ASSERT_EQ(expected_stderr, std::string(zimcheck_stderr)) << cmdline; - ASSERT_EQ(expected_stdout, std::string(zimcheck_output)) << cmdline; + EXPECT_EQ(expected_exit_code, zimcheck(cmdline)) << cmdline; + EXPECT_EQ(expected_stderr, std::string(zimcheck_stderr)) << cmdline; + EXPECT_EQ(expected_stdout, std::string(zimcheck_output)) << cmdline; } } @@ -293,7 +293,7 @@ TEST(zimcheck, metadata_goodzimfile) const std::string expected_output( "[INFO] Checking zim file data/zimfiles/good.zim" "\n" "[INFO] Zimcheck version is " VERSION "\n" - "[INFO] Searching for metadata entries..." "\n" + "[INFO] Checking metadata..." "\n" "[INFO] Overall Test Status: Pass" "\n" "[INFO] Total time taken by zimcheck: <3 seconds." "\n" ); @@ -413,7 +413,7 @@ const std::string ALL_CHECKS_OUTPUT_ON_GOODZIMFILE( "[INFO] Zimcheck version is " VERSION "\n" "[INFO] Verifying ZIM-archive structure integrity..." "\n" "[INFO] Avoiding redundant checksum test (already performed by the integrity check)." "\n" - "[INFO] Searching for metadata entries..." "\n" + "[INFO] Checking metadata..." "\n" "[INFO] Searching for Favicon..." "\n" "[INFO] Searching for main page..." "\n" "[INFO] Verifying Articles' content..." "\n" @@ -527,10 +527,10 @@ TEST(zimcheck, metadata_poorzimfile) const std::string expected_stdout( "[INFO] Checking zim file data/zimfiles/poor.zim" "\n" "[INFO] Zimcheck version is " VERSION "\n" - "[INFO] Searching for metadata entries..." "\n" + "[INFO] Checking metadata..." "\n" "[ERROR] Metadata errors:" "\n" - " Missing Title" "\n" - " Missing Description" "\n" + " Missing mandatory metadata: Title" "\n" + " Missing mandatory metadata: Description" "\n" "[INFO] Overall Test Status: Fail" "\n" "[INFO] Total time taken by zimcheck: <3 seconds." "\n" ); @@ -709,7 +709,7 @@ const std::string ALL_CHECKS_OUTPUT_ON_POORZIMFILE( "[INFO] Zimcheck version is " VERSION "\n" "[INFO] Verifying ZIM-archive structure integrity..." "\n" "[INFO] Avoiding redundant checksum test (already performed by the integrity check)." "\n" - "[INFO] Searching for metadata entries..." "\n" + "[INFO] Checking metadata..." "\n" "[INFO] Searching for Favicon..." "\n" "[INFO] Searching for main page..." "\n" "[INFO] Verifying Articles' content..." "\n" @@ -719,8 +719,8 @@ const std::string ALL_CHECKS_OUTPUT_ON_POORZIMFILE( "[ERROR] Empty articles:" "\n" " Entry empty.html is empty" "\n" "[ERROR] Metadata errors:" "\n" - " Missing Title" "\n" - " Missing Description" "\n" + " Missing mandatory metadata: Title" "\n" + " Missing mandatory metadata: Description" "\n" "[ERROR] Favicon:" "\n" " Favicon is missing" "\n" "[ERROR] Missing mainpage:" "\n" @@ -832,14 +832,14 @@ TEST(zimcheck, json_poorzimfile) " {" "\n" " \"check\" : \"metadata\"," "\n" " \"level\" : \"ERROR\"," "\n" - " \"message\" : \"Missing Title\"," "\n" - " \"error\" : \"Missing Title\"" "\n" + " \"message\" : \"Missing mandatory metadata: Title\"," "\n" + " \"error\" : \"Missing mandatory metadata: Title\"" "\n" " }," "\n" " {" "\n" " \"check\" : \"metadata\"," "\n" " \"level\" : \"ERROR\"," "\n" - " \"message\" : \"Missing Description\"," "\n" - " \"error\" : \"Missing Description\"" "\n" + " \"message\" : \"Missing mandatory metadata: Description\"," "\n" + " \"error\" : \"Missing mandatory metadata: Description\"" "\n" " }," "\n" " {" "\n" " \"check\" : \"favicon\"," "\n" From e7b0f5a2720cad8270f2754d6a691d4cfe18b40f Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Wed, 26 Apr 2023 11:53:15 +0400 Subject: [PATCH 13/16] Fixed test data and restored metadata constraints Test ZIM files (good.zim, poor.zim and bad_checksum.zim) under test/data/zimfiles were regenerated using zimwriterfs v3.1.3 and the updated script create_test_zimfiles. Metadata constraints that were relaxed in the previous commit have been restored. --- src/metadata.cpp | 2 +- src/metadata_constraints.cpp | 6 +++--- test/data/zimfiles/bad_checksum.zim | Bin 74155 -> 46486 bytes test/data/zimfiles/create_test_zimfiles | 4 +++- test/data/zimfiles/good.zim | Bin 74155 -> 46486 bytes test/data/zimfiles/poor.zim | Bin 69801 -> 58518 bytes test/metadata-test.cpp | 5 ++++- test/zimcheck-test.cpp | 10 +++++++++- 8 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/metadata.cpp b/src/metadata.cpp index ed7ca02e..617a133f 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -36,7 +36,7 @@ namespace const bool MANDATORY = true; const bool OPTIONAL = false; -const std::string LANGS_REGEXP = "\\w{2,3}(,\\w{2,3})*"; +const std::string LANGS_REGEXP = "\\w{3}(,\\w{3})*"; const std::string DATE_REGEXP = R"(\d\d\d\d-\d\d-\d\d)"; // PNG regexp has to be defined in such a tricky way because it includes diff --git a/src/metadata_constraints.cpp b/src/metadata_constraints.cpp index d13eb88a..48c8e1c4 100644 --- a/src/metadata_constraints.cpp +++ b/src/metadata_constraints.cpp @@ -1,8 +1,8 @@ const Metadata::ReservedMetadataTable reservedMetadataInfoTable = { // name isMandatory minLength maxLength regex - { "Name", OPTIONAL, 1, 0, "" }, + { "Name", MANDATORY, 1, 0, "" }, { "Title", MANDATORY, 1, 30, "" }, - { "Language", MANDATORY, 2, 0, LANGS_REGEXP }, + { "Language", MANDATORY, 3, 0, LANGS_REGEXP }, { "Creator", MANDATORY, 1, 0, "" }, { "Publisher", MANDATORY, 1, 0, "" }, { "Date", MANDATORY, 10, 10, DATE_REGEXP }, @@ -18,7 +18,7 @@ const Metadata::ReservedMetadataTable reservedMetadataInfoTable = { { "Illustration_48x48@1", - OPTIONAL, + MANDATORY, 0, // There are no constraints on the illustration metadata size 0, // in order to avoid decoding it as UTF-8 encoded text PNG_REGEXP diff --git a/test/data/zimfiles/bad_checksum.zim b/test/data/zimfiles/bad_checksum.zim index 6f6314ae184c09e7e36400978948cd5bc5efb4f6..d2391e7792d6bbe69716d8e82f651b56cbb92675 100644 GIT binary patch delta 1368 zcmZ2|m}S~)rU?q#&$cjtLG~sHUA7fM2S6wWJ|O)c2->zXO!SuB*jUfJd4iE7%On=| zdQl+;x0ra%2j_B_H5}R||IYn<BZ_J1jG!z3_Q<C1QoghLW`~Z(QQ?Dj$0U+>F!eBo zG59kCFnp`Kysvw~SM8a>N(Lt%-Jh8&*PbfErZT~+%0$Ke>WXcBEizYrSPS-b&XNhb zsrc8BQ)r_r%Z~CFlI3b{%`@Vy!ZV#$*d`cBFx3BUI`lYEC+tYW!2}y7*;TIB+@~3f zY-}%^BNct7WtZrKX$vMDbBUbyHr(sxqvKNkOegLC6kgcHpETjy_208EPOfIL{O2;Q zJ1m&<>qGU|Q=T39{J5V_h3mTOwQ5f`-U({Og$4(iA1$5gu|O_L(EUyH1^XYlWkt{a zzP<m%ZKl=iqWZ@>Sd08`9rw*l`Llmv&#jC_I%_9hE&SxyUA%0?DbC){XTm1`i<%u0 zxZ*fxea_UWNB4hm{>M2*G;-FnL{@R%w#&KS4mYMg&SF*FW5a(u=%vQYV@|zyzRApL zo&RCOs?_o=IiZfvEA`r6w`pt(mB>14^^QqflyP^*gf7b~+HW@9uFsWv#iu<za)shf zr(-vIScKJbW}QuN@w!>a{HaRrS&y)VGvm>Ke;WMZ(xFTYZ)fOq@3`TWVo<X~`lx-1 zg{9fLuLbGOUv&fO_sD#>{lfH>@Dst;|DE1?7EMViI~&dO?X;z=jq7gjNr9&&m1;JB z(LZu;F2{qy-Dhv=Y`pv7EpJ<5?bDh)Gk0H<4lD23&vGmC+?<x%vpFrT)3<ajX9>J7 z!B+Ud*2iIa?rSI2%58$XU&z*Mdo1y~<fVJcgEfo9su-2)$}^0Mt<5%bGD-jwnY0K4 zgPaBsuV7$E1LC|D44d=W*frewc_R`FG86L@+;b9(i&^p<<@0qPS$kQx-Ffr(`0SG( z7#SJFSs2(DnLsI)iGhhlh>ejM#AaY-U=acmJRn(?&3oha$(zaEm%YzmE&o^kjr=|N zbMkBCi{(S*CFH)#y^^~scUo?@+-kYGa{Y4kas_e;a)EL#a^`Xxa#EWg#@&~lEVwUq z^X@u-Mu7xgE`|d+iFxUziRr1SdFhiw=4ouMx%bbW6XXU)kj>MZ<ru9MfGnVkKn`JW z2C+ea6NnRwN-~pkQVpm3%QK2i&aLNSWSTB7&nUx-tdMKEhdiUYCkq3EFIXdx;09vn z{L;LV)FK83RwN0KCC){ui6!}H5*$DYm&B4(pz`TEfHnXvx+>48%E&tXw>+a{BC;7k zHiKVcE=Vbo$speZlqTh57H6Pa$_A7QPA*C;K$l<#N`xe)7lSN~S74Nr201S`F*7d! z7+&B2iU0~g1L~jxqa=#~kT<<Yfl;;I5G2C@mj~%A$t=l9)vo|20tSZN(1PkSlor?w z*1_NcrDLIV8I<mU(kr3#c_{r7O8<w_Qs9EnhJnEtS|WNv=>#ZU2Bqgi=@U?z0TjkS K;KMcXqZj}OTCt-5 literal 74155 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 diff --git a/test/data/zimfiles/create_test_zimfiles b/test/data/zimfiles/create_test_zimfiles index bb1c8152..a4ed30a1 100755 --- a/test/data/zimfiles/create_test_zimfiles +++ b/test/data/zimfiles/create_test_zimfiles @@ -33,7 +33,8 @@ make__good__zim() --no-uuid \ -w main.html \ -I favicon.png \ - -l en \ + -n good_zimfile \ + -l eng \ -t "Test ZIM file" \ -d "N/A" \ -c "N/A" \ @@ -87,6 +88,7 @@ make__poor__zim() --no-uuid \ -w "" \ -I "" \ + -n poor_zimfile \ -l en \ -t "" \ -d "" \ diff --git a/test/data/zimfiles/good.zim b/test/data/zimfiles/good.zim index cd9da5b910c64c365062a8cc71c6703d30d4e03a..99ea0989736329dbd3a1c2f5d96dd493d28e4bf4 100644 GIT binary patch delta 1387 zcmZ2|m}S~)rU?q#&$cjtLG~sHUA7fM2S6wWJ|O)c2->zXO!SuB*jUfJd4iE7%On=| zdQl+;x0ra%2j_B_H5}R||IYn<BZ_J1jG!z3_Q<C1QoghLW`~Z(QQ?Dj$0U+>F!eBo zG59kCFnp`Kysvw~SM8a>N(Lt%-Jh8&*PbfErZT~+%0$Ke>WXcBEizYrSPS-b&XNhb zsrc8BQ)r_r%Z~CFlI3b{%`@Vy!ZV#$*d`cBFx3BUI`lYEC+tYW!2}y7*;TIB+@~3f zY-}%^BNct7WtZrKX$vMDbBUbyHr(sxqvKNkOegLC6kgcHpETjy_208EPOfIL{O2;Q zJ1m&<>qGU|Q=T39{J5V_h3mTOwQ5f`-U({Og$4(iA1$5gu|O_L(EUyH1^XYlWkt{a zzP<m%ZKl=iqWZ@>Sd08`9rw*l`Llmv&#jC_I%_9hE&SxyUA%0?DbC){XTm1`i<%u0 zxZ*fxea_UWNB4hm{>M2*G;-FnL{@R%w#&KS4mYMg&SF*FW5a(u=%vQYV@|zyzRApL zo&RCOs?_o=IiZfvEA`r6w`pt(mB>14^^QqflyP^*gf7b~+HW@9uFsWv#iu<za)shf zr(-vIScKJbW}QuN@w!>a{HaRrS&y)VGvm>Ke;WMZ(xFTYZ)fOq@3`TWVo<X~`lx-1 zg{9fLuLbGOUv&fO_sD#>{lfH>@Dst;|DE1?7EMViI~&dO?X;z=jq7gjNr9&&m1;JB z(LZu;F2{qy-Dhv=Y`pv7EpJ<5?bDh)Gk0H<4lD23&vGmC+?<x%vpFrT)3<ajX9>J7 z!B+Ud*2iIa?rSI2%58$XU&z*Mdo1y~<fVJcgEfo9su-2)$}^0Mt<5%bGD-jwnY0K4 zgPaBsuV7$E1LC|D44d=W*frewc_R`FG86L@+;b9(i&^p<<@0qPS$kQx-Ffr(`0SG( z7#SJFSs2(DnLsI)iGhhlh>ejM#AaY-U=acmJRn(?&3oha$(zaEm%YzmE&o^kjr=|N zbMkBCi{(S*CFH)#y^^~scUo?@+-kYGa{Y4kas_e;a)EL#a^`Xxa#EWg#@&~lEVwUq z^X@u-Mu7xgE`|d+iFxUziRr1SdFhiw=4ouMx%bbW6XXU)kj>MZ<ru9MfGnVkKn`JW z2C+ea6NnRwN-~pkQVpm3%QK2i&aLNSWSTB7&nUx-tdMKEhdiUYCkq3EFIXdx;09vn z{L;LV)FK83RwN0KCC){ui6!}H5*$DYm&B4(pz`TEfHnXvx+>48%E&tXw>+a{BC;7k zHiKVcE=Vbo$speZlqTh57H6Pa$_A7QPA*C;K$l<#N`xe)7lSN~S74Nr201S`F*7d! z7+&B2iU0~g1L~jxqa=#~kT<<Yfl;;I5G2C@mj~%A$t=l9)vo|20tSZN(1PkSlor?w z*1_NcrDLIV8I<mU(kr3#c_{r7O8<w_Qs9Cxh=IWvS|WNv=>#ZU2Bqgi=@U?z0TjkS Z;KK!>QzX5=|D0C2Ys0Ei|G5ghHUMM1x5fYf literal 74155 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 LsG3#(?*9J)ZaW#T diff --git a/test/data/zimfiles/poor.zim b/test/data/zimfiles/poor.zim index a617f6202ec353dbf1935155f969e2f35ff8dbed..bf3d85010ae0f4747896ee4a11cbc151b4784c8e 100644 GIT binary patch delta 1831 zcmb7@c~BEq9LM)@BtQ;O5ef-Jjuhlf2El_vAV@6~8WAj28H|DuNDv_i9%TgqsTQRn zxZ}Z$QK+DD8F7RnA_^YCO69PziXd2~fG`MJDWH9u>@e7w{?V8D?q_#@-{0@Ox4)MP zxz36E+J(y;s|FDDXzkT-&!C2U4Umt-$Qwj{47RAvLbVlr24*@^9WrXM6fubwuUVED zNd`HH`bUy>x*xQ6+{t#UyzKnDZM%E;wPsV){Hm4MLXN)A-V$#!u(B@f@Uq+j_G^yW z4&)nTGs4dJPS)jM#j#R)_lPD}ISm}l8uVL7FSzIDUjBm(i##!2(_$4OwOPY^JyUo0 zzM=90U!LmWd=$OawBZ!uzg&wVvuyUQ$kLUW+>_Do`BoshGLqhM-Hv*`pTAD%A-OC_ zr~ASqhZEy3-5o!8a!<BVIa?yX<ehS`8&3}$oSS=}$Qyi-&b-`4Ya-9IO<(HhQBA#1 zKl?y=v@+)J%*0E9b)oS_!#*XaSJk7u!u*yre%)MG`Yby9+*0-1Zq6nXM=UR{sNTNm zR)7Cc-N71{-ZwVs(H)*QUH@#5PAfe}3aoR_qz+uyD`AiB{1Efb|FNV}H$pyA+iPdX zw<Bd<k(wro{r#*x%Em)ZM>q5Yw%_=|zM#Td>HTzWg2s&)LL0T>M3tTGsvv3i*`Q9Z z(f%v<`4aTZEV5m7J%qHbv5{gBTy{Q&p^QmUrHZUQ@(v4C@tZw{<&!Qh58X4Ju0_~7 zIgVVXT;58n37)Ru6r_D0wXIxG7nUCw5D->qo7=W6Ccix2opkQV=WP8Y9Ba_Jes+VG ztzUL2xf|r|jZC=M9U<;K#gQI85%Qe#ZLo8~(64Bhx$4#8wSP5u7#@mF?+$)icH8`o zYfM%TyM^3oB`a)UG+iwydv|R8!0TPd^PaSpY(4JuNARe3>+aIh@Qv{&rNORKNkQJq zwBmGbZpP}g$;DBF4GD{UZ}29E_WB)K<ArWK@OI0R98-sunRD1hTQo3n)@Ma@e_&j8 zsU(3TTkA@xZL1=$>%Z~x_L!JyVqVx-A$YS^RwOc=9m{d_=DzJM?r7?YDi4^{2gdL? zo5G7}7lQ1Es_sG*4?K*Gjh?*|{Q_nJnsa;u+Rw|~9xKLzUa?F-HK<0GvgTN$tX|e5 za0i?NLf}(7VFQYuCFW^m>M5RO-y$o<_l=MhQz`J0B1$0@l^j{=Y{RfY++b1}YoDd7 zg_Q-ue8N;R>zT#OG-d>o%cL`A87f8t<1{0Kv8i@~^4|rg0cjgcd!JBTG;zT~@$e_0 zgU@XUHv#~wkJX15vZh(%3U2UOmK5-GwX~7uJ&M)`G+%cb^iZL6p$u?8ls*&%N(YJr zr46MAr3FQXqGMzVaQq71A^$fWmg3=g_`pjFQY6A1qF64*r2>;8OOZYupKab^h$$(6 zvwYzzd;ziIWXWEac!^k~UfjH$irG?uGtkCVRA7lEQ-LjQVX6@(r-GG63t!>eEb<y= zV>B9I*uxE~YvCUOA40JpF4iSEDelv0Xc(Uc>^><?$8u=E6BuC4G_VR#v1uAu4WHcT zfcuG0AKOg_E6m}S4be=2AdBD)Z&zB9BsK-M0lP^DpP9mL%*XK#1PCOtcyNpk%+Q5F zxECZ;AjX3WQt<Wj)3%F*sqyNf3qp8LH||IgB>y`A^O0~tT&nsAM8XI4QNUXc%SePt zajp`fL=^jV3YKgL3;@8w3;`SE;znfNj!&$%xn>BGg@PmmiF%=SAjJe96C5LG-=~Qu z609cpk)V0MCLT)g7{R-0qHvWjH3m<D2M87utRi@aU@yTbf`$W{e0zdE1mg%65>y{o cXsyKmC2;&)70%~SBF#|k!+K}-8!syT2RnT$WdHyG delta 3273 zcmb_f3sh4_8lF5b$U}mn;0cfrK}02&;0rZSLU<z*AVHuj^g=Gl#k@=|f^;QTt-#_| z;bJR2D(hR?<1<?8x>DbJ+*7gIRq+Xk&$=q^u4ikhxHC6b6wh{d&z_x}k8kFmfBwhZ zf9763J<Ds`#9t$sblisnQ0wq;alO#RYDNmcE4H-=IgJ@X`?A*Yn%o7Bq|qQ*Vu_J7 zobl$Y0O;pcC9F#;t314rY*=@5>GC@wZ~XlmV6@K+X?|+#`H88cNAVu4UnabNB1eJo zbRZuTgX_6VYFld7Z|(XZrGI(q@DO2Jw<D*CnCjL05!XFT=^a|PIrN9Z`ni>ti=FbM z+&kdo&u=_!;Z?gWa1?cJ6qYV?lS_}ugZB!ze8iPU8&Ab_C#>QIlz1(z_gz|+OP1mF z@*I9u(dq~5NB;Qg!-=xmi@c+;zu8t%?%UG7`iX5$onNMQ-k&~JZyPYrpB{hi#r7jV z^mokss;g*jTq}0(fMw$s%JOjC?Vp`tAy+R=ySQ1Ztm!P=`l$asU&nWep4TpA%LMsu z?l?a7@?BGt<@>Mq?oMf26XU<iv)9{8GHYw#C}*t4@A*rvJwMZWxnqCPlYw(rLoXUu zv~175Bf5I#(72Yr*N>mIYsW=uAoO`*gnCo$^NEuV9v-<V`26j<zg^U5`@VUxVW7T^ zOWya{^z{Cd16NvYMe^;NDyvUAie~Q;cFo>m$lLfu&e;!qM*DkxyM5uf#Qf;Tf0aMm zJx6dR;p+IN2I9oEb1}b~aBJLhZK(Jw-Jef>`ta-M-Fv3&Tg5%TKlJ^goeSl;b;(>` zH@CE=sl(K#CM0UVlsDOaH(|J+U*pn}(3-^CA;U+8#`=Jhf`EH?MB8-j!A$cBZ+}PR z5&I=w<U!$v6nc?v#@f!lN7szIYHqHb9Fav{ul|t3iSxhIT2Q-Jbxrwj{wX|r6fJ&r z<YjC1{Htdh>J;sU6@L_jB}GmPu77s$^pcy2r|oT}cQxN0F0AWU&JyL-hI^z|t{hk# zX{i0j&(TX18|ph64Bbnfh5gjqHlwhs_8$4=jSBaXYnPuI(|2dg;m)y!15dB;6Zc;_ z&|H^lALqB@ry~zqcI<4f&fqr2bJp;Cvp4~;oCBf&5G6wJIluwogeZy1OUsnR$mFQp zg#cva9V^jCH35(AVZcaO%#9a9-4PRdo2BuB1g}8@tjtCbM%-LVn)TQMvXZRAN-9XB zPDk3X+$?R5DhaD5O_7wvVzgrvVW%)1PT@#aMj8n_W;fwRV=+4{X68&dX_gr%lX2gL zK;D=Nsw^_b8%&9o!;k?OCk>H|8mV>4#T>5?$_k^0LN1ztE!0K8b!-9PK(#I(2d)EP zizfhCUT_{J_FApa2Y`nV-4LfC)<I-Ks2~y`W<p3ILLhh$_eTJ53*vi-Z4fOGt0C+V z<q*XXG6*SzKg1nx06M)vI5-D8pF?~Cu^b{DA`W6I1P+6m2a)9kgKhJ4C@_?I9FRy9 zScyfq81-PEH0dRFTWPEmZP6^!gzE{+YSzy#!R^HKsgudPgshy(sEic7MG4#7T&*EV ztB1V=G*o7*7*b9*WvFJNZbFJKL8DDlD(9rARaliFAsY!3$_pIYY0*eQ{bn&5!j^6| zB&DH_s$c<?ngi9R6j(G?BvH!KbCQy^Dd~CT1qu4(#YhAdlp1w<%seI_Wv(_aD<@-` zsvs>bR?6mtRWNxn`C&$qkvdSaxWr&1%3`G$X0Z}xSR68oO)rg8+boo&)MAv_V8v17 zB0^#$%?lJ5?y?6LvPO`N*9PCVS4LT9VHo@?8)7$EqaL*60B`|p|H+hrqO5jkY>1&$ zjYge2XbLcf{wqsw3^`n>B1^w$Nb!2#6o%-yq$-NAnQ<edZHLtp{cm;`CH8uAl^AZL z$WkLAXAYY*PKkyNBO#Iht1TVkG|l`AP6x|04J*SJk);;13|1+8;LK(y4Dz^4c)BH4 zSiUe+ISN(bs&drB3`8NWW8*gw6fQC2CL&h4h$Jel78@m%K!X%vredX)q>eJg>ImqA zh-3znC8U|8NZc4{FU5^StXvlLy3i<+G7@ogcK2A70&@*q@i0D04Y<utK$}`ka%2p% zMqwaO917P_RD>jEsWb)ZBndNi9{2F`6He7w#>WRFM*r3nmK?;fuXf%IzAFv{kKr#w z{8IcE@h$QB0574h`xs6$$Htk-8PRy*G0lr#DNJ^Ff@5~dVy(2;baudZO7m^Qgn}Q3 zNO{f~>PLcirCjHn)JFnt_%UZg{BZ%@Tr?wmk7y5w3_cjNBWPvN{GjZh_ku)${ejm5 z4+S;{E(<IUj1LSE4Tx?uUg-IMI`7~)Mb)8vcqsx7N8BUs0Y2gv;y!qVeI{<~&g65s zpwsz>jo$Io&|M7gLIAHiFbo3T!_0k+?qW9x0R$gnID`iT55k=uxHSp?Usl4!Du}^1 zfNbz(BC}Uo8BfeS(h<<*U6Z__??K53dUID)#M=@SF=4V&i<vVrbiF6t-xVo*TN~v- z7q<t{>Tc1H3A9gi>(R0Fy&e&VPe*hI(;eM%dQZ1|ILZ=%xzf>L8R`UW6Uu?^>5&XE ziT0Czc62m-s7pkD(&NtYq(AG4;ds;E^~{8QkK6Jg>h4IrrhN>Iz?W|5@^HYt&xA6j z!FM81St`(>W**Yu1bVTmYy?hO-kCs8E)l1Qq3X!k9Net0fX93249ZYlQDP+R2A2=n z8}I0e=a$-V>$~1jS!i&*9Tp62=nWIX4*(cYK9n*$#-}rS$2#EG3N$__L=iJ59IL{u zByI+v%o8~WU?t1#EcdhQ@8z1$X1ST=PL?NlyXI9aSF(JFVfgvc#|kEna4G6o-o|n} z%U4)_%(8&}cSFkZY?iZGu4Z`)%gix>iR?#33EO*%<~{qh=Z!1k{X1W#M`V0+`k!Yq BkdOcX diff --git a/test/metadata-test.cpp b/test/metadata-test.cpp index e2315a29..903e0a7c 100644 --- a/test/metadata-test.cpp +++ b/test/metadata-test.cpp @@ -17,12 +17,14 @@ TEST(Metadata, detectsAbsenceOfMandatoryEntries) ASSERT_FALSE(m.valid()); ASSERT_EQ(m.check(), zim::Metadata::Errors({ + "Missing mandatory metadata: Name", "Missing mandatory metadata: Title", "Missing mandatory metadata: Language", "Missing mandatory metadata: Creator", "Missing mandatory metadata: Publisher", "Missing mandatory metadata: Date", "Missing mandatory metadata: Description", + "Missing mandatory metadata: Illustration_48x48@1", }) ); @@ -37,6 +39,7 @@ TEST(Metadata, detectsAbsenceOfMandatoryEntries) "Missing mandatory metadata: Title", "Missing mandatory metadata: Language", "Missing mandatory metadata: Publisher", + "Missing mandatory metadata: Illustration_48x48@1", }) ); @@ -118,7 +121,7 @@ TEST(Metadata, regexpConstraints) ASSERT_FALSE(m.valid()); ASSERT_EQ(m.check(), zim::Metadata::Errors({ - "Language doesn't match regex: \\w{2,3}(,\\w{2,3})*" + "Language doesn't match regex: \\w{3}(,\\w{3})*" }) ); diff --git a/test/zimcheck-test.cpp b/test/zimcheck-test.cpp index 6bb2a355..e217ddca 100644 --- a/test/zimcheck-test.cpp +++ b/test/zimcheck-test.cpp @@ -27,7 +27,7 @@ TEST(zimfilechecks, test_checksum) TEST(zimfilechecks, test_metadata) { - std::string fn = "data/zimfiles/wikibooks_be_all_nopic_2017-02.zim"; + std::string fn = "data/zimfiles/good.zim"; zim::Archive archive(fn); ErrorLogger logger; @@ -531,6 +531,7 @@ TEST(zimcheck, metadata_poorzimfile) "[ERROR] Metadata errors:" "\n" " Missing mandatory metadata: Title" "\n" " Missing mandatory metadata: Description" "\n" + " Missing mandatory metadata: Illustration_48x48@1" "\n" "[INFO] Overall Test Status: Fail" "\n" "[INFO] Total time taken by zimcheck: <3 seconds." "\n" ); @@ -721,6 +722,7 @@ const std::string ALL_CHECKS_OUTPUT_ON_POORZIMFILE( "[ERROR] Metadata errors:" "\n" " Missing mandatory metadata: Title" "\n" " Missing mandatory metadata: Description" "\n" + " Missing mandatory metadata: Illustration_48x48@1" "\n" "[ERROR] Favicon:" "\n" " Favicon is missing" "\n" "[ERROR] Missing mainpage:" "\n" @@ -842,6 +844,12 @@ TEST(zimcheck, json_poorzimfile) " \"error\" : \"Missing mandatory metadata: Description\"" "\n" " }," "\n" " {" "\n" + " \"check\" : \"metadata\"," "\n" + " \"level\" : \"ERROR\"," "\n" + " \"message\" : \"Missing mandatory metadata: Illustration_48x48@1\"," "\n" + " \"error\" : \"Missing mandatory metadata: Illustration_48x48@1\"" "\n" + " }," "\n" + " {" "\n" " \"check\" : \"favicon\"," "\n" " \"level\" : \"ERROR\"," "\n" " \"message\" : \"Favicon is missing\"" "\n" From e4766ca21da91a457c0696008e3da27cf1cb3390 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Wed, 26 Apr 2023 12:20:35 +0400 Subject: [PATCH 14/16] Simple metadata checks are run unconditionally Before this change, simple metadata checks were performed only if there were no missing metadata entries. --- src/metadata.cpp | 17 ++++++++++------- test/metadata-test.cpp | 24 ++++++++++++++++++++++++ test/zimcheck-test.cpp | 16 ++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/metadata.cpp b/src/metadata.cpp index 617a133f..216c7d7c 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -139,6 +139,12 @@ std::string escapeNonPrintableChars(const std::string& s) return os.str(); } +Metadata::Errors concat(Metadata::Errors e1, const Metadata::Errors& e2) +{ + e1.insert(e1.end(), e2.begin(), e2.end()); + return e1; +} + } // unnamed namespace const Metadata::ReservedMetadataTable& Metadata::reservedMetadataInfo = reservedMetadataInfoTable; @@ -229,13 +235,10 @@ Metadata::Errors Metadata::checkComplexConstraints() const Metadata::Errors Metadata::check() const { - Errors e = checkMandatoryMetadata(); - if ( !e.empty() ) - return e; - - e = checkSimpleConstraints(); - if ( !e.empty() ) - return e; + const Errors e1 = checkMandatoryMetadata(); + const Errors e2 = checkSimpleConstraints(); + if ( !e1.empty() || !e2.empty() ) + return concat(e1, e2); return checkComplexConstraints(); } diff --git a/test/metadata-test.cpp b/test/metadata-test.cpp index 903e0a7c..34e2b86f 100644 --- a/test/metadata-test.cpp +++ b/test/metadata-test.cpp @@ -167,3 +167,27 @@ TEST(Metadata, complexConstraints) }) ); } + +TEST(Metadata, mandatoryMetadataAndSimpleChecksAreRunUnconditionally) +{ + zim::Metadata m; + + m.set("Description", "Blablabla"); + m.set("Date", "2020-20-20"); + m.set("Creator", "Demiurge"); + m.set("Name", "wikipedia_js_maxi"); + m.set("Title", "A title that is too long to read for a five year old"); + m.set("Publisher", "Zangak"); + m.set("Language", "js"); + //m.set("Illustration_48x48@1", ""); + + ASSERT_FALSE(m.valid()); + ASSERT_EQ(m.check(), + zim::Metadata::Errors({ + "Missing mandatory metadata: Illustration_48x48@1", + "Language must contain at least 3 characters", + "Language doesn't match regex: \\w{3}(,\\w{3})*", + "Title must contain at most 30 characters" + }) + ); +} diff --git a/test/zimcheck-test.cpp b/test/zimcheck-test.cpp index e217ddca..c13f54b7 100644 --- a/test/zimcheck-test.cpp +++ b/test/zimcheck-test.cpp @@ -532,6 +532,8 @@ TEST(zimcheck, metadata_poorzimfile) " Missing mandatory metadata: Title" "\n" " Missing mandatory metadata: Description" "\n" " Missing mandatory metadata: Illustration_48x48@1" "\n" + " Language must contain at least 3 characters" "\n" + " Language doesn't match regex: \\w{3}(,\\w{3})*" "\n" "[INFO] Overall Test Status: Fail" "\n" "[INFO] Total time taken by zimcheck: <3 seconds." "\n" ); @@ -723,6 +725,8 @@ const std::string ALL_CHECKS_OUTPUT_ON_POORZIMFILE( " Missing mandatory metadata: Title" "\n" " Missing mandatory metadata: Description" "\n" " Missing mandatory metadata: Illustration_48x48@1" "\n" + " Language must contain at least 3 characters" "\n" + " Language doesn't match regex: \\w{3}(,\\w{3})*" "\n" "[ERROR] Favicon:" "\n" " Favicon is missing" "\n" "[ERROR] Missing mainpage:" "\n" @@ -850,6 +854,18 @@ TEST(zimcheck, json_poorzimfile) " \"error\" : \"Missing mandatory metadata: Illustration_48x48@1\"" "\n" " }," "\n" " {" "\n" + " \"check\" : \"metadata\"," "\n" + " \"level\" : \"ERROR\"," "\n" + " \"message\" : \"Language must contain at least 3 characters\"," "\n" + " \"error\" : \"Language must contain at least 3 characters\"" "\n" + " }," "\n" + " {" "\n" + " \"check\" : \"metadata\"," "\n" + " \"level\" : \"ERROR\"," "\n" + " \"message\" : \"Language doesn't match regex: \\\\w{3}(,\\\\w{3})*\"," "\n" + " \"error\" : \"Language doesn't match regex: \\\\w{3}(,\\\\w{3})*\"" "\n" + " }," "\n" + " {" "\n" " \"check\" : \"favicon\"," "\n" " \"level\" : \"ERROR\"," "\n" " \"message\" : \"Favicon is missing\"" "\n" From e56713925b9a6d58ced1c5340727e7c91f69da3a Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Wed, 26 Apr 2023 12:38:12 +0400 Subject: [PATCH 15/16] A fake PNG is created via a named function --- test/metadata-test.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/metadata-test.cpp b/test/metadata-test.cpp index 34e2b86f..7a50df58 100644 --- a/test/metadata-test.cpp +++ b/test/metadata-test.cpp @@ -2,6 +2,10 @@ #include "gtest/gtest.h" +std::string fakePNG() +{ + return "\x89PNG\r\n\x1a\n" + std::string(100, 'x'); +} TEST(Metadata, isDefaultConstructible) { @@ -46,7 +50,7 @@ TEST(Metadata, detectsAbsenceOfMandatoryEntries) m.set("Title", "Chief Executive Officer"); m.set("Publisher", "Zangak"); m.set("Language", "py3"); - m.set("Illustration_48x48@1", "\x89PNG\r\n\x1a\n" + std::string(100, 'x')); + m.set("Illustration_48x48@1", fakePNG()); ASSERT_TRUE(m.valid()); ASSERT_TRUE(m.check().empty()); @@ -63,7 +67,7 @@ zim::Metadata makeValidMetadata() m.set("Title", "Chief Executive Officer"); m.set("Publisher", "Zangak"); m.set("Language", "py3"); - m.set("Illustration_48x48@1", "\x89PNG\r\n\x1a\n" + std::string(100, 'x')); + m.set("Illustration_48x48@1", fakePNG()); return m; } From d32037cac255eafda32bcea6041b45c8f24ca89b Mon Sep 17 00:00:00 2001 From: Veloman Yunkan <veloman.yunkan@gmail.com> Date: Wed, 26 Apr 2023 12:40:01 +0400 Subject: [PATCH 16/16] Another test for complex constraints on metadata --- test/metadata-test.cpp | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/metadata-test.cpp b/test/metadata-test.cpp index 7a50df58..fe5110d7 100644 --- a/test/metadata-test.cpp +++ b/test/metadata-test.cpp @@ -195,3 +195,34 @@ TEST(Metadata, mandatoryMetadataAndSimpleChecksAreRunUnconditionally) }) ); } + +TEST(Metadata, complexChecksAreRunOnlyIfMandatoryMetadataRequirementsAreMet) +{ + zim::Metadata m; + + m.set("Description", "Blablabla"); + m.set("LongDescription", "Blabla"); + m.set("Date", "2020-20-20"); + m.set("Creator", "TED"); + m.set("Name", "TED_bodylanguage"); + //m.set("Title", ""); + m.set("Publisher", "Kiwix"); + m.set("Language", "bod,yla,ngu,age"); + m.set("Illustration_48x48@1", fakePNG()); + + ASSERT_FALSE(m.valid()); + ASSERT_EQ(m.check(), + zim::Metadata::Errors({ + "Missing mandatory metadata: Title", + }) + ); + + m.set("Title", "Blabluba"); + + ASSERT_FALSE(m.valid()); + ASSERT_EQ(m.check(), + zim::Metadata::Errors({ + "LongDescription shouldn't be shorter than Description" + }) + ); +}