feat: Add write support

This uses a new struct (struct minitar_w).
This struct is initialized using the minitar_open_w function, and deleted using minitar_close_w.

The functions to write to an archive are minitar_write_file_entry() and minitar_write_special_entry().
The difference is that regular files have content, while the rest (special entries) do not.

This commit also adds a new example program (pack), which can create tar archives from files (no directory support though).

Archives created using pack and minitar can be read and extracted using GNU tar!!

Documentation is coming in another commit.
This commit is contained in:
apio 2023-02-04 19:02:08 +01:00
parent 95700b6916
commit 482ac6d949
Signed by: apio
GPG Key ID: B8A7D06E42258954
5 changed files with 226 additions and 1 deletions

View File

@ -4,4 +4,7 @@ target_link_libraries(list PRIVATE minitar)
add_executable(untar EXCLUDE_FROM_ALL untar.c)
target_link_libraries(untar PRIVATE minitar)
add_custom_target(examples DEPENDS list untar)
add_executable(pack EXCLUDE_FROM_ALL pack.c)
target_link_libraries(pack PRIVATE minitar)
add_custom_target(examples DEPENDS list untar pack)

88
examples/pack.c Normal file
View File

@ -0,0 +1,88 @@
/*
* Copyright (c) 2023, apio.
*
* SPDX-License-Identifier: BSD-2-Clause
*
* pack.c: Example utility which creates a tar archive (POSIX only).
*/
#define _XOPEN_SOURCE 700
#include <minitar.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
int main(int argc, char** argv)
{
if (argc < 3)
{
fprintf(stderr, "Usage: %s [output] files\n", argv[0]);
return 1;
}
struct minitar_w mp;
if (minitar_open_w(argv[1], &mp, MTAR_OVERWRITE) != 0)
{
perror(argv[1]);
return 1;
}
int exit_status = 0;
struct minitar_entry_metadata metadata;
int arg = 2;
while (arg < argc)
{
FILE* fp = fopen(argv[arg], "r");
if (!fp)
{
perror("fopen");
exit_status = 1;
break;
}
// Get the file length.
fseek(fp, 0, SEEK_END);
size_t length = ftell(fp);
fseek(fp, 0, SEEK_SET);
char* buf = malloc(length);
fread(buf, 1, length, fp);
if (ferror(fp))
{
perror("fread");
exit_status = 1;
break;
}
struct stat st;
int rc = fstat(fileno(fp), &st);
if (rc < 0)
{
perror("fstat");
exit_status = 1;
break;
}
struct minitar_entry_metadata metadata;
strncpy(metadata.path, argv[arg], sizeof(metadata.path));
metadata.uid = st.st_uid;
metadata.gid = st.st_gid;
metadata.mtime = st.st_mtim.tv_sec;
metadata.size = length;
metadata.type = MTAR_REGULAR;
metadata.mode = st.st_mode & ~S_IFMT;
rc = minitar_write_file_entry(&mp, &metadata, buf);
free(buf);
if (rc != 0)
{
perror("write entry failed");
exit_status = 1;
break;
}
arg++;
}
minitar_close_w(&mp);
return exit_status;
}

View File

@ -17,6 +17,17 @@ struct minitar
FILE* stream;
};
struct minitar_w
{
FILE* stream;
};
enum minitar_write_mode
{
MTAR_APPEND,
MTAR_OVERWRITE
};
enum minitar_file_type
{
MTAR_REGULAR,
@ -62,13 +73,17 @@ extern "C"
#endif
int minitar_open(const char* pathname, struct minitar* out);
int minitar_open_w(const char* pathname, struct minitar_w* out, enum minitar_write_mode mode);
int minitar_read_entry(struct minitar* mp, struct minitar_entry* out);
int minitar_write_file_entry(struct minitar_w* mp, const struct minitar_entry_metadata* metadata, char* buf);
int minitar_write_special_entry(struct minitar_w* mp, const struct minitar_entry_metadata* metadata);
void minitar_rewind(struct minitar* mp);
int minitar_find_by_name(struct minitar* mp, const char* name, struct minitar_entry* out);
int minitar_find_by_path(struct minitar* mp, const char* path, struct minitar_entry* out);
int minitar_find_any_of(struct minitar* mp, enum minitar_file_type type, struct minitar_entry* out);
size_t minitar_read_contents(struct minitar* mp, const struct minitar_entry* entry, char* buf, size_t max);
int minitar_close(struct minitar* mp);
int minitar_close_w(struct minitar_w* mp);
#ifdef __cplusplus
}

View File

