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