]> granicus.if.org Git - taglib/commitdiff
Check PropertyMap keys format-specifically instead of globally.
authorMichael Helmling <helmling@mathematik.uni-kl.de>
Mon, 30 Jul 2012 18:52:30 +0000 (20:52 +0200)
committerMichael Helmling <helmling@mathematik.uni-kl.de>
Mon, 30 Jul 2012 18:52:30 +0000 (20:52 +0200)
Instead of statically forbidding certain keys in PropertyMap, now the
setProperties() implementations of the different formats check if the
keys are valid for that particular specification and include them in
the returned PropertyMap otherwise.
This should remove an unneccessary complification for programmers since
now there's only one step, namely calling setProperties(), where
problems might occur.
Also the previous implementation leads to problems with invalid keys:
because taglib doesn't use exceptions, something like

  map.insert("FORBIDDEN KEY", "some value");

would lead to the value being inserted under String::null, which
smells like the source of strange bugs.

14 files changed:
taglib/ape/apetag.cpp
taglib/ape/apetag.h
taglib/mpeg/id3v2/frames/commentsframe.cpp
taglib/mpeg/id3v2/frames/textidentificationframe.cpp
taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.cpp
taglib/mpeg/id3v2/frames/urllinkframe.cpp
taglib/ogg/xiphcomment.cpp
taglib/ogg/xiphcomment.h
taglib/toolkit/tpropertymap.cpp
taglib/toolkit/tpropertymap.h
tests/CMakeLists.txt
tests/test_apetag.cpp
tests/test_propertymap.cpp [new file with mode: 0644]
tests/test_xiphcomment.cpp

