Sanitized vcedit_write() (and related) code some more.
[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, 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
224         oggpack_write (&opb, 1, 1);
225
226         packet->bytes = oggpack_bytes (&opb);
227
228         packet->packet = _ogg_malloc (packet->bytes);
229         if (!packet->packet)
230                 return false;
231
232         memcpy (packet->packet, opb.buffer, packet->bytes);
233
234         oggpack_writeclear (&opb);
235
236         return true;
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         if (result == 1)
262                 return 1;
263
264         if (s->eos)
265                 return 0;
266
267         while (ogg_sync_pageout (&s->oy, page) != 1) {
268                 buffer = ogg_sync_buffer (&s->oy, CHUNKSIZE);
269                 bytes = fread (buffer, 1, CHUNKSIZE, s->in);
270                 ogg_sync_wrote (&s->oy, bytes);
271
272                 if (!bytes && (feof (s->in) || ferror (s->in)))
273                         return 0;
274         }
275
276         if (ogg_page_eos (page))
277                 s->eos = true;
278         else if (ogg_page_serialno (page) != s->serial) {
279                 s->eos = true;
280                 s->extra_page = true;
281
282                 return 0;
283         }
284
285         ogg_stream_pagein (&s->os, page);
286
287         return _fetch_next_packet (s, p, page);
288 }
289
290 vcedit_error
291 vcedit_open (vcedit_state *s)
292 {
293         vcedit_error ret;
294         ogg_packet packet_comments, *header = &packet_comments;
295         ogg_page page;
296         struct stat st;
297         char *buffer;
298         size_t bytes, total = 0;
299         int i = 0;
300
301         s->in = fopen (s->filename, "rb");
302         if (!s->in)
303                 return VCEDIT_ERR_OPEN;
304
305         s->file_mode = stat (s->filename, &st) ? 0664 : st.st_mode;
306
307         ogg_sync_init (&s->oy);
308
309         do {
310                 /* Bail if we don't find data in the first 40 kB */
311                 if (feof (s->in) || ferror (s->in) || total >= (CHUNKSIZE * 10)) {
312                         ogg_sync_clear (&s->oy);
313
314                         return VCEDIT_ERR_INVAL;
315                 }
316
317                 buffer = ogg_sync_buffer (&s->oy, CHUNKSIZE);
318
319                 bytes = fread (buffer, 1, CHUNKSIZE, s->in);
320                 total += bytes;
321
322                 ogg_sync_wrote (&s->oy, bytes);
323         } while (ogg_sync_pageout (&s->oy, &page) != 1);
324
325         s->serial = ogg_page_serialno (&page);
326
327         ogg_stream_init (&s->os, s->serial);
328         vorbis_info_init (&s->vi);
329         vorbis_comment_init (&s->vc);
330
331         if (ogg_stream_pagein (&s->os, &page) < 0) {
332                 ret = VCEDIT_ERR_INVAL;
333                 goto err;
334         }
335
336         if (ogg_stream_packetout (&s->os, &s->packet_main) != 1) {
337                 ret = VCEDIT_ERR_INVAL;
338                 goto err;
339         }
340
341         if (!ogg_packet_dup_data (&s->packet_main)) {
342                 s->packet_main.packet = NULL;
343                 ret = VCEDIT_ERR_INVAL;
344                 goto err;
345         }
346
347         if (vorbis_synthesis_headerin (&s->vi, &s->vc, &s->packet_main) < 0) {
348                 ret = VCEDIT_ERR_INVAL;
349                 goto err;
350         }
351
352         ogg_packet_init (&packet_comments, NULL, 0);
353
354         while (i < 2) {
355                 if (feof (s->in) || ferror (s->in)) {
356                         ret = VCEDIT_ERR_INVAL;
357                         goto err;
358                 }
359
360                 while (i < 2) {
361                         int result;
362
363                         result = ogg_sync_pageout (&s->oy, &page);
364                         if (!result)
365                                 break; /* Too little data so far */
366
367                         if (result != 1)
368                                 continue;
369
370                         ogg_stream_pagein (&s->os, &page);
371
372                         while (i < 2) {
373                                 result = ogg_stream_packetout (&s->os, header);
374                                 if (!result)
375                                         break;
376
377                                 if (result != 1) {
378                                         ret = VCEDIT_ERR_INVAL;
379                                         goto err;
380                                 }
381
382                                 if (i++ == 1 && !ogg_packet_dup_data (header)) {
383                                         header->packet = NULL;
384                                         ret = VCEDIT_ERR_INVAL;
385                                         goto err;
386                                 }
387
388                                 vorbis_synthesis_headerin (&s->vi, &s->vc, header);
389
390                                 header = &s->packet_code_books;
391                         }
392                 }
393
394                 buffer = ogg_sync_buffer (&s->oy, CHUNKSIZE);
395
396                 bytes = fread (buffer, 1, CHUNKSIZE, s->in);
397                 ogg_sync_wrote (&s->oy, bytes);
398         }
399
400         /* Copy the vendor tag */
401         s->vendor = strdup (s->vc.vendor);
402
403         /* Headers are done! */
404         s->opened = true;
405
406         return VCEDIT_ERR_SUCCESS;
407
408 err:
409         vcedit_clear_internals (s);
410
411         return ret;
412 }
413
414 static bool
415 write_data (const void *buf, size_t size, size_t nmemb, FILE *stream)
416 {
417         while (nmemb > 0) {
418                 size_t w;
419
420                 w = fwrite (buf, size, nmemb, stream);
421                 if (!w && ferror (stream))
422                         return false;
423
424                 nmemb -= w;
425                 buf += size * w;
426         }
427
428         return true;
429 }
430
431 static bool
432 write_page (FILE *f, ogg_page *p)
433 {
434         return write_data (p->header, 1, p->header_len, f) &&
435                write_data (p->body, 1, p->body_len, f);
436 }
437
438 vcedit_error
439 vcedit_write (vcedit_state *s)
440 {
441         ogg_stream_state stream;
442         ogg_packet packet;
443         ogg_page page_out, page_in;
444         ogg_int64_t granpos = 0;
445         FILE *out;
446         char *buffer, tmpfile[PATH_MAX];
447         bool success = false, need_flush = false, need_out = false;
448         int fd, result, bytes;
449
450         if (!s->opened)
451                 return VCEDIT_ERR_INVAL;
452
453         strcpy (tmpfile, s->filename);
454         strcat (tmpfile, ".XXXXXX");
455
456         fd = mkstemp (tmpfile);
457         if (fd == -1)
458                 return VCEDIT_ERR_TMPFILE;
459
460         out = fdopen (fd, "wb");
461         if (!out) {
462                 unlink (tmpfile);
463                 close (fd);
464
465                 return VCEDIT_ERR_TMPFILE;
466         }
467
468         s->prevW = 0;
469         s->extra_page = s->eos = false;
470
471         ogg_stream_init (&stream, s->serial);
472
473         /* write "main" packet */
474         s->packet_main.b_o_s = 1;
475         ogg_stream_packetin (&stream, &s->packet_main);
476         s->packet_main.b_o_s = 0;
477
478         /* prepare and write comments */
479         if (!write_comments (s, &packet)) {
480                 ogg_stream_clear (&stream);
481                 unlink (tmpfile);
482                 fclose (out);
483
484                 return VCEDIT_ERR_INVAL;
485         }
486
487         ogg_stream_packetin (&stream, &packet);
488         ogg_packet_clear (&packet);
489
490         /* write codebooks */
491         ogg_stream_packetin (&stream, &s->packet_code_books);
492
493         while (ogg_stream_flush (&stream, &page_out))
494                 if (!write_page (out, &page_out))
495                         goto cleanup;
496
497         while (_fetch_next_packet (s, &packet, &page_in)) {
498                 bool write = false;
499                 int size;
500
501                 size = _blocksize (s, &packet);
502                 granpos += size;
503
504                 if (need_flush) {
505                         write = ogg_stream_flush (&stream, &page_out);
506                 } else if (need_out) {
507                         write = ogg_stream_pageout (&stream, &page_out);
508                 }
509
510                 if (write && !write_page (out, &page_out))
511                         goto cleanup;
512
513                 need_flush = need_out = false;
514
515                 if (packet.granulepos == -1) {
516                         packet.granulepos = granpos;
517                         ogg_stream_packetin (&stream, &packet);
518                 } else {
519                         /* granulepos is set, validly. Use it, and force a flush to
520                          * account for shortened blocks (vcut) when appropriate
521                          */
522                         if (granpos > packet.granulepos) {
523                                 granpos = packet.granulepos;
524                                 ogg_stream_packetin (&stream, &packet);
525                                 need_flush = true;
526                         } else {
527                                 ogg_stream_packetin (&stream, &packet);
528                                 need_out = true;
529                         }
530                 }
531         }
532
533         stream.e_o_s = 1;
534
535         while (ogg_stream_flush (&stream, &page_out))
536                 if (!write_page (out, &page_out))
537                         goto cleanup;
538
539         if (s->extra_page && !write_page (out, &page_in))
540                 goto cleanup;
541
542         /* clear it, because not all paths to here do */
543         s->eos = false;
544
545         do {
546                 /* We copy the rest of the stream (other logical streams)
547                  * through, a page at a time.
548                  */
549                 while ((result = ogg_sync_pageout (&s->oy, &page_out))) {
550                         /* Don't bother going through the rest, we can just
551                          * write the page out now
552                          */
553                         if (result == 1 && !write_page (out, &page_out))
554                                 goto cleanup;
555                 }
556
557                 buffer = ogg_sync_buffer (&s->oy, CHUNKSIZE);
558                 bytes = fread (buffer, 1, CHUNKSIZE, s->in);
559                 ogg_sync_wrote (&s->oy, bytes);
560
561                 if (ferror (s->in))
562                         goto cleanup;
563         } while (bytes || !feof (s->in));
564
565         s->eos = success = true;
566
567 cleanup:
568         fclose (s->in);
569
570         if (!success) {
571                 unlink (tmpfile);
572                 fclose (out);
573         } else {
574                 fclose (out);
575                 unlink (s->filename);
576                 rename (tmpfile, s->filename);
577                 chmod (s->filename, s->file_mode);
578         }
579
580         ogg_stream_clear (&stream);
581
582         if (!s->eos)
583                 return VCEDIT_ERR_INVAL;
584
585         vcedit_clear_internals (s);
586
587         return (vcedit_open (s) == VCEDIT_ERR_SUCCESS) ?
588                VCEDIT_ERR_SUCCESS : VCEDIT_ERR_REOPEN;
589 }