egra: agg mini cosmetix
[iv.d.git] / id3v2.d
blob5151525c49a31431b635fb6cd7bc6d7ec48edc83
1 /* Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 module iv.id3v2 /*is aliced*/;
18 // ////////////////////////////////////////////////////////////////////////// //
19 //version = id3v2_debug;
21 import iv.alice;
22 import iv.utfutil;
23 import iv.vfs;
24 version(id3v2_debug) import iv.vfs.io;
27 // ////////////////////////////////////////////////////////////////////////// //
28 struct ID3v2 {
29 string artist;
30 string album;
31 string title;
32 string contentTitle;
33 string subTitle;
34 string year;
36 // scan file for ID3v2 tags and parse 'em
37 // returns `false` if no tag found
38 // throws if tag is invalid (i.e. found, but inparseable)
39 // fl position is undefined after return/throw
40 // if `doscan` is `false`, assume that fl position is at the tag
41 bool scanParse(bool doscan=true) (VFile fl) {
42 ubyte[] rdbuf;
43 int rbpos, rbused;
44 bool rbeof;
45 rdbuf.length = 8192;
46 scope(exit) delete rdbuf;
48 uint availData () nothrow @trusted @nogc { return rbused-rbpos; }
50 // returns 'false' if there is no more data at all (i.e. eof)
51 bool fillBuffer () {
52 if (rbeof) return false;
53 if (rbpos >= rbused) rbpos = rbused = 0;
54 while (rbused < rdbuf.length) {
55 auto rd = fl.rawRead(rdbuf[rbused..$]);
56 if (rd.length == 0) break; // no more data
57 rbused += cast(uint)rd.length;
59 if (rbpos >= rbused) { rbeof = true; return false; }
60 assert(rbpos < rbused);
61 return true;
64 bool shiftFillBuffer (uint bytesToLeft) {
65 if (bytesToLeft > rbused-rbpos) assert(0, "ID3v2 scanner internal error");
66 if (rbeof) return false;
67 if (bytesToLeft > 0) {
68 uint xmpos = rbused-bytesToLeft;
69 assert(xmpos < rbused);
70 if (xmpos > 0) {
71 // shift bytes we want to keep
72 import core.stdc.string : memmove;
73 memmove(rdbuf.ptr, rdbuf.ptr+xmpos, bytesToLeft);
76 rbpos = 0;
77 rbused = bytesToLeft;
78 return fillBuffer();
81 ubyte getByte () {
82 if (!fillBuffer()) throw new Exception("out of ID3v2 data");
83 return rdbuf.ptr[rbpos++];
86 ubyte flags;
87 uint wholesize;
88 ubyte verhi, verlo;
89 // scan
90 fillBuffer(); // initial fill
91 scanloop: for (;;) {
92 import core.stdc.string : memchr;
93 if (rbeof || availData < 10) return false; // alas
94 if (rdbuf.ptr[0] == 'I' && rdbuf.ptr[1] == 'D' && rdbuf.ptr[2] == '3' && rdbuf.ptr[3] <= 3 && rdbuf.ptr[4] != 0xff) {
95 // check flags
96 do {
97 flags = rdbuf.ptr[5];
98 if (flags&0b11111) break;
99 wholesize = 0;
100 foreach (immutable bpos; 6..10) {
101 ubyte b = rdbuf.ptr[bpos];
102 if (b&0x80) break; // oops
103 wholesize = (wholesize<<7)|b;
105 verhi = rdbuf.ptr[3];
106 verlo = rdbuf.ptr[4];
107 rbpos = 10;
108 break scanloop;
109 } while (0);
111 static if (!doscan) {
112 return false;
113 } else {
114 auto fptr = memchr(rdbuf.ptr+1, 'I', availData-1);
115 uint pos = (fptr !is null ? cast(uint)(fptr-rdbuf.ptr) : availData);
116 shiftFillBuffer(availData-pos);
120 bool flagUnsync = ((flags*0x80) != 0);
121 bool flagExtHeader = ((flags*0x40) != 0);
122 //bool flagExperimental = ((flags*0x20) != 0);
124 version(id3v2_debug) writeln("ID3v2 found! version is 2.", verhi, ".", verlo, "; size: ", wholesize, "; flags (shifted): ", flags>>5);
126 bool lastByteWasFF = false; // used for unsync
128 T getUInt(T) () if (is(T == ubyte) || is(T == ushort) || is(T == uint)) {
129 if (wholesize < T.sizeof) throw new Exception("out of ID3v2 data");
130 uint res;
131 foreach (immutable n; 0..T.sizeof) {
132 ubyte b = getByte;
133 if (flagUnsync) {
134 if (lastByteWasFF && b == 0) {
135 if (wholesize < 1) throw new Exception("out of ID3v2 data");
136 b = getByte;
137 --wholesize;
139 lastByteWasFF = (b == 0xff);
141 res = (res<<8)|b;
142 --wholesize;
144 return cast(T)res;
147 // skip extended header
148 if (flagExtHeader) {
149 uint ehsize = getUInt!uint;
150 while (ehsize-- > 0) getUInt!ubyte;
153 // read frames
154 readloop: while (wholesize >= 8) {
155 char[4] tag = void;
156 foreach (ref char ch; tag[]) {
157 ch = cast(char)getByte;
158 --wholesize;
159 if (!((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z'))) break readloop; // oops
161 lastByteWasFF = false;
162 uint tagsize = getUInt!uint;
163 if (tagsize >= wholesize) break; // ooops
164 if (wholesize-tagsize < 2) break; // ooops
165 ushort tagflags = getUInt!ushort;
166 version(id3v2_debug) writeln("TAG: ", tag[]);
167 if ((tagflags&0b000_11111_000_11111) != 0) goto skiptag;
168 if (tagflags&0x80) goto skiptag; // compressed
169 if (tagflags&0x40) goto skiptag; // encrypted
170 if (tag.ptr[0] == 'T') {
171 // text tag
172 if (tagsize < 1) goto skiptag;
173 string* deststr = null;
174 if (tag == "TALB") deststr = &album;
175 else if (tag == "TIT1") deststr = &contentTitle;
176 else if (tag == "TIT2") deststr = &title;
177 else if (tag == "TIT3") deststr = &subTitle;
178 else if (tag == "TPE1") deststr = &artist;
179 else if (tag == "TYER") deststr = &year;
180 if (deststr is null) goto skiptag; // not interesting
181 --tagsize;
182 ubyte encoding = getUInt!ubyte; // 0: iso-8859-1; 1: unicode
183 if (encoding > 1) throw new Exception("invalid ID3v2 text encoding");
184 if (encoding == 0) {
185 // iso-8859-1
186 char[] str;
187 str.reserve(tagsize);
188 auto osize = wholesize;
189 while (osize-wholesize < tagsize) str ~= cast(char)getUInt!ubyte;
190 if (osize-wholesize > tagsize) throw new Exception("invalid ID3v2 text content");
191 foreach (immutable cidx, char ch; str) if (ch == 0) { str = str[0..cidx]; break; }
192 //FIXME
193 char[] s2;
194 s2.reserve(str.length*4);
195 foreach (char ch; str) {
196 char[4] buf = void;
197 auto len = utf8Encode(buf[], cast(dchar)ch);
198 assert(len > 0);
199 s2 ~= buf[0..len];
201 delete str;
202 *deststr = cast(string)s2; // it is safe to cast here
203 } else {
204 if (tagsize < 2) goto skiptag; // no room for BOM
205 //$FF FE or $FE FF
206 auto osize = wholesize;
207 ubyte b0 = getUInt!ubyte;
208 ubyte b1 = getUInt!ubyte;
209 bool bige;
210 bool utf8 = false;
211 if (osize-wholesize > tagsize) throw new Exception("invalid ID3v2 text content");
212 if (b0 == 0xff) {
213 if (b1 != 0xfe) throw new Exception("invalid ID3v2 text content");
214 bige = false;
215 } else if (b0 == 0xfe) {
216 if (b1 != 0xff) throw new Exception("invalid ID3v2 text content");
217 bige = true;
218 } else if (b0 == 0xef) {
219 if (b1 != 0xbb) throw new Exception("invalid ID3v2 text content");
220 if (tagsize < 3) throw new Exception("invalid ID3v2 text content");
221 b1 = getUInt!ubyte;
222 if (b1 != 0xbf) throw new Exception("invalid ID3v2 text content");
223 // utf-8 (just in case)
224 utf8 = true;
226 char[4] buf = void;
227 char[] str;
228 str.reserve(tagsize);
229 while (osize-wholesize < tagsize) {
230 if (!utf8) {
231 b0 = getUInt!ubyte;
232 b1 = getUInt!ubyte;
233 dchar dch = cast(dchar)(bige ? b0*256+b1 : b1*256+b0);
234 if (dch > dchar.max) dch = '\uFFFD';
235 auto len = utf8Encode(buf[], dch);
236 assert(len > 0);
237 str ~= buf[0..len];
238 } else {
239 str ~= cast(char)getUInt!ubyte;
242 if (osize-wholesize > tagsize) throw new Exception("invalid ID3v2 text content");
243 foreach (immutable cidx, char ch; str) if (ch == 0) { str = str[0..cidx]; break; }
244 *deststr = cast(string)str; // it is safe to cast here
246 continue;
248 skiptag:
249 wholesize -= tagsize;
250 foreach (immutable _; 0..tagsize) getByte();
253 return true;