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