LCOV - code coverage report
Current view: top level - src/fe_utils - astreamer_tar.c (source / functions) Coverage Total Hit
Test: PostgreSQL 19devel Lines: 59.2 % 157 93
Test Date: 2026-05-23 08:16:37 Functions: 38.5 % 13 5
Legend: Lines:     hit not hit

            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 *
      93          262 : astreamer_tar_parser_new(astreamer *next)
      94              : {
      95              :     astreamer_tar_parser *streamer;
      96              : 
      97          262 :     streamer = palloc0_object(astreamer_tar_parser);
      98          262 :     *((const astreamer_ops **) &streamer->base.bbs_ops) =
      99              :         &astreamer_tar_parser_ops;
     100          262 :     streamer->base.bbs_next = next;
     101          262 :     initStringInfo(&streamer->base.bbs_buffer);
     102          262 :     streamer->next_context = ASTREAMER_MEMBER_HEADER;
     103              : 
     104          262 :     return &streamer->base;
     105              : }
     106              : 
     107              : /*
     108              :  * Parse unknown content as tar data.
     109              :  */
     110              : static void
     111       362447 : astreamer_tar_parser_content(astreamer *streamer, astreamer_member *member,
     112              :                              const char *data, int len,
     113              :                              astreamer_archive_context context)
     114              : {
     115       362447 :     astreamer_tar_parser *mystreamer = (astreamer_tar_parser *) streamer;
     116              :     size_t      nbytes;
     117              : 
     118              :     /* Expect unparsed input. */
     119              :     Assert(member == NULL);
     120              :     Assert(context == ASTREAMER_UNKNOWN);
     121              : 
     122       786378 :     while (len > 0)
     123              :     {
     124       424153 :         switch (mystreamer->next_context)
     125              :         {
     126       191523 :             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       191523 :                 if (!astreamer_buffer_until(streamer, &data, &len,
     133              :                                             TAR_BLOCK_SIZE))
     134            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              :                  */
     142       191523 :                 if (astreamer_tar_header(mystreamer))
     143              :                 {
     144       191302 :                     if (mystreamer->member.size == 0)
     145              :                     {
     146              :                         /* No content; trailer is zero-length. */
     147        39278 :                         astreamer_content(mystreamer->base.bbs_next,
     148              :                                           &mystreamer->member,
     149              :                                           NULL, 0,
     150              :                                           ASTREAMER_MEMBER_TRAILER);
     151              : 
     152              :                         /* Expect next header. */
     153        39278 :                         mystreamer->next_context = ASTREAMER_MEMBER_HEADER;
     154              :                     }
     155              :                     else
     156              :                     {
     157              :                         /* Expect contents. */
     158       152024 :                         mystreamer->next_context = ASTREAMER_MEMBER_CONTENTS;
     159              :                     }
     160       191302 :                     mystreamer->base.bbs_buffer.len = 0;
     161       191302 :                     mystreamer->file_bytes_sent = 0;
     162              :                 }
     163              :                 else
     164          221 :                     mystreamer->next_context = ASTREAMER_ARCHIVE_TRAILER;
     165       191523 :                 break;
     166              : 
     167       221933 :             case ASTREAMER_MEMBER_CONTENTS:
     168              : 
     169              :                 /*
     170              :                  * Send as much content as we have, but not more than the
     171              :                  * remaining file length.
     172              :                  */
     173              :                 Assert(mystreamer->file_bytes_sent < mystreamer->member.size);
     174       221933 :                 nbytes = mystreamer->member.size - mystreamer->file_bytes_sent;
     175       221933 :                 nbytes = Min(nbytes, len);
     176              :                 Assert(nbytes > 0);
     177       221933 :                 astreamer_content(mystreamer->base.bbs_next,
     178              :                                   &mystreamer->member,
     179              :                                   data, nbytes,
     180              :                                   ASTREAMER_MEMBER_CONTENTS);
     181       221933 :                 mystreamer->file_bytes_sent += nbytes;
     182       221933 :                 data += nbytes;
     183       221933 :                 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              :                 Assert(mystreamer->file_bytes_sent <= mystreamer->member.size);
     191       221933 :                 if (mystreamer->file_bytes_sent == mystreamer->member.size)
     192              :                 {
     193       151984 :                     if (mystreamer->pad_bytes_expected == 0)
     194              :                     {
     195              :                         /* Trailer is zero-length. */
     196       141508 :                         astreamer_content(mystreamer->base.bbs_next,
     197              :                                           &mystreamer->member,
     198              :                                           NULL, 0,
     199              :                                           ASTREAMER_MEMBER_TRAILER);
     200              : 
     201              :                         /* Expect next header. */
     202       141507 :                         mystreamer->next_context = ASTREAMER_MEMBER_HEADER;
     203              :                     }
     204              :                     else
     205              :                     {
     206              :                         /* Trailer is not zero-length. */
     207        10476 :                         mystreamer->next_context = ASTREAMER_MEMBER_TRAILER;
     208              :                     }
     209       151983 :                     mystreamer->base.bbs_buffer.len = 0;
     210              :                 }
     211       221932 :                 break;
     212              : 
     213        10476 :             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        10476 :                 if (!astreamer_buffer_until(streamer, &data, &len,
     221        10476 :                                             mystreamer->pad_bytes_expected))
     222            0 :                     return;
     223              : 
     224              :                 /* OK, now we can send it. */
     225        10476 :                 astreamer_content(mystreamer->base.bbs_next,
     226              :                                   &mystreamer->member,
     227        10476 :                                   mystreamer->base.bbs_buffer.data,
     228        10476 :                                   mystreamer->pad_bytes_expected,
     229              :                                   ASTREAMER_MEMBER_TRAILER);
     230              : 
     231              :                 /* Expect next file header. */
     232        10476 :                 mystreamer->next_context = ASTREAMER_MEMBER_HEADER;
     233        10476 :                 mystreamer->base.bbs_buffer.len = 0;
     234        10476 :                 break;
     235              : 
     236          221 :             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          221 :                 astreamer_buffer_bytes(streamer, &data, &len, len);
     250          221 :                 return;
     251              : 
     252            0 :             default:
     253              :                 /* Shouldn't happen. */
     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
     267       191523 : astreamer_tar_header(astreamer_tar_parser *mystreamer)
     268              : {
     269       191523 :     bool        has_nonzero_byte = false;
     270              :     int         i;
     271       191523 :     astreamer_member *member = &mystreamer->member;
     272       191523 :     char       *buffer = mystreamer->base.bbs_buffer.data;
     273              : 
     274              :     Assert(mystreamer->base.bbs_buffer.len == TAR_BLOCK_SIZE);
     275              : 
     276              :     /* Zero out fields of *member, just for consistency. */
     277       191523 :     memset(member, 0, sizeof(astreamer_member));
     278              : 
     279              :     /* Check whether we've got a block of all zero bytes. */
     280       304675 :     for (i = 0; i < TAR_BLOCK_SIZE; ++i)
     281              :     {
     282       304454 :         if (buffer[i] != '\0')
     283              :         {
     284       191302 :             has_nonzero_byte = true;
     285       191302 :             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       191523 :     if (!has_nonzero_byte)
     294          221 :         return false;
     295              : 
     296              :     /*
     297              :      * Verify that we have a reasonable-looking header.
     298              :      */
     299       191302 :     if (!isValidTarHeader(buffer))
     300            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              :      */
     305       191302 :     strlcpy(member->pathname, &buffer[TAR_OFFSET_NAME], MAXPGPATH);
     306       191302 :     if (member->pathname[0] == '\0')
     307            0 :         pg_fatal("tar member has empty name");
     308       191302 :     if (!path_is_safe_for_extraction(member->pathname))
     309            0 :         pg_fatal("tar member has unsafe path name: \"%s\"",
     310              :                  member->pathname);
     311              : 
     312       191302 :     member->size = read_tar_number(&buffer[TAR_OFFSET_SIZE], 12);
     313       191302 :     member->mode = read_tar_number(&buffer[TAR_OFFSET_MODE], 8);
     314       191302 :     member->uid = read_tar_number(&buffer[TAR_OFFSET_UID], 8);
     315       191302 :     member->gid = read_tar_number(&buffer[TAR_OFFSET_GID], 8);
     316              : 
     317       191302 :     switch (buffer[TAR_OFFSET_TYPEFLAG])
     318              :     {
     319       186524 :         case TAR_FILETYPE_PLAIN:
     320              :         case TAR_FILETYPE_PLAIN_OLD:
     321       186524 :             member->is_regular = true;
     322       186524 :             break;
     323         4762 :         case TAR_FILETYPE_DIRECTORY:
     324         4762 :             member->is_directory = true;
     325         4762 :             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;
     330            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. */
     340       191302 :     mystreamer->pad_bytes_expected = tarPaddingBytesRequired(member->size);
     341              : 
     342              :     /* Forward the entire header to the next astreamer. */
     343       191302 :     astreamer_content(mystreamer->base.bbs_next, member,
     344              :                       buffer, TAR_BLOCK_SIZE,
     345              :                       ASTREAMER_MEMBER_HEADER);
     346              : 
     347       191302 :     return true;
     348              : }
     349              : 
     350              : /*
     351              :  * End-of-stream processing for a tar parser.
     352              :  */
     353              : static void
     354          207 : astreamer_tar_parser_finalize(astreamer *streamer)
     355              : {
     356          207 :     astreamer_tar_parser *mystreamer = (astreamer_tar_parser *) streamer;
     357              : 
     358          207 :     if (mystreamer->next_context != ASTREAMER_ARCHIVE_TRAILER &&
     359            0 :         (mystreamer->next_context != ASTREAMER_MEMBER_HEADER ||
     360            0 :          mystreamer->base.bbs_buffer.len > 0))
     361            0 :         pg_fatal("COPY stream ended before last file was finished");
     362              : 
     363              :     /* Send the archive trailer, even if empty. */
     364          207 :     astreamer_content(streamer->bbs_next, NULL,
     365          207 :                       streamer->bbs_buffer.data, streamer->bbs_buffer.len,
     366              :                       ASTREAMER_ARCHIVE_TRAILER);
     367              : 
     368              :     /* Now finalize successor. */
     369          207 :     astreamer_finalize(streamer->bbs_next);
     370          207 : }
     371              : 
     372              : /*
     373              :  * Free memory associated with a tar parser.
     374              :  */
     375              : static void
     376          255 : astreamer_tar_parser_free(astreamer *streamer)
     377              : {
     378          255 :     pfree(streamer->bbs_buffer.data);
     379          255 :     astreamer_free(streamer->bbs_next);
     380          255 :     pfree(streamer);
     381          255 : }
     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 *
     392            0 : astreamer_tar_archiver_new(astreamer *next)
     393              : {
     394              :     astreamer_tar_archiver *streamer;
     395              : 
     396            0 :     streamer = palloc0_object(astreamer_tar_archiver);
     397            0 :     *((const astreamer_ops **) &streamer->base.bbs_ops) =
     398              :         &astreamer_tar_archiver_ops;
     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
     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              :     Assert(context != ASTREAMER_UNKNOWN);
     435              : 
     436            0 :     if (context == ASTREAMER_MEMBER_HEADER && len != TAR_BLOCK_SIZE)
     437              :     {
     438              :         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              :     }
     450            0 :     else if (context == ASTREAMER_MEMBER_TRAILER &&
     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              :     }
     463            0 :     else if (context == ASTREAMER_ARCHIVE_TRAILER)
     464              :     {
     465              :         /* Trailer should always be two blocks of zero bytes. */
     466            0 :         memset(buffer, 0, 2 * TAR_BLOCK_SIZE);
     467            0 :         data = buffer;
     468            0 :         len = 2 * TAR_BLOCK_SIZE;
     469              :     }
     470              : 
     471            0 :     astreamer_content(streamer->bbs_next, member, data, len, context);
     472            0 : }
     473              : 
     474              : /*
     475              :  * End-of-stream processing for a tar archiver.
     476              :  */
     477              : static void
     478            0 : astreamer_tar_archiver_finalize(astreamer *streamer)
     479              : {
     480            0 :     astreamer_finalize(streamer->bbs_next);
     481            0 : }
     482              : 
     483              : /*
     484              :  * Free memory associated with a tar archiver.
     485              :  */
     486              : static void
     487            0 : astreamer_tar_archiver_free(astreamer *streamer)
     488              : {
     489            0 :     astreamer_free(streamer->bbs_next);
     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 *
     498            0 : astreamer_tar_terminator_new(astreamer *next)
     499              : {
     500              :     astreamer  *streamer;
     501              : 
     502            0 :     streamer = palloc0_object(astreamer);
     503            0 :     *((const astreamer_ops **) &streamer->bbs_ops) =
     504              :         &astreamer_tar_terminator_ops;
     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
     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. */
     520              :     Assert(member == NULL);
     521              :     Assert(context == ASTREAMER_UNKNOWN);
     522              : 
     523              :     /* Just forward it. */
     524            0 :     astreamer_content(streamer->bbs_next, member, data, len, context);
     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
     532            0 : astreamer_tar_terminator_finalize(astreamer *streamer)
     533              : {
     534              :     char        buffer[2 * TAR_BLOCK_SIZE];
     535              : 
     536            0 :     memset(buffer, 0, 2 * TAR_BLOCK_SIZE);
     537            0 :     astreamer_content(streamer->bbs_next, NULL, buffer,
     538              :                       2 * TAR_BLOCK_SIZE, ASTREAMER_UNKNOWN);
     539            0 :     astreamer_finalize(streamer->bbs_next);
     540            0 : }
     541              : 
     542              : /*
     543              :  * Free memory associated with a tar terminator.
     544              :  */
     545              : static void
     546            0 : astreamer_tar_terminator_free(astreamer *streamer)
     547              : {
     548            0 :     astreamer_free(streamer->bbs_next);
     549            0 :     pfree(streamer);
     550            0 : }
        

Generated by: LCOV version 2.0-1