Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * astreamer_tar.c
4 : : *
5 : : * This module implements three types of tar processing. A tar parser
6 : : * expects unlabelled chunks of data (e.g. ASTREAMER_UNKNOWN) and splits
7 : : * it into labelled chunks (any other value of astreamer_archive_context).
8 : : * A tar archiver does the reverse: it takes a bunch of labelled chunks
9 : : * and produces a tarfile, optionally replacing member headers and trailers
10 : : * so that upstream astreamer objects can perform surgery on the tarfile
11 : : * contents without knowing the details of the tar format. A tar terminator
12 : : * just adds two blocks of NUL bytes to the end of the file, since older
13 : : * server versions produce files with this terminator omitted.
14 : : *
15 : : * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
16 : : *
17 : : * IDENTIFICATION
18 : : * src/fe_utils/astreamer_tar.c
19 : : *-------------------------------------------------------------------------
20 : : */
21 : :
22 : : #include "postgres_fe.h"
23 : :
24 : : #include <time.h>
25 : :
26 : : #include "common/logging.h"
27 : : #include "fe_utils/astreamer.h"
28 : : #include "pgtar.h"
29 : :
30 : : typedef struct astreamer_tar_parser
31 : : {
32 : : astreamer base;
33 : : astreamer_archive_context next_context;
34 : : astreamer_member member;
35 : : size_t file_bytes_sent;
36 : : size_t pad_bytes_expected;
37 : : } astreamer_tar_parser;
38 : :
39 : : typedef struct astreamer_tar_archiver
40 : : {
41 : : astreamer base;
42 : : bool rearchive_member;
43 : : } astreamer_tar_archiver;
44 : :
45 : : static void astreamer_tar_parser_content(astreamer *streamer,
46 : : astreamer_member *member,
47 : : const char *data, int len,
48 : : astreamer_archive_context context);
49 : : static void astreamer_tar_parser_finalize(astreamer *streamer);
50 : : static void astreamer_tar_parser_free(astreamer *streamer);
51 : : static bool astreamer_tar_header(astreamer_tar_parser *mystreamer);
52 : :
53 : : static const astreamer_ops astreamer_tar_parser_ops = {
54 : : .content = astreamer_tar_parser_content,
55 : : .finalize = astreamer_tar_parser_finalize,
56 : : .free = astreamer_tar_parser_free
57 : : };
58 : :
59 : : static void astreamer_tar_archiver_content(astreamer *streamer,
60 : : astreamer_member *member,
61 : : const char *data, int len,
62 : : astreamer_archive_context context);
63 : : static void astreamer_tar_archiver_finalize(astreamer *streamer);
64 : : static void astreamer_tar_archiver_free(astreamer *streamer);
65 : :
66 : : static const astreamer_ops astreamer_tar_archiver_ops = {
67 : : .content = astreamer_tar_archiver_content,
68 : : .finalize = astreamer_tar_archiver_finalize,
69 : : .free = astreamer_tar_archiver_free
70 : : };
71 : :
72 : : static void astreamer_tar_terminator_content(astreamer *streamer,
73 : : astreamer_member *member,
74 : : const char *data, int len,
75 : : astreamer_archive_context context);
76 : : static void astreamer_tar_terminator_finalize(astreamer *streamer);
77 : : static void astreamer_tar_terminator_free(astreamer *streamer);
78 : :
79 : : static const astreamer_ops astreamer_tar_terminator_ops = {
80 : : .content = astreamer_tar_terminator_content,
81 : : .finalize = astreamer_tar_terminator_finalize,
82 : : .free = astreamer_tar_terminator_free
83 : : };
84 : :
85 : : /*
86 : : * Create a astreamer that can parse a stream of content as tar data.
87 : : *
88 : : * The input should be a series of ASTREAMER_UNKNOWN chunks; the astreamer
89 : : * specified by 'next' will receive a series of typed chunks, as per the
90 : : * conventions described in astreamer.h.
91 : : */
92 : : astreamer *
663 rhaas@postgresql.org 93 :CBC 279 : astreamer_tar_parser_new(astreamer *next)
94 : : {
95 : : astreamer_tar_parser *streamer;
96 : :
172 michael@paquier.xyz 97 :GNC 279 : streamer = palloc0_object(astreamer_tar_parser);
663 rhaas@postgresql.org 98 :CBC 279 : *((const astreamer_ops **) &streamer->base.bbs_ops) =
99 : : &astreamer_tar_parser_ops;
1667 100 : 279 : streamer->base.bbs_next = next;
101 : 279 : initStringInfo(&streamer->base.bbs_buffer);
663 102 : 279 : streamer->next_context = ASTREAMER_MEMBER_HEADER;
103 : :
1667 104 : 279 : return &streamer->base;
105 : : }
106 : :
107 : : /*
108 : : * Parse unknown content as tar data.
109 : : */
110 : : static void
663 111 : 369469 : astreamer_tar_parser_content(astreamer *streamer, astreamer_member *member,
112 : : const char *data, int len,
113 : : astreamer_archive_context context)
114 : : {
115 : 369469 : astreamer_tar_parser *mystreamer = (astreamer_tar_parser *) streamer;
116 : : size_t nbytes;
117 : :
118 : : /* Expect unparsed input. */
1667 119 [ - + ]: 369469 : Assert(member == NULL);
663 120 [ - + ]: 369469 : Assert(context == ASTREAMER_UNKNOWN);
121 : :
1667 122 [ + + ]: 825103 : while (len > 0)
123 : : {
124 [ + + + + : 455854 : switch (mystreamer->next_context)
- ]
125 : : {
663 126 : 207244 : case ASTREAMER_MEMBER_HEADER:
127 : :
128 : : /*
129 : : * If we're expecting an archive member header, accumulate a
130 : : * full block of data before doing anything further.
131 : : */
132 [ - + ]: 207244 : if (!astreamer_buffer_until(streamer, &data, &len,
133 : : TAR_BLOCK_SIZE))
1667 rhaas@postgresql.org 134 :UBC 0 : return;
135 : :
136 : : /*
137 : : * Now we can process the header and get ready to process the
138 : : * file contents; however, we might find out that what we
139 : : * thought was the next file header is actually the start of
140 : : * the archive trailer. Switch modes accordingly.
141 : : */
663 rhaas@postgresql.org 142 [ + + ]:CBC 207244 : if (astreamer_tar_header(mystreamer))
143 : : {
1667 144 [ + + ]: 207025 : if (mystreamer->member.size == 0)
145 : : {
146 : : /* No content; trailer is zero-length. */
663 147 : 42490 : astreamer_content(mystreamer->base.bbs_next,
148 : : &mystreamer->member,
149 : : NULL, 0,
150 : : ASTREAMER_MEMBER_TRAILER);
151 : :
152 : : /* Expect next header. */
153 : 42490 : mystreamer->next_context = ASTREAMER_MEMBER_HEADER;
154 : : }
155 : : else
156 : : {
157 : : /* Expect contents. */
158 : 164535 : mystreamer->next_context = ASTREAMER_MEMBER_CONTENTS;
159 : : }
1667 160 : 207025 : mystreamer->base.bbs_buffer.len = 0;
161 : 207025 : mystreamer->file_bytes_sent = 0;
162 : : }
163 : : else
663 164 : 219 : mystreamer->next_context = ASTREAMER_ARCHIVE_TRAILER;
1667 165 : 207244 : break;
166 : :
663 167 : 237691 : case ASTREAMER_MEMBER_CONTENTS:
168 : :
169 : : /*
170 : : * Send as much content as we have, but not more than the
171 : : * remaining file length.
172 : : */
1667 173 [ - + ]: 237691 : Assert(mystreamer->file_bytes_sent < mystreamer->member.size);
174 : 237691 : nbytes = mystreamer->member.size - mystreamer->file_bytes_sent;
175 : 237691 : nbytes = Min(nbytes, len);
176 [ - + ]: 237691 : Assert(nbytes > 0);
663 177 : 237691 : astreamer_content(mystreamer->base.bbs_next,
178 : : &mystreamer->member,
179 : : data, nbytes,
180 : : ASTREAMER_MEMBER_CONTENTS);
1667 181 : 237691 : mystreamer->file_bytes_sent += nbytes;
182 : 237691 : data += nbytes;
183 : 237691 : len -= nbytes;
184 : :
185 : : /*
186 : : * If we've not yet sent the whole file, then there's more
187 : : * content to come; otherwise, it's time to expect the file
188 : : * trailer.
189 : : */
190 [ - + ]: 237691 : Assert(mystreamer->file_bytes_sent <= mystreamer->member.size);
191 [ + + ]: 237691 : if (mystreamer->file_bytes_sent == mystreamer->member.size)
192 : : {
193 [ + + ]: 164477 : if (mystreamer->pad_bytes_expected == 0)
194 : : {
195 : : /* Trailer is zero-length. */
663 196 : 153777 : astreamer_content(mystreamer->base.bbs_next,
197 : : &mystreamer->member,
198 : : NULL, 0,
199 : : ASTREAMER_MEMBER_TRAILER);
200 : :
201 : : /* Expect next header. */
202 : 153776 : mystreamer->next_context = ASTREAMER_MEMBER_HEADER;
203 : : }
204 : : else
205 : : {
206 : : /* Trailer is not zero-length. */
207 : 10700 : mystreamer->next_context = ASTREAMER_MEMBER_TRAILER;
208 : : }
1667 209 : 164476 : mystreamer->base.bbs_buffer.len = 0;
210 : : }
211 : 237690 : break;
212 : :
663 213 : 10700 : case ASTREAMER_MEMBER_TRAILER:
214 : :
215 : : /*
216 : : * If we're expecting an archive member trailer, accumulate
217 : : * the expected number of padding bytes before sending
218 : : * anything onward.
219 : : */
220 [ - + ]: 10700 : if (!astreamer_buffer_until(streamer, &data, &len,
221 : 10700 : mystreamer->pad_bytes_expected))
1667 rhaas@postgresql.org 222 :UBC 0 : return;
223 : :
224 : : /* OK, now we can send it. */
663 rhaas@postgresql.org 225 :CBC 10700 : astreamer_content(mystreamer->base.bbs_next,
226 : : &mystreamer->member,
68 andrew@dunslane.net 227 : 10700 : mystreamer->base.bbs_buffer.data,
228 : 10700 : mystreamer->pad_bytes_expected,
229 : : ASTREAMER_MEMBER_TRAILER);
230 : :
231 : : /* Expect next file header. */
663 rhaas@postgresql.org 232 : 10700 : mystreamer->next_context = ASTREAMER_MEMBER_HEADER;
1667 233 : 10700 : mystreamer->base.bbs_buffer.len = 0;
234 : 10700 : break;
235 : :
663 236 : 219 : case ASTREAMER_ARCHIVE_TRAILER:
237 : :
238 : : /*
239 : : * We've seen an end-of-archive indicator, so anything more is
240 : : * buffered and sent as part of the archive trailer.
241 : : *
242 : : * Per POSIX, the last physical block of a tar archive is
243 : : * always full-sized, so there may be undefined data after the
244 : : * two zero blocks that mark end-of-archive. GNU tar, for
245 : : * example, zero-pads to a 10kB boundary by default. We just
246 : : * buffer whatever we receive and pass it along at finalize
247 : : * time.
248 : : */
249 : 219 : astreamer_buffer_bytes(streamer, &data, &len, len);
1667 250 : 219 : return;
251 : :
1667 rhaas@postgresql.org 252 :UBC 0 : default:
253 : : /* Shouldn't happen. */
1513 tgl@sss.pgh.pa.us 254 : 0 : pg_fatal("unexpected state while parsing tar archive");
255 : : }
256 : : }
257 : : }
258 : :
259 : : /*
260 : : * Parse a file header within a tar stream.
261 : : *
262 : : * The return value is true if we found a file header and passed it on to the
263 : : * next astreamer; it is false if we have found the archive trailer.
264 : : * We throw error if we see invalid data.
265 : : */
266 : : static bool
663 rhaas@postgresql.org 267 :CBC 207244 : astreamer_tar_header(astreamer_tar_parser *mystreamer)
268 : : {
1667 269 : 207244 : bool has_nonzero_byte = false;
270 : : int i;
663 271 : 207244 : astreamer_member *member = &mystreamer->member;
1667 272 : 207244 : char *buffer = mystreamer->base.bbs_buffer.data;
273 : :
274 [ - + ]: 207244 : Assert(mystreamer->base.bbs_buffer.len == TAR_BLOCK_SIZE);
275 : :
276 : : /* Zero out fields of *member, just for consistency. */
58 tgl@sss.pgh.pa.us 277 : 207244 : memset(member, 0, sizeof(astreamer_member));
278 : :
279 : : /* Check whether we've got a block of all zero bytes. */
1667 rhaas@postgresql.org 280 [ + + ]: 319372 : for (i = 0; i < TAR_BLOCK_SIZE; ++i)
281 : : {
282 [ + + ]: 319153 : if (buffer[i] != '\0')
283 : : {
284 : 207025 : has_nonzero_byte = true;
285 : 207025 : break;
286 : : }
287 : : }
288 : :
289 : : /*
290 : : * If the entire block was zeros, this is the end of the archive, not the
291 : : * start of the next file.
292 : : */
293 [ + + ]: 207244 : if (!has_nonzero_byte)
294 : 219 : return false;
295 : :
296 : : /*
297 : : * Verify that we have a reasonable-looking header.
298 : : */
58 tgl@sss.pgh.pa.us 299 [ - + ]: 207025 : if (!isValidTarHeader(buffer))
58 tgl@sss.pgh.pa.us 300 :UBC 0 : pg_fatal("input file does not appear to be a valid tar archive");
301 : :
302 : : /*
303 : : * Parse key fields out of the header.
304 : : */
1033 rhaas@postgresql.org 305 :CBC 207025 : strlcpy(member->pathname, &buffer[TAR_OFFSET_NAME], MAXPGPATH);
1667 306 [ - + ]: 207025 : if (member->pathname[0] == '\0')
1513 tgl@sss.pgh.pa.us 307 :UBC 0 : pg_fatal("tar member has empty name");
19 michael@paquier.xyz 308 [ - + ]:CBC 207025 : if (!path_is_safe_for_extraction(member->pathname))
19 michael@paquier.xyz 309 :UBC 0 : pg_fatal("tar member has unsafe path name: \"%s\"",
310 : : member->pathname);
311 : :
1033 rhaas@postgresql.org 312 :CBC 207025 : member->size = read_tar_number(&buffer[TAR_OFFSET_SIZE], 12);
313 : 207025 : member->mode = read_tar_number(&buffer[TAR_OFFSET_MODE], 8);
314 : 207025 : member->uid = read_tar_number(&buffer[TAR_OFFSET_UID], 8);
315 : 207025 : member->gid = read_tar_number(&buffer[TAR_OFFSET_GID], 8);
316 : :
58 tgl@sss.pgh.pa.us 317 [ + + + - : 207025 : switch (buffer[TAR_OFFSET_TYPEFLAG])
- ]
318 : : {
319 : 201874 : case TAR_FILETYPE_PLAIN:
320 : : case TAR_FILETYPE_PLAIN_OLD:
321 : 201874 : member->is_regular = true;
322 : 201874 : break;
323 : 5135 : case TAR_FILETYPE_DIRECTORY:
324 : 5135 : member->is_directory = true;
325 : 5135 : break;
326 : 16 : case TAR_FILETYPE_SYMLINK:
327 : 16 : member->is_symlink = true;
328 : 16 : strlcpy(member->linktarget, &buffer[TAR_OFFSET_LINKNAME], 100);
329 : 16 : break;
58 tgl@sss.pgh.pa.us 330 :UBC 0 : case TAR_FILETYPE_PAX_EXTENDED:
331 : : case TAR_FILETYPE_PAX_EXTENDED_GLOBAL:
332 : 0 : pg_fatal("pax extensions to tar format are not supported");
333 : : break;
334 : 0 : default:
335 : : /* For special filetypes, set none of the three is_xxx flags */
336 : 0 : break;
337 : : }
338 : :
339 : : /* Compute number of padding bytes. */
1667 rhaas@postgresql.org 340 :CBC 207025 : mystreamer->pad_bytes_expected = tarPaddingBytesRequired(member->size);
341 : :
342 : : /* Forward the entire header to the next astreamer. */
663 343 : 207025 : astreamer_content(mystreamer->base.bbs_next, member,
344 : : buffer, TAR_BLOCK_SIZE,
345 : : ASTREAMER_MEMBER_HEADER);
346 : :
1667 347 : 207025 : return true;
348 : : }
349 : :
350 : : /*
351 : : * End-of-stream processing for a tar parser.
352 : : */
353 : : static void
663 354 : 219 : astreamer_tar_parser_finalize(astreamer *streamer)
355 : : {
356 : 219 : astreamer_tar_parser *mystreamer = (astreamer_tar_parser *) streamer;
357 : :
358 [ - + ]: 219 : if (mystreamer->next_context != ASTREAMER_ARCHIVE_TRAILER &&
663 rhaas@postgresql.org 359 [ # # ]:UBC 0 : (mystreamer->next_context != ASTREAMER_MEMBER_HEADER ||
1667 360 [ # # ]: 0 : mystreamer->base.bbs_buffer.len > 0))
1513 tgl@sss.pgh.pa.us 361 : 0 : pg_fatal("COPY stream ended before last file was finished");
362 : :
363 : : /* Send the archive trailer, even if empty. */
663 rhaas@postgresql.org 364 :CBC 219 : astreamer_content(streamer->bbs_next, NULL,
365 : 219 : streamer->bbs_buffer.data, streamer->bbs_buffer.len,
366 : : ASTREAMER_ARCHIVE_TRAILER);
367 : :
368 : : /* Now finalize successor. */
369 : 219 : astreamer_finalize(streamer->bbs_next);
1667 370 : 219 : }
371 : :
372 : : /*
373 : : * Free memory associated with a tar parser.
374 : : */
375 : : static void
663 376 : 272 : astreamer_tar_parser_free(astreamer *streamer)
377 : : {
1667 378 : 272 : pfree(streamer->bbs_buffer.data);
663 379 : 272 : astreamer_free(streamer->bbs_next);
68 andrew@dunslane.net 380 : 272 : pfree(streamer);
1667 rhaas@postgresql.org 381 : 272 : }
382 : :
383 : : /*
384 : : * Create a astreamer that can generate a tar archive.
385 : : *
386 : : * This is intended to be usable either for generating a brand-new tar archive
387 : : * or for modifying one on the fly. The input should be a series of typed
388 : : * chunks (i.e. not ASTREAMER_UNKNOWN). See also the comments for
389 : : * astreamer_tar_parser_content.
390 : : */
391 : : astreamer *
663 rhaas@postgresql.org 392 :UBC 0 : astreamer_tar_archiver_new(astreamer *next)
393 : : {
394 : : astreamer_tar_archiver *streamer;
395 : :
172 michael@paquier.xyz 396 :UNC 0 : streamer = palloc0_object(astreamer_tar_archiver);
663 rhaas@postgresql.org 397 :UBC 0 : *((const astreamer_ops **) &streamer->base.bbs_ops) =
398 : : &astreamer_tar_archiver_ops;
1667 399 : 0 : streamer->base.bbs_next = next;
400 : :
401 : 0 : return &streamer->base;
402 : : }
403 : :
404 : : /*
405 : : * Fix up the stream of input chunks to create a valid tar file.
406 : : *
407 : : * If a ASTREAMER_MEMBER_HEADER chunk is of size 0, it is replaced with a
408 : : * newly-constructed tar header. If it is of size TAR_BLOCK_SIZE, it is
409 : : * passed through without change. Any other size is a fatal error (and
410 : : * indicates a bug).
411 : : *
412 : : * Whenever a new ASTREAMER_MEMBER_HEADER chunk is constructed, the
413 : : * corresponding ASTREAMER_MEMBER_TRAILER chunk is also constructed from
414 : : * scratch. Specifically, we construct a block of zero bytes sufficient to
415 : : * pad out to a block boundary, as required by the tar format. Other
416 : : * ASTREAMER_MEMBER_TRAILER chunks are passed through without change.
417 : : *
418 : : * Any ASTREAMER_MEMBER_CONTENTS chunks are passed through without change.
419 : : *
420 : : * The ASTREAMER_ARCHIVE_TRAILER chunk is replaced with two
421 : : * blocks of zero bytes. Not all tar programs require this, but apparently
422 : : * some do. The server does not supply this trailer. If no archive trailer is
423 : : * present, one will be added by astreamer_tar_parser_finalize.
424 : : */
425 : : static void
663 426 : 0 : astreamer_tar_archiver_content(astreamer *streamer,
427 : : astreamer_member *member,
428 : : const char *data, int len,
429 : : astreamer_archive_context context)
430 : : {
431 : 0 : astreamer_tar_archiver *mystreamer = (astreamer_tar_archiver *) streamer;
432 : : char buffer[2 * TAR_BLOCK_SIZE];
433 : :
434 [ # # ]: 0 : Assert(context != ASTREAMER_UNKNOWN);
435 : :
436 [ # # # # ]: 0 : if (context == ASTREAMER_MEMBER_HEADER && len != TAR_BLOCK_SIZE)
437 : : {
1667 438 [ # # ]: 0 : Assert(len == 0);
439 : :
440 : : /* Replace zero-length tar header with a newly constructed one. */
441 : 0 : tarCreateHeader(buffer, member->pathname, NULL,
442 : : member->size, member->mode, member->uid, member->gid,
443 : : time(NULL));
444 : 0 : data = buffer;
445 : 0 : len = TAR_BLOCK_SIZE;
446 : :
447 : : /* Also make a note to replace padding, in case size changed. */
448 : 0 : mystreamer->rearchive_member = true;
449 : : }
663 450 [ # # ]: 0 : else if (context == ASTREAMER_MEMBER_TRAILER &&
1667 451 [ # # ]: 0 : mystreamer->rearchive_member)
452 : 0 : {
453 : 0 : int pad_bytes = tarPaddingBytesRequired(member->size);
454 : :
455 : : /* Also replace padding, if we regenerated the header. */
456 : 0 : memset(buffer, 0, pad_bytes);
457 : 0 : data = buffer;
458 : 0 : len = pad_bytes;
459 : :
460 : : /* Don't do this again unless we replace another header. */
461 : 0 : mystreamer->rearchive_member = false;
462 : : }
663 463 [ # # ]: 0 : else if (context == ASTREAMER_ARCHIVE_TRAILER)
464 : : {
465 : : /* Trailer should always be two blocks of zero bytes. */
1667 466 : 0 : memset(buffer, 0, 2 * TAR_BLOCK_SIZE);
467 : 0 : data = buffer;
468 : 0 : len = 2 * TAR_BLOCK_SIZE;
469 : : }
470 : :
663 471 : 0 : astreamer_content(streamer->bbs_next, member, data, len, context);
1667 472 : 0 : }
473 : :
474 : : /*
475 : : * End-of-stream processing for a tar archiver.
476 : : */
477 : : static void
663 478 : 0 : astreamer_tar_archiver_finalize(astreamer *streamer)
479 : : {
480 : 0 : astreamer_finalize(streamer->bbs_next);
1667 481 : 0 : }
482 : :
483 : : /*
484 : : * Free memory associated with a tar archiver.
485 : : */
486 : : static void
663 487 : 0 : astreamer_tar_archiver_free(astreamer *streamer)
488 : : {
489 : 0 : astreamer_free(streamer->bbs_next);
1667 490 : 0 : pfree(streamer);
491 : 0 : }
492 : :
493 : : /*
494 : : * Create a astreamer that blindly adds two blocks of NUL bytes to the
495 : : * end of an incomplete tarfile that the server might send us.
496 : : */
497 : : astreamer *
663 498 : 0 : astreamer_tar_terminator_new(astreamer *next)
499 : : {
500 : : astreamer *streamer;
501 : :
172 michael@paquier.xyz 502 :UNC 0 : streamer = palloc0_object(astreamer);
663 rhaas@postgresql.org 503 :UBC 0 : *((const astreamer_ops **) &streamer->bbs_ops) =
504 : : &astreamer_tar_terminator_ops;
1664 505 : 0 : streamer->bbs_next = next;
506 : :
507 : 0 : return streamer;
508 : : }
509 : :
510 : : /*
511 : : * Pass all the content through without change.
512 : : */
513 : : static void
663 514 : 0 : astreamer_tar_terminator_content(astreamer *streamer,
515 : : astreamer_member *member,
516 : : const char *data, int len,
517 : : astreamer_archive_context context)
518 : : {
519 : : /* Expect unparsed input. */
1664 520 [ # # ]: 0 : Assert(member == NULL);
663 521 [ # # ]: 0 : Assert(context == ASTREAMER_UNKNOWN);
522 : :
523 : : /* Just forward it. */
524 : 0 : astreamer_content(streamer->bbs_next, member, data, len, context);
1664 525 : 0 : }
526 : :
527 : : /*
528 : : * At the end, blindly add the two blocks of NUL bytes which the server fails
529 : : * to supply.
530 : : */
531 : : static void
663 532 : 0 : astreamer_tar_terminator_finalize(astreamer *streamer)
533 : : {
534 : : char buffer[2 * TAR_BLOCK_SIZE];
535 : :
1664 536 : 0 : memset(buffer, 0, 2 * TAR_BLOCK_SIZE);
663 537 : 0 : astreamer_content(streamer->bbs_next, NULL, buffer,
538 : : 2 * TAR_BLOCK_SIZE, ASTREAMER_UNKNOWN);
539 : 0 : astreamer_finalize(streamer->bbs_next);
1664 540 : 0 : }
541 : :
542 : : /*
543 : : * Free memory associated with a tar terminator.
544 : : */
545 : : static void
663 546 : 0 : astreamer_tar_terminator_free(astreamer *streamer)
547 : : {
548 : 0 : astreamer_free(streamer->bbs_next);
1664 549 : 0 : pfree(streamer);
550 : 0 : }
|