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