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