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