1 #include <cstdio>
2 #include <cstring>
3 #include <cstdlib>
4 #include <cctype>
5 #include <algorithm>
6 #include <map>
7 #include <string>
8 #include "id3v1.h"
9 #include "getid3.h"
10 #include "setid3.h"
11 
12 /*
13 
14   copyright (c) 2004, 2005 squell <squell@alumina.nl>
15 
16   use, modification, copying and distribution of this software is permitted
17   under the conditions described in the file 'COPYING'.
18 
19 */
20 
21 #if defined(_WIN32)
22 #    include <io.h>
23 #    define ftrunc(f)  chsize(fileno(f), ftell(f))
24 #else
25 #    include <unistd.h>
26 #    define ftrunc(f)  ftruncate(fileno(f), ftell(f))
27 #endif
28 
29 using namespace std;
30 
31 using tag::write::ID3;
32 using tag::ID3field;
33 
34 typedef int concreteness_check[ sizeof ID3() ];
35 
36 static ID3v1 const zero_tag = {
37     { 'T', 'A', 'G' },
38     "",  // title
39     "",  // artist
40     "",  // album
41     "",  // year
42     "",  // cmnt
43     0,
44     0,   // track
45     255  // genre
46 };
47 
48 /* ====================================================== */
49 
str_upper(string s)50 static string str_upper(string s)
51 {
52     for(string::iterator p = s.begin(); p != s.end(); ++p)
53         *p = toupper(*p);
54     return s;
55 }
56 
57 /* ====================================================== */
58 
59  /*
60     This is basically an enhanced lexicographical_compare which ignores
61     certain parts of a string during comparison:
62 
63     Any plain character compared against a seperator gets 'eaten'
64     e.g. "Alt Rock" will match "Alternative Rock" exactly.
65 
66     Every seperator matches every other seperator.
67     e.g. "Fast Fusion" matches "Fast-Fusion"
68   */
69 
issep(int c)70 static inline bool issep(int c)
71 {
72     return !isalnum(c);
73 }
74 
clipped_compare(const string & is,const string & js)75 static bool clipped_compare(const string& is, const string& js)
76 {
77     string::const_iterator i = is.begin();
78     string::const_iterator j = js.begin();
79 
80     for( ; i != is.end() && j != js.end(); ++i, ++j) {
81         if(issep(*i)) {
82             if(!issep(*j)) --i;
83         } else {
84             if( issep(*j)) --j; else {
85                 if(*i < *j)
86                    return true;
87                 if(*i > *j)
88                    return false;
89             }
90         }
91     }
92     return find_if(i, is.end(), issep) == is.end() &&
93            find_if(j, js.end(), issep) != js.end();
94 }
95 
96 /* ====================================================== */
97 
98 struct genre_map : map<string,int,bool (*)(const string&,const string&)> {
99     typedef const_iterator iter;                // shorthand
100 
genre_mapgenre_map101     genre_map()                                 // initialize associative map
102     : map<string,int,key_compare>( clipped_compare )
103     {
104         (*this)[ "Psych" ] = 67;                // small kludges
105         (*this)[ "Folk0" ] = 80;
106         (*this)[ "Humo"  ] = 100;
107         for(int i=0; i < ID3v1_numgenres; i++) {
108             (*this)[ str_upper(ID3v1_genre[i]) ] = i;
109         }
110     }
111 } const ID3_genre;
112 
113 /* ====================================================== */
114 
from(const char * fn)115 bool ID3::from(const char* fn)
116 {
117     delete null_tag, null_tag = 0;
118     if(fn) {
119         read::ID3 src(fn);
120         if(src) null_tag = new ID3v1(src.tag);
121     }
122     return null_tag;
123 }
124 
125  // note: Intel C++ produces an internal error here if the base class has
126  // the return type as 'const reader*' and this class doesn't. apparently
127  // caused by some compiler bug in relation to multiple inheritance.
128 
read(const char * fn) const129 tag::metadata* ID3::read(const char* fn) const
130 {
131     return new read::ID3(fn);
132 }
133 
134  // note: Borland doesn't suppress array-to-pointer conversion when deducing
135  // reference type parameters (in this case), so use pointer instead
136 
137 template<size_t N>
setfield(char (* dest)[N],const charset::conv<> * src,size_t maxlen=N)138 static inline bool setfield(char (*dest)[N], const charset::conv<>* src, size_t maxlen = N)
139 {
140     if(src)
141         return strncpy(*dest, src->template str<charset::latin1>().c_str(), maxlen);
142     else
143         return false;
144 }
145 
vmodify(const char * fn,const function & edit) const146 bool ID3::vmodify(const char* fn, const function& edit) const
147 {
148     const ID3v1& synth_tag = null_tag? *null_tag : zero_tag;
149 
150     ID3v1 tag;
151 
152     if( FILE* f = fopen(fn, "rb+") ) {
153         fseek(f, -128, SEEK_END)  == 0    &&
154         fread(&tag, 1, 128, f)    == 128  || (tag.TAG[0] = 0);
155 
156         int err = 1;
157         if( memcmp(tag.TAG, "TAG", 3) == 0 ) {
158             err = fseek(f, -128, SEEK_END);   // overwrite existing tag
159         } else if( generate ) {
160             tag = synth_tag;                  // create new tag
161             clearerr(f);
162             err = fseek(f,    0, ftell(f)<128? SEEK_END : SEEK_CUR);
163         } else {
164             fclose(f);
165             return true;
166         }
167 
168         if( err != 0 ) {
169             fclose(f);
170             return false;
171         }
172 
173         if( cleared ) tag = synth_tag;
174 
175         using namespace charset;
176         const string* field;                  // reading aid
177         int n = bool(null_tag);               // count number of set fields
178 
179         if(field = update[title])
180             n += setfield(&tag.title,  edit(*field));
181 
182         if(field = update[artist])
183             n += setfield(&tag.artist, edit(*field));
184 
185         if(field = update[album])
186             n += setfield(&tag.album,  edit(*field));
187 
188         if(field = update[year])
189             n += setfield(&tag.year,   edit(*field));
190 
191         if(field = update[track])
192             if(function::result rs = edit(*field)) {
193                 ++n, tag.track = atoi( rs.str<latin1>().c_str() );
194                 tag.zero = '\0';
195             }
196 
197         if(field = update[cmnt])
198             n += setfield(&tag.cmnt,   edit(*field), sizeof tag.cmnt + 2*(tag.zero||!tag.track));
199 
200         if(field = update[genre]) {
201             if(function::result rs = edit(*field)) {
202                 string s          = str_upper( rs.str<latin1>() );
203                 unsigned int x    = atoi(s.c_str()) - 1;
204                 genre_map::iter g = ID3_genre.find(s);
205                 tag.genre = (s.empty() || g==ID3_genre.end()? x : g->second);
206                 ++n;
207             }
208         }
209 
210         if( cleared && n == 0 ) {
211             err = ftrunc(f) != 0;
212         } else {
213             err = fwrite(&tag, 1, 128, f) != 128;
214         }
215 
216         err |= fclose(f) != 0;
217 
218         if(err)
219             throw failure("error writing TAG to ", fn);
220 
221         return true;
222     };
223 
224     return false;
225 }
226 
~ID3()227 ID3::~ID3()
228 {
229     delete null_tag;
230 }
231 
232 /*
233 
234  annotated bug:
235 
236  Under MS-DOS with DJGPP, if an fwrite() crossed the end of the file, and no
237  fseek() has occured, the buffer space(??) of the last fread() will be
238  appended to the file before the data actually written by fwrite(). (?)
239 
240  ARGH!
241 
242  Actually, this is C conformant behaviour - no write shall follow a read
243  without an intervening fflush or fseek.
244 
245 */
246 
247