1 #include <new>
2 #include <algorithm>
3 #include <functional>
4 #include <cctype>
5 #include <cstdlib>
6 #include "char_ucs.h"
7 #include "id3v1.h"
8 #include "id3v2.h"
9 #include "fileops.h"
10 #include "getid3v2.h"
11 #include "setid3v2.h"
12 
13 /*
14 
15   copyright (c) 2004, 2005, 2015 squell <squell@alumina.nl>
16 
17   use, modification, copying and distribution of this software is permitted
18   under the conditions described in the file 'COPYING'.
19 
20   Note: I'm devoting quite a bit of code to glue the interface from C to C++,
21   and I'm not entirely happy about it. I'm a bit confused as to the reasons I
22   didn't chose to write id3v2.c in C++. :(
23 
24 */
25 
26 using namespace std;
27 
28 using tag::write::ID3v2;
29 using tag::ID3field;
30 
31 namespace {
32 
33     typedef int concreteness_check[ sizeof ID3v2() ];
34 
35 /* ===================================== */
36 
37  // extra hairyness to prevent buffer overflows by re-allocating on the fly
38  // overkill, but i had to do a runtime check anyway, so.
39 
40     class writer {
41         size_t avail;
42         char *base, *dest;
43         ID3VER version;                     // writes ID3v2.3 per default
44 
45     public:
init(size_t len,ID3VER v=ID3_v2_3)46         void init(size_t len, ID3VER v = ID3_v2_3)
47                          { base = (char*) malloc(avail=len+!len);
48                            if(!base) throw bad_alloc();
49                            dest = (char*) ID3_put(base,version=v,0,0,0); }
50 
operator char*()51         operator char*() { return base; }
52 
writer()53         writer()         { base = 0; }
~writer()54        ~writer()         { free(base); }
55 
56         void put(const char* ID, const void* src, size_t len);
57 
58     private:
59         writer(writer&);                    // non-copyable
60         void operator=(writer&);
61     };
62 
put(const char * ID,const void * src,size_t len)63     void writer::put(const char* ID, const void* src, size_t len)
64     {
65         static size_t factor = 0x1000;      // start reallocing in 4k blocks
66 
67         if(len+10 > avail) {
68             while(len+10 > factor) factor *= 2;
69             size_t size = dest - base;
70             base     = (char*) realloc(base, size+factor);
71             avail    = factor;
72             if(!base) throw bad_alloc();
73             dest     = base + size;         // translate current pointer
74         }
75 
76         avail -= (len+10);
77         dest = (char*) ID3_put(dest, version, ID, src, len);
78     }
79 
80 /* ===================================== */
81 
82  // convert C handler to a C++ exception at program startup
83 
84     extern "C" void copy_failure(const char*, const char*);
85 
86     struct guard {
guard__anone0051eea0111::guard87         guard()            { ID3_wfail = copy_failure; }
88         static string err;
89         static void raise();
90     } static fail_inst;
91 
92     string guard::err;
93 
raise()94     void guard::raise()
95     {
96         string emsg;
97         emsg.swap(err);
98         if(!emsg.empty())
99             throw tag::failure(emsg);
100     }
101 
copy_failure(const char * oldn,const char * newn)102     extern "C" void copy_failure(const char* oldn, const char* newn)
103     {
104         if(oldn == newn) {
105             string emsg("error writing ID3 to ");
106             guard::err = emsg + oldn;
107         } else {
108             string emsg(": file lost, contents still in `");
109             guard::err = newn + emsg + oldn + '\'';
110         }
111     }
112 
113 }
114 
115 /* ===================================== */
116 
117  // code for constructing ID3v2 frames. rather hairy, but hey, ID3v2 sucks
118 
needs_unicode(charset::conv<wchar_t> str)119 inline static bool needs_unicode(charset::conv<wchar_t> str)
120 {
121     const wstring& ws = str;
122     return ws.end() != find_if(ws.begin(), ws.end(), bind2nd(greater<wchar_t>(), 0xFF));
123 }
124 
encode(int enc,const charset::conv<> & str)125 static const string encode(int enc, const charset::conv<>& str)
126 {
127     using charset::conv;
128     switch(enc) {
129     case 0:
130         return conv<charset::latin1>(str);
131     case 1:
132         return conv<charset::utf16>(str);
133     /*
134     case 2:
135         return conv<charset::utf16be>(str);
136     case 3:
137         return conv<charset::utf8>(str);
138     */
139     }
140     throw enc;
141 }
142 
143   // split "FIELD[:descr[:lan]]" into three components
144   // returns actual descriptor via result
145 
split_field(string & field,string & lang)146 static string split_field(string& field, string& lang)
147 {
148     string descr;
149     const string::size_type colon = field.find(':');       // split description
150     if(colon != string::npos) {
151         descr = field.substr(colon+1);
152         field.erase(colon);
153     }
154     const string::size_type E = descr.size();              // split language
155     if(E >= 4 && descr[E-4] == ':' && isalpha(descr[E-3]) && isalpha(descr[E-2]) && isalpha(descr[E-1])) {
156         lang = descr.substr(E-3);
157         descr.erase(E-4);
158     }
159     return descr;
160 }
161 
162  // returns empty string if unsupported
163 
binarize(string field,charset::conv<charset::latin1> content)164 static const string binarize(string field, charset::conv<charset::latin1> content)
165 {
166     using tag::read::ID3v2;
167     using charset::conv;
168 
169     string lang = "xxx";
170     const conv<char>& descr = split_field(field, lang) += '\0';
171 
172     if(field == "TCON" || field == "TCO") {                // genre by number
173         unsigned int x = atoi(string(content).c_str())-1;  // is portable
174         if(x < ID3v1_numgenres) content = ID3v1_genre[x];
175     }
176 
177     string data;
178     if(!ID3v2::is_valid(field))
179         return data;
180     if(ID3v2::is_counter(field)) {
181         unsigned long t = strtoul(string(content).c_str(), 0, 0);
182         data.push_back(t >> 24 & 0xFF);
183         data.push_back(t >> 16 & 0xFF);
184         data.push_back(t >>  8 & 0xFF);
185         data.push_back(t       & 0xFF);
186         return data;
187     }
188 
189     // figure out of latin1 is enough to encode strings
190     data = char(needs_unicode(content + descr));
191 
192     if(ID3v2::has_lang(field)) {
193         data.append(lang);
194     }
195     if(ID3v2::has_desc(field)) {
196         data.append(encode(data[0], descr));
197     } else if(descr.length() > 1) {
198         return string();
199     }
200 
201     if(data.length() > 1 || ID3v2::is_text(field)) {
202         const string blob = ID3v2::is_url(field)? string(content) : encode(data[0], content);
203         return data.append(blob);
204     } else if(ID3v2::is_url(field)) {
205         return content;
206     } else {
207         return string();
208     }
209 }
210 
211 /* ===================================== */
212 
213 typedef map<string,string> db;
214 
set(ID3field i,string m)215 ID3v2& ID3v2::set(ID3field i, string m)
216 {
217     static const char xlat2[][4] = {                     // ID3field order!
218         "TT2", "TP1", "TAL", "TYE", "COM", "TRK", "TCO"
219     };
220     static const char xlat3[][5] = {
221         "TIT2", "TPE1", "TALB", "TYER", "COMM", "TRCK", "TCON"
222     };
223     if(i < FIELD_MAX) {
224         set(xlat2[i], m);      // let error handling decide between them.
225         set(xlat3[i], m);
226     }
227     return *this;
228 }
229 
reserve(size_t n)230 ID3v2& ID3v2::reserve(size_t n)
231 {
232     resize = n? n : 1;
233     return *this;
234 }
235 
from(const char * fn)236 bool ID3v2::from(const char* fn)
237 {
238     ID3_free(null_tag);
239     return null_tag = (fn? ID3_readf(fn, 0) : 0);
240 }
241 
make_canonical(string & field)242 static void make_canonical(string& field)
243 {
244     using tag::read::ID3v2;
245     string lang = "xxx";
246     string descr = split_field(field, lang);
247     if(ID3v2::has_lang(field))
248         field += ':' + descr + ':' + lang;
249     else if(ID3v2::has_desc(field) || !descr.empty())
250         field += ':' + descr;
251 }
252 
set(string field,string s)253 bool ID3v2::set(string field, string s)
254 {
255     make_canonical(field);
256     if( binarize(field, "0").length() != 0 ) {      // test a dummy string
257         mod[field] = s;
258         return true;
259     }
260     return false;
261 }
262 
rm(string field)263 bool ID3v2::rm(string field)
264 {
265     make_canonical(field);
266     mod[field].erase();
267     return true;
268 }
269 
270 /* ===================================== */
271 
read(const char * fn) const272 tag::metadata* ID3v2::read(const char* fn) const
273 {
274     return new read::ID3v2(fn);
275 }
276 
277 namespace tag {
278     extern read::ID3v2::value_string unbinarize(ID3FRAME, charset::conv<>& descriptor);
279 }
280 
has_enc(const char * ID)281 static inline bool has_enc(const char* ID)
282 {
283     return tag::read::ID3v2::is_text(ID) || tag::read::ID3v2::has_desc(ID);
284 }
285 
vmodify(const char * fn,const function & edit) const286 bool ID3v2::vmodify(const char* fn, const function& edit) const
287 {
288     size_t check;
289     read::ID3v2 info( ID3_readf(fn, &check) );
290 
291     if(!info.tag) {
292         if(check != 0)
293             return false;                           // evil ID3 tag
294         if(!force)
295             return true;
296     }
297 
298     const void* src = fresh? null_tag : info.tag;
299     ::writer tag;
300     db table(mod);
301 
302     if( src ) {                                     // update existing tags
303         ID3FRAME f;
304         tag.init(0x1000, ID3_start(f, src));
305 
306         while(ID3_frame(f)) {
307             string field = f->ID;
308             if(read::ID3v2::has_desc(f->ID)) {
309                 charset::conv<char> descr;
310                 tag::unbinarize(f, descr);
311                 field += descr;
312             }
313             db::iterator p = table.find(field);
314             if(p == table.end()) {
315                 if(has_enc(f->ID) && *f->data > 1U) {
316                     charset::conv<char> _;          // recode v2.4 text to UTF16
317                     const string b = binarize(field, tag::unbinarize(f, _));
318                     tag.put(f->ID, b.data(), b.length());
319                 } else
320                     tag.put(f->ID, f->data, f->size);
321             } else {
322                 if(!p->second.empty()) {            // else: erase frames
323                     if(function::result s = edit(p->second)) {
324                         const string b = binarize(p->first, s);
325                         tag.put(f->ID, b.data(), b.length());
326                     } else {
327                         tag.put(f->ID, f->data, f->size);
328                     }
329                     table.erase(p);
330                 }
331             }
332         }
333     } else {
334         tag.init(0x1000);
335     }
336 
337     for(db::iterator p = table.begin(); p != table.end(); ++p) {
338         if(!p->second.empty()) {
339             if(function::result s = edit(p->second)) {
340                 const string field = p->first.substr(0, p->first.find(':'));
341                 const string b = binarize(p->first, s);
342                 tag.put(field.c_str(), b.data(), b.length());
343             }
344         }
345     }
346 
347     bool result = ID3_writef(fn, tag, resize);
348     guard::raise();                                 // deferred exception?
349 
350     return result;
351 }
352 
~ID3v2()353 ID3v2::~ID3v2()
354 {
355     if(null_tag) ID3_free(null_tag);
356 }
357 
358