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
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
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"
+      })
+  );
+}