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