@ -11,10 +11,18 @@
#include <stdio.h>
#include <string.h>
#ifndef __TINYC__
#include <stdnoreturn.h>
#else
#define noreturn _Noreturn
#endif
// all of these are defined in util.c
int minitar_read_header(struct minitar*, struct tar_header*);
noreturn void minitar_handle_panic(const char*);
int minitar_validate_header(const struct tar_header*);
void minitar_parse_metadata_from_tar_header(const struct tar_header*, struct minitar_entry_metadata*);
void minitar_construct_header_from_metadata(struct tar_header*, const struct minitar_entry_metadata*);
size_t minitar_align_up_to_block_size(size_t);
int minitar_open(const char* pathname, struct minitar* out)
@ -27,11 +35,31 @@ int minitar_open(const char* pathname, struct minitar* out)
return 0;
}
int minitar_open_w(const char* pathname, struct minitar_w* out, enum minitar_write_mode mode)
{
const char* mode_string;
switch (mode)
{
case MTAR_APPEND: mode_string = "ab"; break;
case MTAR_OVERWRITE: mode_string = "wb"; break;
default: minitar_handle_panic("mode passed to minitar_open_w is not supported");
}
FILE* fp = fopen(pathname, mode_string);
if (!fp) return -1;
out->stream = fp;
return 0;
}
int minitar_close(struct minitar* mp)
{
return fclose(mp->stream);
}
int minitar_close_w(struct minitar_w* mp)
{
return fclose(mp->stream);
}
// Try to read a valid header, and construct an entry from it. If the 512-byte block at the current read offset is not a
// valid header, valid is set to 0 so we can try again with the next block. In any other case, valid is set to 1. This
// helps distinguish valid return values, EOF, and invalid headers that we should just skip.
@ -74,6 +102,47 @@ int minitar_read_entry(struct minitar* mp, struct minitar_entry* out)
return result;
}
int minitar_write_file_entry(struct minitar_w* mp, const struct minitar_entry_metadata* metadata, char* buf)
{
struct minitar_entry_metadata meta = *metadata;
meta.type = MTAR_REGULAR;
struct tar_header hdr;
minitar_construct_header_from_metadata(&hdr, &meta);
// Write the header.
size_t nwrite = fwrite(&hdr, sizeof(hdr), 1, mp->stream);
if (nwrite == 0 && ferror(mp->stream)) return -1;
// Write the file data.
nwrite = fwrite(buf, 1, meta.size, mp->stream);
if (nwrite == 0 && ferror(mp->stream)) return -1;
char zeroes[512];
memset(zeroes, 0, sizeof(zeroes));
// Write as many zeroes as necessary to finish a block.
size_t nzero = minitar_align_up_to_block_size(meta.size) - meta.size;
nwrite = fwrite(zeroes, 1, nzero, mp->stream);
if (nwrite == 0 && ferror(mp->stream)) return -1;
return 0;
}
int minitar_write_special_entry(struct minitar_w* mp, const struct minitar_entry_metadata* metadata)
{
struct minitar_entry_metadata meta = *metadata;
if (meta.type == MTAR_REGULAR)
minitar_handle_panic("Trying to write a special entry, yet MTAR_REGULAR passed as the entry type");
meta.size = 0;
struct tar_header hdr;
minitar_construct_header_from_metadata(&hdr, &meta);
size_t nwrite = fwrite(&hdr, sizeof(hdr), 1, mp->stream);
if (nwrite == 0 && ferror(mp->stream)) return -1;
return 0;
}
void minitar_rewind(struct minitar* mp)
{
rewind(mp->stream);

View File

@ -234,6 +234,56 @@ uint32_t minitar_checksum_header(const struct tar_header* hdr)
return sum;
}
void minitar_construct_header_from_metadata(struct tar_header* hdr, const struct minitar_entry_metadata* metadata)
{
if (strlen(metadata->path) > 100)
{
minitar_handle_panic("FIXME: pathnames over 100 (using the prefix field) are unsupported for now");
}
// We intentionally want strncpy to not write a null terminator here if the path field is 100 bytes long.
strncpy(hdr->name, metadata->path, 100);
snprintf(hdr->mode, 8, "%.7o", metadata->mode);
snprintf(hdr->uid, 8, "%.7o", metadata->uid);
snprintf(hdr->gid, 8, "%.7o", metadata->gid);
// snprintf will write the null terminator past the size field. We don't care, as we will overwrite that zero later.
snprintf(hdr->size, 13, "%.12zo", metadata->size);
// Same here.
snprintf(hdr->mtime, 13, "%.12lo", metadata->mtime);
switch (metadata->type)
{
case MTAR_REGULAR: hdr->typeflag = '0'; break;
case MTAR_HARDLINK: hdr->typeflag = '1'; break;
case MTAR_SYMLINK: hdr->typeflag = '2'; break;
case MTAR_CHRDEV: hdr->typeflag = '3'; break;
case MTAR_BLKDEV: hdr->typeflag = '4'; break;
case MTAR_DIRECTORY: hdr->typeflag = '5'; break;
case MTAR_FIFO: hdr->typeflag = '6'; break;
}
strncpy(hdr->linkname, metadata->link, 100);
memcpy(hdr->magic, "ustar", 6);
hdr->version[0] = '0';
hdr->version[1] = '0';
strncpy(hdr->uname, metadata->uname, 32);
strncpy(hdr->gname, metadata->gname, 32);
snprintf(hdr->devmajor, 8, "%.7o", metadata->devmajor);
snprintf(hdr->devminor, 8, "%.7o", metadata->devminor);
memset(hdr->prefix, 0, sizeof(hdr->prefix));
memset(hdr->padding, 0, sizeof(hdr->padding));
uint32_t checksum = minitar_checksum_header(hdr);
snprintf(hdr->chksum, 8, "%.7o", checksum);
}
int minitar_validate_header(const struct tar_header* hdr)
{
if (hdr->typeflag != '\0' && hdr->typeflag != '0' && hdr->typeflag != '1' && hdr->typeflag != '2' &&