419 lines
12 KiB
C++
419 lines
12 KiB
C++
// Copyright (C) 2016 James Turner - <zakalawe@mac.com>
|
|
//
|
|
// This library is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU Library General Public
|
|
// License as published by the Free Software Foundation; either
|
|
// version 2 of the License, or (at your option) any later version.
|
|
//
|
|
// This library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
// Library General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, write to the Free Software
|
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
//
|
|
|
|
#include "untar.hxx"
|
|
|
|
#include <cstdlib>
|
|
#include <cassert>
|
|
#include <stdint.h>
|
|
#include <cstring>
|
|
#include <cstddef>
|
|
#include <algorithm>
|
|
|
|
#include <zlib.h>
|
|
|
|
#include <simgear/io/sg_file.hxx>
|
|
#include <simgear/misc/sg_dir.hxx>
|
|
|
|
#include <simgear/debug/logstream.hxx>
|
|
|
|
namespace simgear
|
|
{
|
|
|
|
const int ZLIB_DECOMPRESS_BUFFER_SIZE = 32 * 1024;
|
|
const int ZLIB_INFLATE_WINDOW_BITS = MAX_WBITS;
|
|
const int ZLIB_DECODE_GZIP_HEADER = 16;
|
|
|
|
/* tar Header Block, from POSIX 1003.1-1990. */
|
|
|
|
typedef struct
|
|
{
|
|
char fileName[100];
|
|
char mode[8]; /* 100 */
|
|
char uid[8]; /* 108 */
|
|
char gid[8]; /* 116 */
|
|
char size[12]; /* 124 */
|
|
char mtime[12]; /* 136 */
|
|
char chksum[8]; /* 148 */
|
|
char typeflag; /* 156 */
|
|
char linkname[100]; /* 157 */
|
|
char magic[6]; /* 257 */
|
|
char version[2]; /* 263 */
|
|
char uname[32]; /* 265 */
|
|
char gname[32]; /* 297 */
|
|
char devmajor[8]; /* 329 */
|
|
char devminor[8]; /* 337 */
|
|
char prefix[155]; /* 345 */
|
|
} UstarHeaderBlock;
|
|
|
|
const size_t TAR_HEADER_BLOCK_SIZE = 512;
|
|
|
|
#define TMAGIC "ustar" /* ustar and a null */
|
|
#define TMAGLEN 5 // 5, not 6, becuase some files use 'ustar '
|
|
#define TVERSION "00" /* 00 and no null */
|
|
#define TVERSLEN 2
|
|
|
|
/* Values used in typeflag field. */
|
|
#define REGTYPE '0' /* regular file */
|
|
#define AREGTYPE '\0' /* regular file */
|
|
#define LNKTYPE '1' /* link */
|
|
#define SYMTYPE '2' /* reserved */
|
|
#define CHRTYPE '3' /* character special */
|
|
#define BLKTYPE '4' /* block special */
|
|
#define DIRTYPE '5' /* directory */
|
|
#define FIFOTYPE '6' /* FIFO special */
|
|
#define CONTTYPE '7' /* reserved */
|
|
|
|
class TarExtractorPrivate
|
|
{
|
|
public:
|
|
typedef enum {
|
|
INVALID = 0,
|
|
READING_HEADER,
|
|
READING_FILE,
|
|
READING_PADDING,
|
|
PRE_END_OF_ARCHVE,
|
|
END_OF_ARCHIVE,
|
|
ERROR_STATE, ///< states above this are error conditions
|
|
BAD_ARCHIVE,
|
|
BAD_DATA
|
|
} State;
|
|
|
|
SGPath path;
|
|
State state;
|
|
union {
|
|
UstarHeaderBlock header;
|
|
uint8_t headerBytes[TAR_HEADER_BLOCK_SIZE];
|
|
};
|
|
|
|
size_t bytesRemaining;
|
|
std::auto_ptr<SGFile> currentFile;
|
|
size_t currentFileSize;
|
|
z_stream zlibStream;
|
|
uint8_t* zlibOutput;
|
|
bool haveInitedZLib;
|
|
bool uncompressedData; // set if reading a plain .tar (not tar.gz)
|
|
uint8_t* headerPtr;
|
|
|
|
TarExtractorPrivate() :
|
|
haveInitedZLib(false),
|
|
uncompressedData(false)
|
|
{
|
|
}
|
|
|
|
~TarExtractorPrivate()
|
|
{
|
|
free(zlibOutput);
|
|
}
|
|
|
|
void checkEndOfState()
|
|
{
|
|
if (bytesRemaining > 0) {
|
|
return;
|
|
}
|
|
|
|
if (state == READING_FILE) {
|
|
currentFile->close();
|
|
size_t pad = currentFileSize % TAR_HEADER_BLOCK_SIZE;
|
|
if (pad) {
|
|
bytesRemaining = TAR_HEADER_BLOCK_SIZE - pad;
|
|
setState(READING_PADDING);
|
|
} else {
|
|
setState(READING_HEADER);
|
|
}
|
|
} else if (state == READING_HEADER) {
|
|
processHeader();
|
|
} else if (state == PRE_END_OF_ARCHVE) {
|
|
if (headerIsAllZeros()) {
|
|
setState(END_OF_ARCHIVE);
|
|
} else {
|
|
// what does the spec say here?
|
|
}
|
|
} else if (state == READING_PADDING) {
|
|
setState(READING_HEADER);
|
|
}
|
|
}
|
|
|
|
void setState(State newState)
|
|
{
|
|
if ((newState == READING_HEADER) || (newState == PRE_END_OF_ARCHVE)) {
|
|
bytesRemaining = TAR_HEADER_BLOCK_SIZE;
|
|
headerPtr = headerBytes;
|
|
}
|
|
|
|
state = newState;
|
|
}
|
|
|
|
void processHeader()
|
|
{
|
|
if (headerIsAllZeros()) {
|
|
if (state == PRE_END_OF_ARCHVE) {
|
|
setState(END_OF_ARCHIVE);
|
|
} else {
|
|
setState(PRE_END_OF_ARCHVE);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (strncmp(header.magic, TMAGIC, TMAGLEN) != 0) {
|
|
SG_LOG(SG_IO, SG_WARN, "magic is wrong");
|
|
state = BAD_ARCHIVE;
|
|
return;
|
|
}
|
|
|
|
std::string tarPath = std::string(header.prefix) + std::string(header.fileName);
|
|
|
|
if (!isSafePath(tarPath)) {
|
|
//state = BAD_ARCHIVE;
|
|
SG_LOG(SG_IO, SG_WARN, "bad tar path:" << tarPath);
|
|
//return;
|
|
}
|
|
|
|
SGPath p = path;
|
|
p.append(tarPath);
|
|
|
|
if (header.typeflag == DIRTYPE) {
|
|
Dir dir(p);
|
|
dir.create(0755);
|
|
setState(READING_HEADER);
|
|
} else if ((header.typeflag == REGTYPE) || (header.typeflag == AREGTYPE)) {
|
|
currentFileSize = ::strtol(header.size, NULL, 8);
|
|
bytesRemaining = currentFileSize;
|
|
currentFile.reset(new SGBinaryFile(p));
|
|
currentFile->open(SG_IO_OUT);
|
|
setState(READING_FILE);
|
|
} else {
|
|
SG_LOG(SG_IO, SG_WARN, "Unsupported tar file type:" << header.typeflag);
|
|
state = BAD_ARCHIVE;
|
|
}
|
|
}
|
|
|
|
void processBytes(const char* bytes, size_t count)
|
|
{
|
|
if ((state >= ERROR_STATE) || (state == END_OF_ARCHIVE)) {
|
|
return;
|
|
}
|
|
|
|
size_t curBytes = std::min(bytesRemaining, count);
|
|
if (state == READING_FILE) {
|
|
currentFile->write(bytes, curBytes);
|
|
bytesRemaining -= curBytes;
|
|
} else if ((state == READING_HEADER) || (state == PRE_END_OF_ARCHVE) || (state == END_OF_ARCHIVE)) {
|
|
memcpy(headerPtr, bytes, curBytes);
|
|
bytesRemaining -= curBytes;
|
|
headerPtr += curBytes;
|
|
} else if (state == READING_PADDING) {
|
|
bytesRemaining -= curBytes;
|
|
}
|
|
|
|
checkEndOfState();
|
|
if (count > curBytes) {
|
|
// recurse with unprocessed bytes
|
|
processBytes(bytes + curBytes, count - curBytes);
|
|
}
|
|
}
|
|
|
|
bool headerIsAllZeros() const
|
|
{
|
|
char* headerAsChar = (char*) &header;
|
|
for (size_t i=0; i < offsetof(UstarHeaderBlock, magic); ++i) {
|
|
if (*headerAsChar++ != 0) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool isSafePath(const std::string& p) const
|
|
{
|
|
if (p.empty()) {
|
|
return false;
|
|
}
|
|
|
|
// reject absolute paths
|
|
if (p.at(0) == '/') {
|
|
return false;
|
|
}
|
|
|
|
// reject paths containing '..'
|
|
size_t doubleDot = p.find("..");
|
|
if (doubleDot != std::string::npos) {
|
|
return false;
|
|
}
|
|
|
|
// on POSIX could use realpath to sanity check
|
|
return true;
|
|
}
|
|
};
|
|
|
|
TarExtractor::TarExtractor(const SGPath& rootPath) :
|
|
d(new TarExtractorPrivate)
|
|
{
|
|
|
|
d->path = rootPath;
|
|
d->state = TarExtractorPrivate::INVALID;
|
|
|
|
memset(&d->zlibStream, 0, sizeof(z_stream));
|
|
d->zlibOutput = (unsigned char*) malloc(ZLIB_DECOMPRESS_BUFFER_SIZE);
|
|
d->zlibStream.zalloc = Z_NULL;
|
|
d->zlibStream.zfree = Z_NULL;
|
|
|
|
d->zlibStream.avail_out = ZLIB_DECOMPRESS_BUFFER_SIZE;
|
|
d->zlibStream.next_out = d->zlibOutput;
|
|
}
|
|
|
|
TarExtractor::~TarExtractor()
|
|
{
|
|
|
|
}
|
|
|
|
void TarExtractor::extractBytes(const char* bytes, size_t count)
|
|
{
|
|
if (d->state >= TarExtractorPrivate::ERROR_STATE) {
|
|
return;
|
|
}
|
|
|
|
d->zlibStream.next_in = (uint8_t*) bytes;
|
|
d->zlibStream.avail_in = count;
|
|
|
|
if (!d->haveInitedZLib) {
|
|
// now we have data, see if we're dealing with GZ-compressed data or not
|
|
uint8_t* ubytes = (uint8_t*) bytes;
|
|
if ((ubytes[0] == 0x1f) && (ubytes[1] == 0x8b)) {
|
|
// GZIP identification bytes
|
|
if (inflateInit2(&d->zlibStream, ZLIB_INFLATE_WINDOW_BITS | ZLIB_DECODE_GZIP_HEADER) != Z_OK) {
|
|
SG_LOG(SG_IO, SG_WARN, "inflateInit2 failed");
|
|
d->state = TarExtractorPrivate::BAD_DATA;
|
|
return;
|
|
}
|
|
} else {
|
|
UstarHeaderBlock* header = (UstarHeaderBlock*) bytes;
|
|
if (strncmp(header->magic, TMAGIC, TMAGLEN) != 0) {
|
|
SG_LOG(SG_IO, SG_WARN, "didn't find tar magic in header");
|
|
d->state = TarExtractorPrivate::BAD_DATA;
|
|
return;
|
|
}
|
|
|
|
d->uncompressedData = true;
|
|
}
|
|
|
|
d->haveInitedZLib = true;
|
|
d->setState(TarExtractorPrivate::READING_HEADER);
|
|
} // of init on first-bytes case
|
|
|
|
if (d->uncompressedData) {
|
|
d->processBytes(bytes, count);
|
|
} else {
|
|
size_t writtenSize;
|
|
// loop, running zlib() inflate and sending output bytes to
|
|
// our request body handler. Keep calling inflate until no bytes are
|
|
// written, and ZLIB has consumed all available input
|
|
do {
|
|
d->zlibStream.next_out = d->zlibOutput;
|
|
d->zlibStream.avail_out = ZLIB_DECOMPRESS_BUFFER_SIZE;
|
|
int result = inflate(&d->zlibStream, Z_NO_FLUSH);
|
|
if (result == Z_OK || result == Z_STREAM_END) {
|
|
// nothing to do
|
|
|
|
} else if (result == Z_BUF_ERROR) {
|
|
// transient error, fall through
|
|
} else {
|
|
// _error = result;
|
|
SG_LOG(SG_IO, SG_WARN, "Permanent ZLib error:" << d->zlibStream.msg);
|
|
d->state = TarExtractorPrivate::BAD_DATA;
|
|
return;
|
|
}
|
|
|
|
writtenSize = ZLIB_DECOMPRESS_BUFFER_SIZE - d->zlibStream.avail_out;
|
|
if (writtenSize > 0) {
|
|
d->processBytes((const char*) d->zlibOutput, writtenSize);
|
|
}
|
|
|
|
if (result == Z_STREAM_END) {
|
|
break;
|
|
}
|
|
} while ((d->zlibStream.avail_in > 0) || (writtenSize > 0));
|
|
} // of Zlib-compressed data
|
|
}
|
|
|
|
bool TarExtractor::isAtEndOfArchive() const
|
|
{
|
|
return (d->state == TarExtractorPrivate::END_OF_ARCHIVE);
|
|
}
|
|
|
|
bool TarExtractor::hasError() const
|
|
{
|
|
return (d->state >= TarExtractorPrivate::ERROR_STATE);
|
|
}
|
|
|
|
bool TarExtractor::isTarData(const uint8_t* bytes, size_t count)
|
|
{
|
|
if (count < 2) {
|
|
return false;
|
|
}
|
|
|
|
UstarHeaderBlock* header = 0;
|
|
if ((bytes[0] == 0x1f) && (bytes[1] == 0x8b)) {
|
|
// GZIP identification bytes
|
|
z_stream z;
|
|
uint8_t* zlibOutput = static_cast<uint8_t*>(alloca(4096));
|
|
memset(&z, 0, sizeof(z_stream));
|
|
z.zalloc = Z_NULL;
|
|
z.zfree = Z_NULL;
|
|
z.avail_out = 4096;
|
|
z.next_out = zlibOutput;
|
|
z.next_in = (uint8_t*) bytes;
|
|
z.avail_in = count;
|
|
|
|
if (inflateInit2(&z, ZLIB_INFLATE_WINDOW_BITS | ZLIB_DECODE_GZIP_HEADER) != Z_OK) {
|
|
return false;
|
|
}
|
|
|
|
int result = inflate(&z, Z_SYNC_FLUSH);
|
|
if (result != Z_OK) {
|
|
SG_LOG(SG_IO, SG_WARN, "inflate failed:" << result);
|
|
return false; // not tar data
|
|
}
|
|
|
|
size_t written = 4096 - z.avail_out;
|
|
if (written < TAR_HEADER_BLOCK_SIZE) {
|
|
SG_LOG(SG_IO, SG_WARN, "insufficient data for header");
|
|
return false;
|
|
}
|
|
|
|
header = reinterpret_cast<UstarHeaderBlock*>(zlibOutput);
|
|
} else {
|
|
// uncompressed tar
|
|
if (count < TAR_HEADER_BLOCK_SIZE) {
|
|
SG_LOG(SG_IO, SG_WARN, "insufficient data for header");
|
|
return false;
|
|
}
|
|
|
|
header = (UstarHeaderBlock*) bytes;
|
|
}
|
|
|
|
if (strncmp(header->magic, TMAGIC, TMAGLEN) != 0) {
|
|
SG_LOG(SG_IO, SG_WARN, "not a tar file");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // of simgear
|