Use a flexiable array to store the filename.
[ruby-vorbistagger.git] / ext / vcedit.c
1 /*
2  * Copyright (C) 2000-2001 Michael Smith (msmith at xiph org)
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation, version 2.
7  *
8  * This library 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 GNU
11  * Lesser General Public License for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public
14  * License along with this library; if not, write to the Free Software
15  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
16  * MA 02110-1301 USA
17  */
18
19 #include <stdio.h>
20 #include <stdbool.h>
21 #include <stdlib.h>
22 #include <string.h>
23 #include <errno.h>
24 #include <limits.h>
25 #include <unistd.h>
26 #include <ogg/ogg.h>
27 #include <vorbis/codec.h>
28 #include <assert.h>
29
30 #include "vcedit.h"
31
32 #define CHUNKSIZE 4096
33
34 struct vcedit_state_St {
35         int refcount;
36
37         ogg_sync_state oy;
38         ogg_stream_state os;
39
40         vorbis_comment vc;
41         vorbis_info vi;
42
43         FILE *in;
44         bool opened;
45         long serial;
46         unsigned char *mainbuf;
47         unsigned char *bookbuf;
48         int     mainlen;
49         int     booklen;
50         char *vendor;
51         int prevW;
52         int extrapage;
53         int eosin;
54
55         char filename[0];
56 };
57
58 static void
59 vcedit_state_free (vcedit_state *state)
60 {
61         free (state->mainbuf);
62         free (state->bookbuf);
63         free (state->vendor);
64
65         if (state->in) {
66                 fclose (state->in);
67                 state->in = NULL;
68         }
69
70         free (state);
71 }
72
73 static bool
74 vcedit_state_init (vcedit_state *state)
75 {
76         state->refcount = 1;
77
78         return true;
79 }
80
81 vcedit_state *
82 vcedit_state_new (const char *filename)
83 {
84         vcedit_state *state;
85         size_t len;
86
87         len = strlen (filename);
88
89         state = malloc (sizeof (vcedit_state) + len + 1);
90         if (!state)
91                 return NULL;
92
93         memset (state, 0, sizeof (vcedit_state));
94
95         if (!vcedit_state_init (state)) {
96                 vcedit_state_free (state);
97                 return NULL;
98         }
99
100         strcpy (state->filename, filename);
101
102         return state;
103 }
104
105 vorbis_comment *
106 vcedit_comments (vcedit_state *state)
107 {
108         return state->opened ? &state->vc : NULL;
109 }
110
111 static void
112 vcedit_clear_internals (vcedit_state *state)
113 {
114         ogg_stream_clear (&state->os);
115         ogg_sync_clear (&state->oy);
116
117         vorbis_info_clear (&state->vi);
118         vorbis_comment_clear (&state->vc);
119
120         free (state->vendor);
121         state->vendor = NULL;
122
123         free (state->mainbuf);
124         state->mainbuf = NULL;
125         state->mainlen = 0;
126
127         free (state->bookbuf);
128         state->bookbuf = NULL;
129         state->booklen = 0;
130
131         state->serial = 0;
132         state->opened = false;
133 }
134
135 void
136 vcedit_state_ref (vcedit_state *state)
137 {
138         state->refcount++;
139 }
140
141 void
142 vcedit_state_unref (vcedit_state *state)
143 {
144         if (--state->refcount)
145                 return;
146
147         if (state->opened)
148                 vcedit_clear_internals (state);
149
150         vcedit_state_free (state);
151 }
152
153 /* Next two functions pulled straight from libvorbis, apart from one change
154  * - we don't want to overwrite the vendor string.
155  */
156 static void
157 _v_writestring (oggpack_buffer *o, char *s, int len)
158 {
159         while (len--) {
160                 oggpack_write (o, *s++, 8);
161         }
162 }
163
164 static int
165 _commentheader_out (vorbis_comment *vc, char *vendor, ogg_packet *op)
166 {
167         int i;
168
169         oggpack_buffer opb;
170
171         oggpack_writeinit (&opb);
172
173         /* preamble */
174         oggpack_write (&opb, 0x03, 8);
175         _v_writestring (&opb, "vorbis", 6);
176
177         /* vendor */
178         oggpack_write (&opb, strlen (vendor), 32);
179         _v_writestring (&opb, vendor, strlen (vendor));
180
181         /* comments */
182         oggpack_write (&opb, vc->comments, 32);
183
184         for (i = 0; i < vc->comments; i++) {
185                 if (!vc->user_comments[i])
186                         oggpack_write (&opb, 0, 32);
187                 else {
188                         oggpack_write (&opb, vc->comment_lengths[i], 32);
189                         _v_writestring (&opb, vc->user_comments[i],
190                                         vc->comment_lengths[i]);
191                 }
192         }
193
194         oggpack_write (&opb, 1, 1);
195
196         op->packet = _ogg_malloc (oggpack_bytes (&opb));
197         memcpy (op->packet, opb.buffer, oggpack_bytes (&opb));
198
199         op->bytes = oggpack_bytes (&opb);
200         op->b_o_s = 0;
201         op->e_o_s = 0;
202         op->granulepos = 0;
203
204         oggpack_writeclear (&opb);
205
206         return 0;
207 }
208
209 static int
210 _blocksize (vcedit_state *s, ogg_packet *p)
211 {
212         int this, ret = 0;
213
214         this = vorbis_packet_blocksize (&s->vi, p);
215
216         if (s->prevW)
217                 ret = (this + s->prevW) / 4;
218
219         s->prevW = this;
220
221         return ret;
222 }
223
224 static int
225 _fetch_next_packet (vcedit_state *s, ogg_packet *p, ogg_page *page)
226 {
227         char *buffer;
228         int result, bytes;
229
230         result = ogg_stream_packetout (&s->os, p);
231
232         if (result > 0)
233                 return 1;
234
235         if (s->eosin)
236                 return 0;
237
238         while (ogg_sync_pageout (&s->oy, page) <= 0) {
239                 buffer = ogg_sync_buffer (&s->oy, CHUNKSIZE);
240                 bytes = fread (buffer, 1, CHUNKSIZE, s->in);
241                 ogg_sync_wrote (&s->oy, bytes);
242
243                 if (!bytes)
244                         return 0;
245         }
246
247         if (ogg_page_eos (page))
248                 s->eosin = 1;
249         else if (ogg_page_serialno (page) != s->serial) {
250                 s->eosin = 1;
251                 s->extrapage = 1;
252                 return 0;
253         }
254
255         ogg_stream_pagein (&s->os, page);
256
257         return _fetch_next_packet (s, p, page);
258 }
259
260 vcedit_error
261 vcedit_open (vcedit_state *state)
262 {
263         vcedit_error ret;
264         char *buffer;
265         int bytes, i;
266         int chunks = 0;
267         ogg_packet *header;
268         ogg_packet header_main, header_comments, header_codebooks;
269         ogg_page og;
270
271         state->in = fopen (state->filename, "rb");
272         if (!state->in)
273                 return VCEDIT_ERR_OPEN;
274
275         ogg_sync_init (&state->oy);
276
277         while (1) {
278                 buffer = ogg_sync_buffer (&state->oy, CHUNKSIZE);
279                 bytes = fread (buffer, 1, CHUNKSIZE, state->in);
280
281                 ogg_sync_wrote (&state->oy, bytes);
282
283                 if (ogg_sync_pageout (&state->oy, &og) == 1)
284                         break;
285
286                 /* Bail if we don't find data in the first 40 kB */
287                 if (chunks++ >= 10) {
288                         ogg_sync_clear (&state->oy);
289
290                         return VCEDIT_ERR_INVAL;
291                 }
292         }
293
294         state->serial = ogg_page_serialno (&og);
295
296         ogg_stream_init (&state->os, state->serial);
297         vorbis_info_init (&state->vi);
298         vorbis_comment_init (&state->vc);
299
300         if (ogg_stream_pagein (&state->os, &og) < 0) {
301                 ret = VCEDIT_ERR_INVAL;
302                 goto err;
303         }
304
305         if (ogg_stream_packetout (&state->os, &header_main) != 1) {
306                 ret = VCEDIT_ERR_INVAL;
307                 goto err;
308         }
309
310         if (vorbis_synthesis_headerin (&state->vi, &state->vc, &header_main) < 0) {
311                 ret = VCEDIT_ERR_INVAL;
312                 goto err;
313         }
314
315         state->mainlen = header_main.bytes;
316         state->mainbuf = malloc (state->mainlen);
317         memcpy (state->mainbuf, header_main.packet, header_main.bytes);
318
319         i = 0;
320         header = &header_comments;
321
322         while (i < 2) {
323                 while (i < 2) {
324                         int result = ogg_sync_pageout (&state->oy, &og);
325
326                         if (!result)
327                                 break; /* Too little data so far */
328
329                         if (result == 1) {
330                                 ogg_stream_pagein (&state->os, &og);
331
332                                 while (i < 2) {
333                                         result = ogg_stream_packetout (&state->os, header);
334
335                                         if (!result)
336                                                 break;
337
338                                         if (result == -1) {
339                                                 ret = VCEDIT_ERR_INVAL;
340                                                 goto err;
341                                         }
342
343                                         vorbis_synthesis_headerin (&state->vi, &state->vc, header);
344
345                                         if (i == 1) {
346                                                 state->booklen = header->bytes;
347                                                 state->bookbuf = malloc (state->booklen);
348                                                 memcpy (state->bookbuf, header->packet, header->bytes);
349                                         }
350
351                                         i++;
352                                         header = &header_codebooks;
353                                 }
354                         }
355                 }
356
357                 buffer = ogg_sync_buffer (&state->oy, CHUNKSIZE);
358                 bytes = fread (buffer, 1, CHUNKSIZE, state->in);
359
360                 if (bytes == 0 && i < 2) {
361                         ret = VCEDIT_ERR_INVAL;
362                         goto err;
363                 }
364
365                 ogg_sync_wrote (&state->oy, bytes);
366         }
367
368         /* Copy the vendor tag */
369         state->vendor = strdup (state->vc.vendor);
370
371         /* Headers are done! */
372         state->opened = true;
373
374         return VCEDIT_ERR_SUCCESS;
375
376 err:
377         vcedit_clear_internals (state);
378
379         return ret;
380 }
381
382 vcedit_error
383 vcedit_write (vcedit_state *state)
384 {
385         ogg_stream_state streamout;
386         ogg_packet header_main, header_comments, header_codebooks, op;
387         ogg_page ogout, ogin;
388         ogg_int64_t granpos = 0;
389         FILE *out;
390         char *buffer, tmpfile[PATH_MAX];
391         int s, result, bytes, needflush = 0, needout = 0;
392         size_t tmp;
393
394         if (!state->opened)
395                 return VCEDIT_ERR_INVAL;
396
397         strcpy (tmpfile, state->filename);
398         strcat (tmpfile, ".XXXXXX");
399
400         s = mkstemp (tmpfile);
401         if (s == -1)
402                 return VCEDIT_ERR_TMPFILE;
403
404         out = fdopen (s, "wb");
405         if (!out) {
406                 unlink (tmpfile);
407                 close (s);
408
409                 return VCEDIT_ERR_TMPFILE;
410         }
411
412         state->prevW = state->extrapage = state->eosin = 0;
413
414         header_main.bytes = state->mainlen;
415         header_main.packet = state->mainbuf;
416         header_main.b_o_s = 1;
417         header_main.e_o_s = 0;
418         header_main.granulepos = 0;
419
420         header_codebooks.bytes = state->booklen;
421         header_codebooks.packet = state->bookbuf;
422         header_codebooks.b_o_s = 0;
423         header_codebooks.e_o_s = 0;
424         header_codebooks.granulepos = 0;
425
426         ogg_stream_init (&streamout, state->serial);
427
428         _commentheader_out (&state->vc, state->vendor, &header_comments);
429
430         ogg_stream_packetin (&streamout, &header_main);
431         ogg_stream_packetin (&streamout, &header_comments);
432         ogg_stream_packetin (&streamout, &header_codebooks);
433
434         while ((result = ogg_stream_flush (&streamout, &ogout))) {
435                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
436                 if (tmp != (size_t) ogout.header_len)
437                         goto cleanup;
438
439                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
440                 if (tmp != (size_t) ogout.body_len)
441                         goto cleanup;
442         }
443
444         while (_fetch_next_packet (state, &op, &ogin)) {
445                 int size;
446
447                 size = _blocksize (state, &op);
448                 granpos += size;
449
450                 if (needflush) {
451                         if (ogg_stream_flush (&streamout, &ogout)) {
452                                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
453                                 if (tmp != (size_t) ogout.header_len)
454                                         goto cleanup;
455
456                                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
457                                 if (tmp != (size_t) ogout.body_len)
458                                         goto cleanup;
459                         }
460                 } else if (needout) {
461                         if (ogg_stream_pageout (&streamout, &ogout)) {
462                                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
463                                 if (tmp != (size_t) ogout.header_len)
464                                         goto cleanup;
465
466                                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
467                                 if (tmp != (size_t) ogout.body_len)
468                                         goto cleanup;
469                         }
470                 }
471
472                 needflush = needout = 0;
473
474                 if (op.granulepos == -1) {
475                         op.granulepos = granpos;
476                         ogg_stream_packetin (&streamout, &op);
477                 } else {
478                         /* granulepos is set, validly. Use it, and force a flush to
479                          * account for shortened blocks (vcut) when appropriate
480                          */
481                         if (granpos > op.granulepos) {
482                                 granpos = op.granulepos;
483                                 ogg_stream_packetin (&streamout, &op);
484                                 needflush = 1;
485                         } else {
486                                 ogg_stream_packetin (&streamout, &op);
487                                 needout = 1;
488                         }
489                 }
490         }
491
492         streamout.e_o_s = 1;
493
494         while (ogg_stream_flush (&streamout, &ogout)) {
495                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
496                 if (tmp != (size_t) ogout.header_len)
497                         goto cleanup;
498
499                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
500                 if (tmp != (size_t) ogout.body_len)
501                         goto cleanup;
502         }
503
504         if (state->extrapage) {
505                 tmp = fwrite (ogin.header, 1, ogin.header_len, out);
506                 if (tmp != (size_t) ogin.header_len)
507                         goto cleanup;
508
509                 tmp = fwrite (ogin.body, 1, ogin.body_len, out);
510                 if (tmp != (size_t) ogin.body_len)
511                         goto cleanup;
512         }
513
514         /* clear it, because not all paths to here do */
515         state->eosin = 0;
516
517         while (!state->eosin) { /* We reached eos, not eof */
518                 /* We copy the rest of the stream (other logical streams)
519                  * through, a page at a time.
520                  */
521                 while (1) {
522                         result = ogg_sync_pageout (&state->oy, &ogout);
523
524                         if (!result)
525                 break;
526
527                         if (result >= 0) {
528                                 /* Don't bother going through the rest, we can just
529                                  * write the page out now
530                                  */
531                                 tmp = fwrite (ogout.header, 1, ogout.header_len, out);
532                                 if (tmp != (size_t) ogout.header_len)
533                                         goto cleanup;
534
535                                 tmp = fwrite (ogout.body, 1, ogout.body_len, out);
536                                 if (tmp != (size_t) ogout.body_len)
537                                         goto cleanup;
538                         }
539                 }
540
541                 buffer = ogg_sync_buffer (&state->oy, CHUNKSIZE);
542                 bytes = fread (buffer, 1, CHUNKSIZE, state->in);
543                 ogg_sync_wrote (&state->oy, bytes);
544
545                 if (!bytes) {
546                         state->eosin = 1;
547                         break;
548                 }
549         }
550
551         fclose (out);
552         fclose (state->in);
553
554         unlink (state->filename);
555         rename (tmpfile, state->filename);
556
557 cleanup:
558         ogg_stream_clear (&streamout);
559
560     /* We don't ogg_packet_clear() this, because the memory was
561          * allocated in _commentheader_out(), so we mirror that here
562          */
563     _ogg_free (header_comments.packet);
564
565         free (state->mainbuf);
566         free (state->bookbuf);
567
568     state->mainbuf = state->bookbuf = NULL;
569
570         if (!state->eosin)
571                 return VCEDIT_ERR_INVAL;
572
573         vcedit_clear_internals (state);
574
575         return (vcedit_open (state) == VCEDIT_ERR_SUCCESS) ?
576                VCEDIT_ERR_SUCCESS : VCEDIT_ERR_REOPEN;
577 }