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;
24 version(id3v2_debug
) import iv
.vfs
.io
;
27 // ////////////////////////////////////////////////////////////////////////// //
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
) {
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)
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
);
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
);
71 // shift bytes we want to keep
72 import core
.stdc
.string
: memmove
;
73 memmove(rdbuf
.ptr
, rdbuf
.ptr
+xmpos
, bytesToLeft
);
82 if (!fillBuffer()) throw new Exception("out of ID3v2 data");
83 return rdbuf
.ptr
[rbpos
++];
90 fillBuffer(); // initial fill
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) {
98 if (flags
&0b11111) break;
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];
111 static if (!doscan
) {
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");
131 foreach (immutable n
; 0..T
.sizeof
) {
134 if (lastByteWasFF
&& b
== 0) {
135 if (wholesize
< 1) throw new Exception("out of ID3v2 data");
139 lastByteWasFF
= (b
== 0xff);
147 // skip extended header
149 uint ehsize
= getUInt
!uint;
150 while (ehsize
-- > 0) getUInt
!ubyte;
154 readloop
: while (wholesize
>= 8) {
156 foreach (ref char ch
; tag
[]) {
157 ch
= cast(char)getByte
;
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') {
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
182 ubyte encoding
= getUInt
!ubyte; // 0: iso-8859-1; 1: unicode
183 if (encoding
> 1) throw new Exception("invalid ID3v2 text encoding");
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; }
194 s2
.reserve(str.length
*4);
195 foreach (char ch
; str) {
197 auto len
= utf8Encode(buf
[], cast(dchar)ch
);
202 *deststr
= cast(string
)s2
; // it is safe to cast here
204 if (tagsize
< 2) goto skiptag
; // no room for BOM
206 auto osize
= wholesize
;
207 ubyte b0
= getUInt
!ubyte;
208 ubyte b1
= getUInt
!ubyte;
211 if (osize
-wholesize
> tagsize
) throw new Exception("invalid ID3v2 text content");
213 if (b1
!= 0xfe) throw new Exception("invalid ID3v2 text content");
215 } else if (b0
== 0xfe) {
216 if (b1
!= 0xff) throw new Exception("invalid ID3v2 text content");
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");
222 if (b1
!= 0xbf) throw new Exception("invalid ID3v2 text content");
223 // utf-8 (just in case)
228 str.reserve(tagsize
);
229 while (osize
-wholesize
< tagsize
) {
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
);
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
249 wholesize
-= tagsize
;
250 foreach (immutable _
; 0..tagsize
) getByte();