1 /***************************************************************************
2     copyright            : (C) 2002 - 2008 by Scott Wheeler
3     email                : wheeler@kde.org
4  ***************************************************************************/
5 
6 /***************************************************************************
7  *   This library is free software; you can redistribute it and/or modify  *
8  *   it under the terms of the GNU Lesser General Public License version   *
9  *   2.1 as published by the Free Software Foundation.                     *
10  *                                                                         *
11  *   This library is distributed in the hope that it will be useful, but   *
12  *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
13  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *
14  *   Lesser General Public License for more details.                       *
15  *                                                                         *
16  *   You should have received a copy of the GNU Lesser General Public      *
17  *   License along with this library; if not, write to the Free Software   *
18  *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA         *
19  *   02110-1301  USA                                                       *
20  *                                                                         *
21  *   Alternatively, this file is available under the Mozilla Public        *
22  *   License Version 1.1.  You may obtain a copy of the License at         *
23  *   http://www.mozilla.org/MPL/                                           *
24  ***************************************************************************/
25 
26 #include <tbytevector.h>
27 #include <tdebug.h>
28 
29 #include <flacpicture.h>
30 #include <xiphcomment.h>
31 #include <tpropertymap.h>
32 
33 using namespace TagLib;
34 
35 namespace
36 {
37   typedef Ogg::FieldListMap::Iterator FieldIterator;
38   typedef Ogg::FieldListMap::ConstIterator FieldConstIterator;
39 
40   typedef List<FLAC::Picture *> PictureList;
41   typedef PictureList::Iterator PictureIterator;
42   typedef PictureList::Iterator PictureConstIterator;
43 }
44 
45 class Ogg::XiphComment::XiphCommentPrivate
46 {
47 public:
XiphCommentPrivate()48   XiphCommentPrivate()
49   {
50     pictureList.setAutoDelete(true);
51   }
52 
53   FieldListMap fieldListMap;
54   String vendorID;
55   String commentField;
56   PictureList pictureList;
57 };
58 
59 ////////////////////////////////////////////////////////////////////////////////
60 // public members
61 ////////////////////////////////////////////////////////////////////////////////
62 
XiphComment()63 Ogg::XiphComment::XiphComment() :
64   TagLib::Tag(),
65   d(new XiphCommentPrivate())
66 {
67 }
68 
XiphComment(const ByteVector & data)69 Ogg::XiphComment::XiphComment(const ByteVector &data) :
70   TagLib::Tag(),
71   d(new XiphCommentPrivate())
72 {
73   parse(data);
74 }
75 
~XiphComment()76 Ogg::XiphComment::~XiphComment()
77 {
78   delete d;
79 }
80 
title() const81 String Ogg::XiphComment::title() const
82 {
83   if(d->fieldListMap["TITLE"].isEmpty())
84     return String();
85   return d->fieldListMap["TITLE"].toString();
86 }
87 
artist() const88 String Ogg::XiphComment::artist() const
89 {
90   if(d->fieldListMap["ARTIST"].isEmpty())
91     return String();
92   return d->fieldListMap["ARTIST"].toString();
93 }
94 
album() const95 String Ogg::XiphComment::album() const
96 {
97   if(d->fieldListMap["ALBUM"].isEmpty())
98     return String();
99   return d->fieldListMap["ALBUM"].toString();
100 }
101 
comment() const102 String Ogg::XiphComment::comment() const
103 {
104   if(!d->fieldListMap["DESCRIPTION"].isEmpty()) {
105     d->commentField = "DESCRIPTION";
106     return d->fieldListMap["DESCRIPTION"].toString();
107   }
108 
109   if(!d->fieldListMap["COMMENT"].isEmpty()) {
110     d->commentField = "COMMENT";
111     return d->fieldListMap["COMMENT"].toString();
112   }
113 
114   return String();
115 }
116 
genre() const117 String Ogg::XiphComment::genre() const
118 {
119   if(d->fieldListMap["GENRE"].isEmpty())
120     return String();
121   return d->fieldListMap["GENRE"].toString();
122 }
123 
year() const124 unsigned int Ogg::XiphComment::year() const
125 {
126   if(!d->fieldListMap["DATE"].isEmpty())
127     return d->fieldListMap["DATE"].front().toInt();
128   if(!d->fieldListMap["YEAR"].isEmpty())
129     return d->fieldListMap["YEAR"].front().toInt();
130   return 0;
131 }
132 
track() const133 unsigned int Ogg::XiphComment::track() const
134 {
135   if(!d->fieldListMap["TRACKNUMBER"].isEmpty())
136     return d->fieldListMap["TRACKNUMBER"].front().toInt();
137   if(!d->fieldListMap["TRACKNUM"].isEmpty())
138     return d->fieldListMap["TRACKNUM"].front().toInt();
139   return 0;
140 }
141 
setTitle(const String & s)142 void Ogg::XiphComment::setTitle(const String &s)
143 {
144   addField("TITLE", s);
145 }
146 
setArtist(const String & s)147 void Ogg::XiphComment::setArtist(const String &s)
148 {
149   addField("ARTIST", s);
150 }
151 
setAlbum(const String & s)152 void Ogg::XiphComment::setAlbum(const String &s)
153 {
154   addField("ALBUM", s);
155 }
156 
setComment(const String & s)157 void Ogg::XiphComment::setComment(const String &s)
158 {
159   if(d->commentField.isEmpty()) {
160     if(!d->fieldListMap["DESCRIPTION"].isEmpty())
161       d->commentField = "DESCRIPTION";
162     else
163       d->commentField = "COMMENT";
164   }
165 
166   addField(d->commentField, s);
167 }
168 
setGenre(const String & s)169 void Ogg::XiphComment::setGenre(const String &s)
170 {
171   addField("GENRE", s);
172 }
173 
setYear(unsigned int i)174 void Ogg::XiphComment::setYear(unsigned int i)
175 {
176   removeFields("YEAR");
177   if(i == 0)
178     removeFields("DATE");
179   else
180     addField("DATE", String::number(i));
181 }
182 
setTrack(unsigned int i)183 void Ogg::XiphComment::setTrack(unsigned int i)
184 {
185   removeFields("TRACKNUM");
186   if(i == 0)
187     removeFields("TRACKNUMBER");
188   else
189     addField("TRACKNUMBER", String::number(i));
190 }
191 
isEmpty() const192 bool Ogg::XiphComment::isEmpty() const
193 {
194   for(FieldConstIterator it = d->fieldListMap.begin(); it != d->fieldListMap.end(); ++it) {
195     if(!(*it).second.isEmpty())
196       return false;
197   }
198 
199   return true;
200 }
201 
fieldCount() const202 unsigned int Ogg::XiphComment::fieldCount() const
203 {
204   unsigned int count = 0;
205 
206   for(FieldConstIterator it = d->fieldListMap.begin(); it != d->fieldListMap.end(); ++it)
207     count += (*it).second.size();
208 
209   count += d->pictureList.size();
210 
211   return count;
212 }
213 
fieldListMap() const214 const Ogg::FieldListMap &Ogg::XiphComment::fieldListMap() const
215 {
216   return d->fieldListMap;
217 }
218 
properties() const219 PropertyMap Ogg::XiphComment::properties() const
220 {
221   return d->fieldListMap;
222 }
223 
setProperties(const PropertyMap & properties)224 PropertyMap Ogg::XiphComment::setProperties(const PropertyMap &properties)
225 {
226   // check which keys are to be deleted
227   StringList toRemove;
228   for(FieldConstIterator it = d->fieldListMap.begin(); it != d->fieldListMap.end(); ++it)
229     if (!properties.contains(it->first))
230       toRemove.append(it->first);
231 
232   for(StringList::ConstIterator it = toRemove.begin(); it != toRemove.end(); ++it)
233       removeFields(*it);
234 
235   // now go through keys in \a properties and check that the values match those in the xiph comment
236   PropertyMap invalid;
237   PropertyMap::ConstIterator it = properties.begin();
238   for(; it != properties.end(); ++it)
239   {
240     if(!checkKey(it->first))
241       invalid.insert(it->first, it->second);
242     else if(!d->fieldListMap.contains(it->first) || !(it->second == d->fieldListMap[it->first])) {
243       const StringList &sl = it->second;
244       if(sl.isEmpty())
245         // zero size string list -> remove the tag with all values
246         removeFields(it->first);
247       else {
248         // replace all strings in the list for the tag
249         StringList::ConstIterator valueIterator = sl.begin();
250         addField(it->first, *valueIterator, true);
251         ++valueIterator;
252         for(; valueIterator != sl.end(); ++valueIterator)
253           addField(it->first, *valueIterator, false);
254       }
255     }
256   }
257   return invalid;
258 }
259 
checkKey(const String & key)260 bool Ogg::XiphComment::checkKey(const String &key)
261 {
262   if(key.size() < 1)
263     return false;
264 
265   // A key may consist of ASCII 0x20 through 0x7D, 0x3D ('=') excluded.
266 
267   for(String::ConstIterator it = key.begin(); it != key.end(); it++) {
268       if(*it < 0x20 || *it > 0x7D || *it == 0x3D)
269         return false;
270   }
271 
272   return true;
273 }
274 
vendorID() const275 String Ogg::XiphComment::vendorID() const
276 {
277   return d->vendorID;
278 }
279 
addField(const String & key,const String & value,bool replace)280 void Ogg::XiphComment::addField(const String &key, const String &value, bool replace)
281 {
282   if(!checkKey(key)) {
283     debug("Ogg::XiphComment::addField() - Invalid key. Field not added.");
284     return;
285   }
286 
287   const String upperKey = key.upper();
288 
289   if(replace)
290     removeFields(upperKey);
291 
292   if(!key.isEmpty() && !value.isEmpty())
293     d->fieldListMap[upperKey].append(value);
294 }
295 
removeField(const String & key,const String & value)296 void Ogg::XiphComment::removeField(const String &key, const String &value)
297 {
298   if(!value.isNull())
299     removeFields(key, value);
300   else
301     removeFields(key);
302 }
303 
removeFields(const String & key)304 void Ogg::XiphComment::removeFields(const String &key)
305 {
306   d->fieldListMap.erase(key.upper());
307 }
308 
removeFields(const String & key,const String & value)309 void Ogg::XiphComment::removeFields(const String &key, const String &value)
310 {
311   StringList &fields = d->fieldListMap[key.upper()];
312   for(StringList::Iterator it = fields.begin(); it != fields.end(); ) {
313     if(*it == value)
314       it = fields.erase(it);
315     else
316       ++it;
317   }
318 }
319 
removeAllFields()320 void Ogg::XiphComment::removeAllFields()
321 {
322   d->fieldListMap.clear();
323 }
324 
contains(const String & key) const325 bool Ogg::XiphComment::contains(const String &key) const
326 {
327   return !d->fieldListMap[key.upper()].isEmpty();
328 }
329 
removePicture(FLAC::Picture * picture,bool del)330 void Ogg::XiphComment::removePicture(FLAC::Picture *picture, bool del)
331 {
332   PictureIterator it = d->pictureList.find(picture);
333   if(it != d->pictureList.end())
334     d->pictureList.erase(it);
335 
336   if(del)
337     delete picture;
338 }
339 
removeAllPictures()340 void Ogg::XiphComment::removeAllPictures()
341 {
342   d->pictureList.clear();
343 }
344 
addPicture(FLAC::Picture * picture)345 void Ogg::XiphComment::addPicture(FLAC::Picture * picture)
346 {
347   d->pictureList.append(picture);
348 }
349 
pictureList()350 List<FLAC::Picture *> Ogg::XiphComment::pictureList()
351 {
352   return d->pictureList;
353 }
354 
render() const355 ByteVector Ogg::XiphComment::render() const
356 {
357   return render(true);
358 }
359 
render(bool addFramingBit) const360 ByteVector Ogg::XiphComment::render(bool addFramingBit) const
361 {
362   ByteVector data;
363 
364   // Add the vendor ID length and the vendor ID.  It's important to use the
365   // length of the data(String::UTF8) rather than the length of the the string
366   // since this is UTF8 text and there may be more characters in the data than
367   // in the UTF16 string.
368 
369   ByteVector vendorData = d->vendorID.data(String::UTF8);
370 
371   data.append(ByteVector::fromUInt(vendorData.size(), false));
372   data.append(vendorData);
373 
374   // Add the number of fields.
375 
376   data.append(ByteVector::fromUInt(fieldCount(), false));
377 
378   // Iterate over the the field lists.  Our iterator returns a
379   // std::pair<String, StringList> where the first String is the field name and
380   // the StringList is the values associated with that field.
381 
382   FieldListMap::ConstIterator it = d->fieldListMap.begin();
383   for(; it != d->fieldListMap.end(); ++it) {
384 
385     // And now iterate over the values of the current list.
386 
387     String fieldName = (*it).first;
388     StringList values = (*it).second;
389 
390     StringList::ConstIterator valuesIt = values.begin();
391     for(; valuesIt != values.end(); ++valuesIt) {
392       ByteVector fieldData = fieldName.data(String::UTF8);
393       fieldData.append('=');
394       fieldData.append((*valuesIt).data(String::UTF8));
395 
396       data.append(ByteVector::fromUInt(fieldData.size(), false));
397       data.append(fieldData);
398     }
399   }
400 
401   for(PictureConstIterator it = d->pictureList.begin(); it != d->pictureList.end(); ++it) {
402     ByteVector picture = (*it)->render().toBase64();
403     data.append(ByteVector::fromUInt(picture.size() + 23, false));
404     data.append("METADATA_BLOCK_PICTURE=");
405     data.append(picture);
406   }
407 
408   // Append the "framing bit".
409 
410   if(addFramingBit)
411     data.append(char(1));
412 
413   return data;
414 }
415 
416 ////////////////////////////////////////////////////////////////////////////////
417 // protected members
418 ////////////////////////////////////////////////////////////////////////////////
419 
parse(const ByteVector & data)420 void Ogg::XiphComment::parse(const ByteVector &data)
421 {
422   // The first thing in the comment data is the vendor ID length, followed by a
423   // UTF8 string with the vendor ID.
424 
425   unsigned int pos = 0;
426 
427   const unsigned int vendorLength = data.toUInt(0, false);
428   pos += 4;
429 
430   d->vendorID = String(data.mid(pos, vendorLength), String::UTF8);
431   pos += vendorLength;
432 
433   // Next the number of fields in the comment vector.
434 
435   const unsigned int commentFields = data.toUInt(pos, false);
436   pos += 4;
437 
438   if(commentFields > (data.size() - 8) / 4) {
439     return;
440   }
441 
442   for(unsigned int i = 0; i < commentFields; i++) {
443 
444     // Each comment field is in the format "KEY=value" in a UTF8 string and has
445     // 4 bytes before the text starts that gives the length.
446 
447     const unsigned int commentLength = data.toUInt(pos, false);
448     pos += 4;
449 
450     const ByteVector entry = data.mid(pos, commentLength);
451     pos += commentLength;
452 
453     // Don't go past data end
454 
455     if(pos > data.size())
456       break;
457 
458     // Check for field separator
459 
460     const int sep = entry.find('=');
461     if(sep < 1) {
462       debug("Ogg::XiphComment::parse() - Discarding a field. Separator not found.");
463       continue;
464     }
465 
466     // Parse the key
467 
468     const String key = String(entry.mid(0, sep), String::UTF8).upper();
469     if(!checkKey(key)) {
470       debug("Ogg::XiphComment::parse() - Discarding a field. Invalid key.");
471       continue;
472     }
473 
474     if(key == "METADATA_BLOCK_PICTURE" || key == "COVERART") {
475 
476       // Handle Pictures separately
477 
478       const ByteVector picturedata = ByteVector::fromBase64(entry.mid(sep + 1));
479       if(picturedata.isEmpty()) {
480         debug("Ogg::XiphComment::parse() - Discarding a field. Invalid base64 data");
481         continue;
482       }
483 
484       if(key[0] == L'M') {
485 
486         // Decode FLAC Picture
487 
488         FLAC::Picture * picture = new FLAC::Picture();
489         if(picture->parse(picturedata)) {
490           d->pictureList.append(picture);
491         }
492         else {
493           delete picture;
494           debug("Ogg::XiphComment::parse() - Failed to decode FLAC Picture block");
495         }
496       }
497       else {
498 
499         // Assume it's some type of image file
500 
501         FLAC::Picture * picture = new FLAC::Picture();
502         picture->setData(picturedata);
503         picture->setMimeType("image/");
504         picture->setType(FLAC::Picture::Other);
505         d->pictureList.append(picture);
506       }
507     }
508     else {
509 
510       // Parse the text
511 
512       addField(key, String(entry.mid(sep + 1), String::UTF8), false);
513     }
514   }
515 }
516