index 7a16f3f23d47aefdf2389701e549a4dfc8fbf67a..f1af0028c452707a890ca49558ef55170379b092 100644 (file)
@@ -189,7 +189,7 @@ PropertyMap APE::Tag::properties() const
   PropertyMap properties;
   ItemListMap::ConstIterator it = itemListMap().begin();
   for(; it != itemListMap().end(); ++it) {
-    String tagName = PropertyMap::prepareKey(it->first);
+    String tagName = it->first.upper();
     // if the item is Binary or Locator, or if the key is an invalid string,
     // add to unsupportedData
     if(it->second.type() != Item::Text || tagName.isNull())
@@ -227,7 +227,7 @@ PropertyMap APE::Tag::setProperties(const PropertyMap &origProps)
   StringList toRemove;
   ItemListMap::ConstIterator remIt = itemListMap().begin();
   for(; remIt != itemListMap().end(); ++remIt) {
-    String key = PropertyMap::prepareKey(remIt->first);
+    String key = remIt->first.upper();
     // only remove if a) key is valid, b) type is text, c) key not contained in new properties
     if(!key.isNull() && remIt->second.type() == APE::Item::Text && !properties.contains(key))
       toRemove.append(remIt->first);
@@ -238,9 +238,12 @@ PropertyMap APE::Tag::setProperties(const PropertyMap &origProps)
 
   // now sync in the "forward direction"
   PropertyMap::ConstIterator it = properties.begin();
+  PropertyMap invalid;
   for(; it != properties.end(); ++it) {
     const String &tagName = it->first;
-    if(!(itemListMap().contains(tagName)) || !(itemListMap()[tagName].values() == it->second)) {
+    if(!checkKey(tagName))
+      invalid.insert(it->first, it->second);
+    else if(!(itemListMap().contains(tagName)) || !(itemListMap()[tagName].values() == it->second)) {
       if(it->second.size() == 0)
         removeItem(tagName);
       else {
@@ -252,7 +255,21 @@ PropertyMap APE::Tag::setProperties(const PropertyMap &origProps)
       }
     }
   }
-  return PropertyMap();
+  return invalid;
+}
+
+bool APE::Tag::checkKey(const String &key)
+{
+  if(key.size() < 2 or key.size() > 16)
+      return false;
+    for(String::ConstIterator it = key.begin(); it != key.end(); it++)
+        // only allow printable ASCII including space (32..127)
+        if (*it < 32 || *it >= 128)
+          return false;
+    String upperKey = key.upper();
+    if (upperKey=="ID3" || upperKey=="TAG" || upperKey=="OGGS" || upperKey=="MP+")
+      return false;
+    return true;
 }
 
 APE::Footer *APE::Tag::footer() const
index 1a8c30c9bad4e1b567c8a942d683e814a650d68b..c1f12e296d917a13331aaf234b5c76ffd523da5d 100644 (file)
@@ -123,10 +123,17 @@ namespace TagLib {
 
       /*!
        * Implements the unified tag dictionary interface -- import function. The same
-       * comments as for the export function apply.
+       * comments as for the export function apply; additionally note that the APE tag
+       * specification requires keys to have between 2 and 16 printable ASCII characters
+       * with the exception of the fixed strings "ID3", "TAG", "OGGS", and "MP+".
        */
       PropertyMap setProperties(const PropertyMap &);
 
+      /*!
+       * Check if the given String is a valid APE tag key.
+       */
+      static bool checkKey(const String&);
+
       /*!
        * Returns a pointer to the tag's footer.
        */
index 2c6c49f9cada6de2a79733c0c244aa77e6e89f9f..181955314924bb220b7db6845c5372ec410ee645 100644 (file)
@@ -112,7 +112,7 @@ void CommentsFrame::setTextEncoding(String::Type encoding)
 
 PropertyMap CommentsFrame::asProperties() const
 {
-  String key = PropertyMap::prepareKey(description());
+  String key = description().upper();
   PropertyMap map;
   if(key.isEmpty() || key == "COMMENT")
     map.insert("COMMENT", text());
index b6a02b06818610ed5dcf704ee061bf6b9ded18d3..b01f5129906d63c28238253e548c6062e73e4eb3 100644 (file)
@@ -289,7 +289,7 @@ PropertyMap TextIdentificationFrame::makeTMCLProperties() const
   }
   StringList l = fieldList();
   for(StringList::ConstIterator it = l.begin(); it != l.end(); ++it) {
-    String instrument = PropertyMap::prepareKey(*it);
+    String instrument = it->upper();
     if(instrument.isNull()) {
       // instrument is not a valid key -> frame unsupported
       map.clear();
@@ -382,7 +382,7 @@ PropertyMap UserTextIdentificationFrame::asProperties() const
   String tagName = description();
 
   PropertyMap map;
-  String key = map.prepareKey(tagName);
+  String key = tagName.upper();
   if(key.isNull()) // this frame's description is not a valid PropertyMap key -> add to unsupported list
     map.unsupportedData().append(L"TXXX/" + description());
   else {
index 9d76164da9c5eb0ed04f4ef72385b4e4b13e999c..3f4d6a79d475b6cbd619f1ab4bd939926e7b1fc9 100644 (file)
@@ -116,7 +116,7 @@ void UnsynchronizedLyricsFrame::setTextEncoding(String::Type encoding)
 PropertyMap UnsynchronizedLyricsFrame::asProperties() const
 {
   PropertyMap map;
-  String key = PropertyMap::prepareKey(description());
+  String key = description().upper();
   if(key.isEmpty() || key.upper() == "LYRICS")
     map.insert("LYRICS", text());
   else if(key.isNull())
index 5e4f2db7e926f4b0d5a6c4f5d58ce4982c71b2ba..6bcbbda4593e0bed62d9b38fe527c47a3bdf366a 100644 (file)
@@ -156,7 +156,7 @@ void UserUrlLinkFrame::setDescription(const String &s)
 PropertyMap UserUrlLinkFrame::asProperties() const
 {
   PropertyMap map;
-  String key = PropertyMap::prepareKey(description());
+  String key = description().upper();
   if(key.isEmpty() || key.upper() == "URL")
     map.insert("URL", url());
   else if(key.isNull())
index 5a6feef3e42a1267573dcc03e08cb12753e478b4..675614b3fb74cb6163bcc96db74aed3150f6f65c 100644 (file)
@@ -205,11 +205,14 @@ PropertyMap Ogg::XiphComment::setProperties(const PropertyMap &properties)
   for(StringList::ConstIterator it = toRemove.begin(); it != toRemove.end(); ++it)
       removeField(*it);
 
-  // now go through keys in \a properties and check that the values match those in the xiph comment */
+  // now go through keys in \a properties and check that the values match those in the xiph comment
+  PropertyMap invalid;
   PropertyMap::ConstIterator it = properties.begin();
   for(; it != properties.end(); ++it)
   {
-    if(!d->fieldListMap.contains(it->first) || !(it->second == d->fieldListMap[it->first])) {
+    if(!checkKey(it->first))
+      invalid.insert(it->first, it->second);
+    else if(!d->fieldListMap.contains(it->first) || !(it->second == d->fieldListMap[it->first])) {
       const StringList &sl = it->second;
       if(sl.size() == 0)
         // zero size string list -> remove the tag with all values
@@ -224,7 +227,18 @@ PropertyMap Ogg::XiphComment::setProperties(const PropertyMap &properties)
       }
     }
   }
-  return PropertyMap();
+  return invalid;
+}
+
+bool Ogg::XiphComment::checkKey(const String &key)
+{
+  if(key.size() < 1)
+    return false;
+  for(String::ConstIterator it = key.begin(); it != key.end(); it++)
+      // forbid non-printable, non-ascii, '=' (#61) and '~' (#126)
+      if (*it < 32 || *it >= 128 || *it == 61 || *it == 126)
+        return false;
+  return true;
 }
 
 String Ogg::XiphComment::vendorID() const
index 3b44086eec544be5d78d57b55915a920f680f9f3..54f3070bf39de621bf8fcdeafb91e867829cbd5e 100644 (file)
@@ -151,10 +151,18 @@ namespace TagLib {
 
       /*!
        * Implements the unified property interface -- import function.
-       * The tags from the given map will be stored one-to-one in the file.
+       * The tags from the given map will be stored one-to-one in the file,
+       * except for invalid keys (less than one character, non-ASCII, or
+       * containing '=' or '~') in which case the according values will
+       * be contained in the returned PropertyMap.
        */
       PropertyMap setProperties(const PropertyMap&);
 
+      /*!
+       * Check if the given String is a valid Xiph comment key.
+       */
+      static bool checkKey(const String&);
+
       /*!
        * Returns the vendor ID of the Ogg Vorbis encoder.  libvorbis 1.0 as the
        * most common case always returns "Xiph.Org libVorbis I 20020717".
index 1743a0b239910e1b467f01b5ddd6a816b83a4b66..3317cc92749b77491b7d52e24bbecd31838b8efe 100644 (file)
@@ -34,7 +34,7 @@ PropertyMap::PropertyMap(const PropertyMap &m) : SimplePropertyMap(m), unsupport
 PropertyMap::PropertyMap(const SimplePropertyMap &m)
 {
   for(SimplePropertyMap::ConstIterator it = m.begin(); it != m.end(); ++it){
-    String key = prepareKey(it->first);
+    String key = it->first.upper();
     if(!key.isNull())
       insert(it->first, it->second);
     else
@@ -48,10 +48,7 @@ PropertyMap::~PropertyMap()
 
 bool PropertyMap::insert(const String &key, const StringList &values)
 {
-  String realKey = prepareKey(key);
-  if(realKey.isNull())
-    return false;
-
+  String realKey = key.upper();
   Iterator result = SimplePropertyMap::find(realKey);
   if(result == end())
     SimplePropertyMap::insert(realKey, values);
@@ -62,9 +59,7 @@ bool PropertyMap::insert(const String &key, const StringList &values)
 
 bool PropertyMap::replace(const String &key, const StringList &values)
 {
-  String realKey = prepareKey(key);
-  if(realKey.isNull())
-    return false;
+  String realKey = key.upper();
   SimplePropertyMap::erase(realKey);
   SimplePropertyMap::insert(realKey, values);
   return true;
@@ -72,26 +67,17 @@ bool PropertyMap::replace(const String &key, const StringList &values)
 
 PropertyMap::Iterator PropertyMap::find(const String &key)
 {
-  String realKey = prepareKey(key);
-  if(realKey.isNull())
-    return end();
-  return SimplePropertyMap::find(realKey);
+  return SimplePropertyMap::find(key.upper());
 }
 
 PropertyMap::ConstIterator PropertyMap::find(const String &key) const
 {
-  String realKey = prepareKey(key);
-  if(realKey.isNull())
-    return end();
-  return SimplePropertyMap::find(realKey);
+  return SimplePropertyMap::find(key.upper());
 }
 
 bool PropertyMap::contains(const String &key) const
 {
-  String realKey = prepareKey(key);
-  if(realKey.isNull())
-    return false;
-  return SimplePropertyMap::contains(realKey);
+  return SimplePropertyMap::contains(key.upper());
 }
 
 bool PropertyMap::contains(const PropertyMap &other) const
@@ -107,9 +93,7 @@ bool PropertyMap::contains(const PropertyMap &other) const
 
 PropertyMap &PropertyMap::erase(const String &key)
 {
-  String realKey = prepareKey(key);
-  if(!realKey.isNull())
-    SimplePropertyMap::erase(realKey);
+  SimplePropertyMap::erase(key.upper());
   return *this;
 }
 
@@ -122,23 +106,20 @@ PropertyMap &PropertyMap::erase(const PropertyMap &other)
 
 PropertyMap &PropertyMap::merge(const PropertyMap &other)
 {
-  for(PropertyMap::ConstIterator it = other.begin(); it != other.end(); ++it) {
+  for(PropertyMap::ConstIterator it = other.begin(); it != other.end(); ++it)
     insert(it->first, it->second);
-  }
   unsupported.append(other.unsupported);
   return *this;
 }
 
 const StringList &PropertyMap::operator[](const String &key) const
 {
-  String realKey = prepareKey(key);
-  return SimplePropertyMap::operator[](realKey);
+  return SimplePropertyMap::operator[](key.upper());
 }
 
 StringList &PropertyMap::operator[](const String &key)
 {
-  String realKey = prepareKey(key);
-  return SimplePropertyMap::operator[](realKey);
+  return SimplePropertyMap::operator[](key.upper());
 }
 
 bool PropertyMap::operator==(const PropertyMap &other) const
@@ -190,13 +171,3 @@ const StringList &PropertyMap::unsupportedData() const
 {
   return unsupported;
 }
-
-String PropertyMap::prepareKey(const String &proposed) {
-  if(proposed.isEmpty())
-    return String::null;
-  for (String::ConstIterator it = proposed.begin(); it != proposed.end(); it++)
-    // forbid non-printable, non-ascii, '=' (#61) and '~' (#126)
-    if (*it < 32 || *it >= 128 || *it == 61 || *it == 126)
-      return String::null;
-  return proposed.upper();
-}
index b955739b8137c7d07fa91d32d3154a231fd6b0ee..7f59b21ea0375711d94426915b07a33fb425f5bc 100644 (file)
@@ -36,15 +36,10 @@ namespace TagLib {
    * ("tags") realized as pairs of a case-insensitive key
    * and a nonempty list of corresponding values, each value being an an arbitrary
    * unicode String.
-   * The key has the same restrictions as in the vorbis comment specification,
-   * i.e. it must contain at least one character; all printable ASCII characters
-   * except '=' and '~' are allowed.
-   *
-   * In order to be safe with other formats, keep these additional restrictions in mind:
-   *
-   * - APE only allows keys from 2 to 16 printable ASCII characters (including space),
-   *   with the exception of these strings: ID3, TAG, OggS, MP+
    *
+   * Note that most metadata formats pose additional conditions on the tag keys. The
+   * most popular ones (Vorbis, APE, ID3v2) should support all ASCII only words of
+   * length between 2 and 16.
    */
 
   class TAGLIB_EXPORT PropertyMap: public SimplePropertyMap
@@ -126,16 +121,17 @@ namespace TagLib {
     /*!
      * Returns a reference to the value associated with \a key.
      *
-     * \note: This has undefined behavior if the key is not valid or not
-     * present in the map.
+     * \note: If \a key is not contained in the map, an empty
+     * StringList is returned without error.
      */
     const StringList &operator[](const String &key) const;
 
     /*!
      * Returns a reference to the value associated with \a key.
      *
-     * \note: This has undefined behavior if the key is not valid or not
-     * present in the map.
+     * \note: If \a key is not contained in the map, an empty
+     * StringList is returned. You can also directly add entries
+     * by using this function as an lvalue.
      */
     StringList &operator[](const String &key);
 
@@ -168,12 +164,6 @@ namespace TagLib {
 
     String toString() const;
 
-    /*!
-     * Converts \a proposed into another String suitable to be used as
-     * a key, or returns String::null if this is not possible.
-     */
-    static String prepareKey(const String &proposed);
-
   private:
 
 
index ed4eec0d2b0191a170c178eb8e11054c83c0563c..38ba4d25f0d82afb17715b585e83776c1564d56c 100644 (file)
@@ -35,6 +35,7 @@ SET(test_runner_SRCS
   test_bytevectorlist.cpp
   test_bytevectorstream.cpp
   test_string.cpp
+  test_propertymap.cpp
   test_fileref.cpp
   test_id3v1.cpp
   test_id3v2.cpp
index c469670396ef0e0abfc976e0fb6683e4b7f97dde..6cda5e52f1dd49b696c63d922fa9a9dcf6267a1c 100644 (file)
@@ -19,6 +19,7 @@ class TestAPETag : public CppUnit::TestFixture
   CPPUNIT_TEST(testIsEmpty2);
   CPPUNIT_TEST(testPropertyInterface1);
   CPPUNIT_TEST(testPropertyInterface2);
+  CPPUNIT_TEST(testInvalidKeys);
   CPPUNIT_TEST_SUITE_END();
 
 public:
@@ -76,12 +77,27 @@ public:
     APE::Item item3 = APE::Item("TRACKNUMBER", "29");
     tag.setItem("TRACKNUMBER", item3);
     properties = tag.properties();
-    CPPUNIT_ASSERT_EQUAL(2u, properties["TRACKNUMBER"].size());
+    CPPUNIT_ASSERT_EQUAL(uint(2), properties["TRACKNUMBER"].size());
     CPPUNIT_ASSERT_EQUAL(String("17"), properties["TRACKNUMBER"][0]);
     CPPUNIT_ASSERT_EQUAL(String("29"), properties["TRACKNUMBER"][1]);
 
   }
 
+  void testInvalidKeys()
+  {
+    PropertyMap properties;
+    properties["A"] = String("invalid key: one character");
+    properties["MP+"] = String("invalid key: forbidden string");
+    properties["A B~C"] = String("valid key: space and tilde");
+    properties["ARTIST"] = String("valid key: normal one");
+
+    APE::Tag tag;
+    PropertyMap unsuccessful = tag.setProperties(properties);
+    CPPUNIT_ASSERT_EQUAL(uint(2), unsuccessful.size());
+    CPPUNIT_ASSERT(unsuccessful.contains("A"));
+    CPPUNIT_ASSERT(unsuccessful.contains("MP+"));
+  }
+
 };
 
 CPPUNIT_TEST_SUITE_REGISTRATION(TestAPETag);
diff --git a/tests/test_propertymap.cpp b/tests/test_propertymap.cpp
new file mode 100644 (file)
index 0000000..d0b2fbb
--- /dev/null
@@ -0,0 +1,32 @@
+#include <cppunit/extensions/HelperMacros.h>
+#include <tpropertymap.h>
+class TestPropertyMap : public CppUnit::TestFixture
+{
+  CPPUNIT_TEST_SUITE(TestPropertyMap);
+  CPPUNIT_TEST(testInvalidKeys);
+  CPPUNIT_TEST_SUITE_END();
+
+public:
+  void testInvalidKeys()
+  {
+    TagLib::PropertyMap map1;
+    CPPUNIT_ASSERT(map1.isEmpty());
+    map1["ÄÖÜ"].append("test");
+    CPPUNIT_ASSERT_EQUAL(map1.size(), 1u);
+
+    TagLib::PropertyMap map2;
+    map2["ÄÖÜ"].append("test");
+    CPPUNIT_ASSERT(map1 == map2);
+    CPPUNIT_ASSERT(map1.contains(map2));
+
+    map2["ARTIST"] = TagLib::String("Test Artist");
+    CPPUNIT_ASSERT(map1 != map2);
+    CPPUNIT_ASSERT(map2.contains(map1));
+
+    map2["ÄÖÜ"].append("test 2");
+    CPPUNIT_ASSERT(!map2.contains(map1));
+
+  }
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(TestPropertyMap);
index 359206e80e1820b5438b05c9da60d913dc4ecff4..a98aea6169b8056a6972c5bbb6a2d6664e6a5bbe 100644 (file)
@@ -2,6 +2,7 @@
 #include <string>
 #include <stdio.h>
 #include <xiphcomment.h>
+#include <tpropertymap.h>
 #include <tdebug.h>
 #include "utils.h"
 
@@ -15,6 +16,7 @@ class TestXiphComment : public CppUnit::TestFixture
   CPPUNIT_TEST(testSetYear);
   CPPUNIT_TEST(testTrack);
   CPPUNIT_TEST(testSetTrack);
+  CPPUNIT_TEST(testInvalidKeys);
   CPPUNIT_TEST_SUITE_END();
 
 public:
@@ -59,6 +61,19 @@ public:
     CPPUNIT_ASSERT_EQUAL(String("3"), cmt.fieldListMap()["TRACKNUMBER"].front());
   }
 
+  void testInvalidKeys()
+  {
+    PropertyMap map;
+    map[""] = String("invalid key: empty string");
+    map["A=B"] = String("invalid key: contains '='");
+    map["A~B"] = String("invalid key: contains '~'");
+
+    Ogg::XiphComment cmt;
+    PropertyMap unsuccessful = cmt.setProperties(map);
+    CPPUNIT_ASSERT_EQUAL(TagLib::uint(3), unsuccessful.size());
+    CPPUNIT_ASSERT(cmt.properties().isEmpty());
+  }
+
 };
 
 CPPUNIT_TEST_SUITE_REGISTRATION(TestXiphComment);