Files
squeezelite-esp32/components/spotify/cspot/bell/main/io/BellTar.cpp
2023-05-06 23:50:26 +02:00

329 lines
9.1 KiB
C++

#include "BellTar.h"
#include <sys/stat.h> // for mkdir
using namespace bell::BellTar;
#include <algorithm> // for min
#include <cassert> // for assert
#include <cstdint> // for uint8_t
#include <cstdio> // for sprintf, size_t, sscanf, EOF, NULL
#include <cstdlib> // for rand
#include <cstring> // for memset, strlen
#include <ctime> // for time
#include <fstream> // for ofstream
#include <vector> // for vector
#ifdef _WIN32
#include <direct.h>
#endif
#ifdef ENABLE_LOGGING
#define LOG printf
#else
#ifdef _WIN32
#define LOG(fmt, ...) ((void)0)
#else
#define LOG(fmt, args...) ((void)0)
#endif
#endif
const char FILL_CHAR = '\0';
const int FILE_NAME_LENGTH = 100;
// From http://en.wikipedia.org/wiki/Tar_(computing)#UStar_format
typedef enum tar_file_type {
tar_file_type_normal = '0',
tar_file_type_hard_link = '1',
tar_file_type_soft_link = '2',
tar_file_type_directory = '5'
} tar_file_type_t;
struct tar_header {
char name[FILE_NAME_LENGTH]; // file name
char mode[8]; // file mode
char uid[8]; // Owner's numeric user ID
char gid[8]; // Group's numeric user ID
char size[12]; // File size in bytes (octal base)
char mtime[12]; // Last modification time in
// numeric Unix time format (octal)
char checksum[8]; // Checksum for header record
char typeflag[1]; // file type, see tar_file_type_t
char linkname[100]; // Name of linked file
char magic[6]; // UStar indicator "ustar"
char version[2]; // UStar version "00"
char uname[32]; // Owner user name
char gname[32]; // Owner group name
char devmajor[8]; // Device major number
char devminor[8]; // Device minor number
char prefix[155]; // Filename prefix
char pad[12]; // padding
};
void header_set_metadata(tar_header* header) {
std::memset(header, 0, sizeof(tar_header));
std::sprintf(header->magic, "ustar");
std::sprintf(header->mtime, "%011lo", (unsigned long)std::time(NULL));
std::sprintf(header->mode, "%07o", 0644);
std::sprintf(header->uname, "unkown"); // ... a bit random
std::sprintf(header->gname, "users");
header->typeflag[0] = 0; // always just a normal file
}
/* From Wikipedia: The checksum is calculated by taking the sum of the
* unsigned byte values of the header record with the eight checksum
* bytes taken to be ascii spaces. */
void header_set_checksum(tar_header* header) {
unsigned int sum = 0;
char* pointer = (char*)header;
char* end = pointer + sizeof(tar_header);
// Iterate over header struct until we are at checksum field.
while (pointer < header->checksum) {
sum += *pointer & 0xff;
pointer++;
}
// ... then add eight 'ascii spaces' ...
sum += ' ' * 8;
pointer += 8;
// ... and go until the end.
while (pointer < end) {
sum += *pointer & 0xff;
pointer++;
}
std::sprintf(header->checksum, "%06o", sum);
}
void header_set_filetype(tar_header* header, tar_file_type_t file_type) {
header->typeflag[0] = file_type;
}
tar_file_type_t header_get_filetype(tar_header* header) {
return tar_file_type_t(header->typeflag[0]);
}
void header_set_filesize(tar_header* header, file_size_t file_size) {
std::sprintf(header->size, "%011llo", file_size);
}
file_size_t header_get_filesize(tar_header* header) {
file_size_t file_size;
std::sscanf(header->size, "%011llo", &file_size);
return file_size;
}
void header_set_filename(tar_header* header, const char* file_name) {
size_t len = std::strlen(file_name);
// len > 0 also ensures that the header does not start with \0
if (len == 0 || len >= FILE_NAME_LENGTH) {
LOG("Invalid file name for tar: %s\n", file_name);
std::sprintf(header->name, "INVALID_%d", std::rand());
} else {
std::sprintf(header->name, "%s", file_name);
}
}
std::string header_get_filename(tar_header* header) {
return std::string(header->name);
}
////////////////////////////////////////
/* Every file in a tar file starts with the tar header */
void _write_header(std::ostream& dst, const char* file_name,
file_size_t file_size,
tar_file_type_t file_type = tar_file_type_normal) {
tar_header header;
header_set_metadata(&header);
header_set_filename(&header, file_name);
header_set_filesize(&header, file_size);
header_set_filetype(&header, file_type);
header_set_checksum(&header);
dst.write((const char*)&header, sizeof(tar_header));
}
void _read_header(std::istream& inp, tar_header* header) {
inp.read((char*)header, sizeof(tar_header));
}
/* The length of the data after the header must be rounded up to a
multiple of 512 bytes, the length of the header. */
void _fill(std::ostream& dst, unsigned long file_size) {
while (file_size % sizeof(tar_header) != 0) {
dst.put(FILL_CHAR);
file_size++;
}
}
bool _check_if_header_is_next(std::istream& inp) {
if (inp.eof() || inp.peek() == EOF) {
LOG("Can not read next file info, istream at EOF.\n");
return false;
}
if (inp.peek() == FILL_CHAR) {
LOG("Can not read next file info, istream is pointing "
"to %d, which a tar header can not start with.\n",
FILL_CHAR);
return false;
}
return true;
}
void _seek_to_next_header(std::istream& inp) {
// Advance to start of next header or to end of file
// Works because
// - header never starts with FILL_CHAR
// - at end of file, peek() returns EOF.
// - FILL_CHAR != EOF
while (inp.peek() == FILL_CHAR)
inp.get();
}
////////////////////////////////////////
// writer Implementation
////////////////////////////////////////
void writer::put(std::string path_in_tar, char const* const data,
const file_size_t data_size) {
_write_header(_dst, path_in_tar.c_str(), data_size);
_dst.write(data, data_size);
_fill(_dst, data_size);
}
void writer::put_directory(std::string path_in_tar) {
_write_header(_dst, path_in_tar.c_str(), 0, tar_file_type_directory);
}
/* The end of an tar is marked by at least two consecutive zero-filled
* records, a record having the size of the header. */
void writer::finish() {
unsigned long i = 0;
while (i < 2 * sizeof(tar_header)) {
_dst.put(FILL_CHAR);
i++;
}
}
////////////////////////////////////////
// reader Implementation
////////////////////////////////////////
bool reader::contains_another_file() {
return _check_if_header_is_next(_inp);
}
void reader::_cache_header() {
if (_cached_header_data_valid)
return;
assert(contains_another_file());
tar_header h;
_read_header(_inp, &h);
_cached_header_data.file_name = header_get_filename(&h);
_cached_header_data.file_size = header_get_filesize(&h);
_cached_header_data.file_type = h.typeflag[0];
_cached_header_data_valid = true;
}
std::string reader::get_next_file_name() {
_cache_header();
return _cached_header_data.file_name;
}
file_size_t reader::get_next_file_size() {
_cache_header();
return _cached_header_data.file_size;
}
void reader::read_next_file(char* const data) {
_inp.read(data, get_next_file_size());
_cached_header_data_valid = false;
_seek_to_next_header(_inp);
}
void reader::skip_next_file() {
_inp.seekg(get_next_file_size(), std::ios::cur);
_cached_header_data_valid = false;
_seek_to_next_header(_inp);
}
char reader::get_next_file_type() {
_cache_header();
return _cached_header_data.file_type;
}
int reader::number_of_files() {
if (_number_of_files == -1) {
std::streampos current_position = _inp.tellg();
_inp.seekg(0, std::ios::beg);
_number_of_files = 0;
while (contains_another_file()) {
_number_of_files++;
skip_next_file();
}
_inp.seekg(current_position);
}
return _number_of_files;
}
void reader::extract_all_files(std::string dest_directory) {
std::vector<uint8_t> scratch_buffer(1024);
while (contains_another_file()) {
char fileType = get_next_file_type();
auto fileName = get_next_file_name();
// 0 is the normal file type, skip apple's ._ files
if (fileType == '0' && !fileName.starts_with("._")) {
std::string path = dest_directory + "/" + fileName;
size_t pos = 0;
while ((pos = path.find('/', pos)) != std::string::npos) {
std::string dir = path.substr(0, pos);
// Create the directory if it doesn't exist
#ifdef _WIN32
mkdir(dir.c_str());
#else
mkdir(dir.c_str(), 0777);
#endif
pos++;
}
std::ofstream out(path, std::ios::binary);
size_t read_size = 0;
size_t file_size = get_next_file_size();
while (read_size < file_size) {
size_t to_read = std::min(file_size - read_size, scratch_buffer.size());
_inp.read((char*)scratch_buffer.data(), to_read);
// Move the read size forward
read_size += _inp.gcount();
// Write the data to the destination file
out.write((char*)scratch_buffer.data(), _inp.gcount());
}
_cached_header_data_valid = false;
_seek_to_next_header(_inp);
} else {
skip_next_file();
}
}
}