mesa/src/intel/mda/tar.c
Caio Oliveira bccc0fa984 intel/mda: Add code to produce mesa debug archives
Uses the tar format to collect multiple output files.  It can
be inspected using the regular UNIX tools, but a later commit
will add a specialized tool to perform common tasks.

The tar implementation is enough to fulfill the current needs
without adding a dependency.  There's also a small test mostly
to ensure scaffolding is there in case we need to expand the
implementation.

Acked-by: Kenneth Graunke <kenneth@whitecape.org>
Acked-by: Lionel Landwerlin <lionel.g.landwerlin@intel.com>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/29146>
2025-09-24 23:08:45 -07:00

310 lines
8.4 KiB
C

/*
* Copyright 2023 Intel Corporation
* SPDX-License-Identifier: MIT
*/
#include "tar.h"
#include <assert.h>
#include <string.h>
#include <time.h>
/* A tar archive contain a sequence of files, each files is composed of a
* sequence of fixed size records. The first record of a file has header,
* defined by the table below:
*
* Field Name Byte Offset Length in Bytes Field Type
* name 0 100 NUL-terminated if NUL fits
* mode 100 8
* uid 108 8
* gid 116 8
* size 124 12
* mtime 136 12
* chksum 148 8
* typeflag 156 1 see below
* linkname 157 100 NUL-terminated if NUL fits
* magic 256 6 must be TMAGIC (NUL term.)
* version 263 2 must be TVERSION
* uname 265 32 NUL-terminated
* gname 297 32 NUL-terminated
* devmajor 329 8
* devminor 337 8
* prefix 345 155 NUL-terminated if NUL fits
*
* The subsequent records contain the file contents, with extra padding to
* fill a full record. After that the header for the next file starts.
* There's no archive-wide index. See the code below for how checksum is
* calculated.
*
* Comprehensive references for the tar archive are available in
* https://www.loc.gov/preservation/digital/formats/fdd/fdd000531.shtml
*
* Note: the tar_writer implementation uses only the features and fields
* needed for storing debug files. The archive_reader implementation covers
* only what's provided by the writer.
*/
enum {
HEADER_NAME_OFFSET = 0,
HEADER_NAME_LENGTH = 100,
HEADER_MODE_OFFSET = 100,
HEADER_MODE_LENGTH = 8,
HEADER_SIZE_OFFSET = 124,
HEADER_SIZE_LENGTH = 12,
HEADER_MTIME_OFFSET = 136,
HEADER_MTIME_LENGTH = 12,
HEADER_CHECKSUM_OFFSET = 148,
HEADER_CHECKSUM_LENGTH = 8,
HEADER_MAGIC_OFFSET = 257,
HEADER_MAGIC_LENGTH = 6,
HEADER_VERSION_OFFSET = 263,
HEADER_VERSION_LENGTH = 2,
HEADER_PREFIX_OFFSET = 345,
HEADER_PREFIX_LENGTH = 155,
};
static void
archive_update_size(char *header, unsigned size)
{
snprintf(&header[HEADER_SIZE_OFFSET], HEADER_SIZE_LENGTH, "%011o", size);
/* Checksum of the header assumes the checksum field itself is
* filled with ASCII spaces (value 32).
*/
memset(&header[HEADER_CHECKSUM_OFFSET], 32, HEADER_CHECKSUM_LENGTH);
unsigned checksum = 0;
for (int i = 0; i < RECORD_SIZE; i++)
checksum += header[i];
snprintf(&header[HEADER_CHECKSUM_OFFSET], HEADER_CHECKSUM_LENGTH, "%07o", checksum);
}
static void
archive_start_header(char *header, const char *prefix, const char *filename, time_t timestamp)
{
/* NOTE: If we ever need more, implement the more complex `path` extension. */
assert(!prefix || strlen(prefix) < HEADER_PREFIX_LENGTH);
assert(strlen(filename) < HEADER_NAME_LENGTH);
strncpy(&header[HEADER_NAME_OFFSET], filename, HEADER_NAME_LENGTH);
if (prefix)
strncpy(&header[HEADER_PREFIX_OFFSET], prefix, HEADER_PREFIX_LENGTH);
const char *filemode = "0644";
strcpy(&header[HEADER_MODE_OFFSET], filemode);
snprintf(&header[HEADER_MTIME_OFFSET], HEADER_MTIME_LENGTH, "%011lo", (unsigned long)timestamp);
const char *ustar_magic = "ustar";
strcpy(&header[HEADER_MAGIC_OFFSET], ustar_magic);
const char *ustar_version = "00";
strcpy(&header[HEADER_VERSION_OFFSET], ustar_version);
}
static unsigned
archive_calculate_padding(unsigned size)
{
const unsigned m = size % RECORD_SIZE;
return m ? RECORD_SIZE - m : 0;
}
static const char archive_empty_records[RECORD_SIZE * 2] = {0};
static bool
archive_write_padding(FILE *archive, unsigned contents_size)
{
const unsigned padding_size = archive_calculate_padding(contents_size);
return fwrite(archive_empty_records, padding_size, 1, archive) == 1;
}
static void
archive_prewrite_end_of_archive(FILE *archive)
{
/* Two empty records mark the proper end of the file, so always
* keep them but reposition the cursor so next write overwrites
* them.
*/
fwrite(&archive_empty_records, RECORD_SIZE * 2, 1, archive);
fflush(archive);
fseek(archive, -RECORD_SIZE * 2, SEEK_END);
}
static bool
archive_file_from_bytes(FILE *archive, const char *prefix, const char *filename,
const char *contents, unsigned contents_size, time_t timestamp)
{
char header[RECORD_SIZE] = {0};
archive_start_header(header, prefix, filename, timestamp);
archive_update_size(header, contents_size);
if (fwrite(header, RECORD_SIZE, 1, archive) != 1)
return false;
if (fwrite(contents, contents_size, 1, archive) != 1)
return false;
archive_write_padding(archive, contents_size);
archive_prewrite_end_of_archive(archive);
fflush(archive);
return ferror(archive) == 0;
}
void
tar_writer_init(tar_writer *tw, FILE *f)
{
memset(tw, 0, sizeof(*tw));
tw->file = f;
tw->header_pos = -1;
tw->error = false;
tw->prefix = NULL;
tw->timestamp = time(NULL);
}
void
tar_writer_start_file(tar_writer *tw, const char *filename)
{
assert(tw->header_pos == -1);
memset(tw->header, 0, RECORD_SIZE);
archive_start_header(tw->header, tw->prefix, filename, tw->timestamp);
archive_update_size(tw->header, 0);
tw->header_pos = ftell(tw->file);
if (tw->header_pos == -1)
goto fail;
if (fwrite(tw->header, RECORD_SIZE, 1, tw->file) != 1)
goto fail;
if (fflush(tw->file) != 0)
goto fail;
return;
fail:
tw->error = true;
}
void
tar_writer_finish_file(tar_writer *tw)
{
assert(tw->header_pos >= 0);
const long end_pos = ftell(tw->file);
if (end_pos == -1)
goto fail;
assert((tw->header_pos + RECORD_SIZE) <= end_pos);
const unsigned size = end_pos - tw->header_pos - RECORD_SIZE;
archive_write_padding(tw->file, size);
archive_update_size(tw->header, size);
if (fseek(tw->file, tw->header_pos, SEEK_SET) == -1)
goto fail;
if (fwrite(tw->header, RECORD_SIZE, 1, tw->file) != 1)
goto fail;
if (fseek(tw->file, 0, SEEK_END) == -1)
goto fail;
archive_prewrite_end_of_archive(tw->file);
if (fflush(tw->file) != 0)
goto fail;
goto end;
fail:
tw->error = true;
end:
tw->header_pos = -1;
memset(tw->header, 0, RECORD_SIZE);
}
void
tar_writer_file_from_bytes(tar_writer *tw, const char *filename,
const char *contents, unsigned contents_size)
{
assert(tw->header_pos == -1);
if (!archive_file_from_bytes(tw->file, tw->prefix, filename, contents, contents_size, tw->timestamp))
tw->error = true;
}
void
tar_reader_init_from_bytes(tar_reader *ar, const char *contents, unsigned contents_size)
{
memset(ar, 0, sizeof(*ar));
ar->contents = (slice){contents, contents_size};
}
bool
tar_reader_next(tar_reader *ar, tar_reader_entry *entry)
{
if (ar->error)
return false;
if (ar->pos >= ar->contents.len)
return false;
if (ar->pos + RECORD_SIZE > ar->contents.len) {
ar->error = true;
return false;
}
const char *header = &ar->contents.data[ar->pos];
const char *name = &header[HEADER_NAME_OFFSET];
const char *prefix = &header[HEADER_PREFIX_OFFSET];
ar->pos += RECORD_SIZE;
/* The current writer enforces the NUL termination and padding, so for
* now let's rely on it.
*/
if (name[HEADER_NAME_LENGTH-1] != 0 || prefix[HEADER_PREFIX_LENGTH-1] != 0) {
ar->error = true;
return false;
}
unsigned size = 0;
if (sscanf((const char *)&header[HEADER_SIZE_OFFSET], "%011o", &size) == EOF) {
ar->error = true;
return false;
}
unsigned long mtime = 0;
if (sscanf((const char *)&header[HEADER_MTIME_OFFSET], "%011lo", &mtime) == EOF) {
ar->error = true;
return false;
}
const unsigned padded_size = size + archive_calculate_padding(size);
if (ar->pos + padded_size > ar->contents.len) {
ar->error = true;
return false;
}
slice contents = slice_substr(ar->contents, ar->pos, ar->pos + size);
ar->pos += padded_size;
/* TODO: Verify checksum. */
entry->prefix = slice_from_cstr(prefix);
entry->name = slice_from_cstr(name);
entry->contents = contents;
entry->mtime = (time_t)mtime;
return true;
}