* Copyright 2005-2007, Ingo Weinhold, bonefish@cs.tu-berlin.de.
* Copyright 2005-2013, Axel Dörfler, axeld@pinc-software.de.
*
* Distributed under the terms of the MIT License.
*/
#include "tarfs.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <AutoDeleter.h>
#include <OS.h>
#include <SupportDefs.h>
#include <zlib.h>
#include <boot/partitions.h>
#include <boot/platform.h>
#include <boot/stage2.h>
#include <util/DoublyLinkedList.h>
#ifdef TRACE_TARFS
# define TRACE(x) dprintf x
#else
# define TRACE(x) ;
#endif
static const uint32 kFloppyArchiveOffset = BOOT_ARCHIVE_IMAGE_OFFSET * 1024;
static const size_t kTarRegionSize = 11 * 1024 * 1024;
using std::nothrow;
namespace TarFS {
struct RegionDelete {
inline void operator()(void* memory)
{
if (memory != NULL)
platform_free_region(memory, kTarRegionSize);
}
};
typedef BPrivate::AutoDeleter<void, RegionDelete> RegionDeleter;
class Directory;
class Entry : public DoublyLinkedListLinkImpl<Entry> {
public:
Entry(const char* name);
virtual ~Entry() {}
const char* Name() const { return fName; }
virtual ::Node* ToNode() = 0;
virtual TarFS::Directory* ToTarDirectory() { return NULL; }
protected:
const char* fName;
int32 fID;
};
typedef DoublyLinkedList<TarFS::Entry> EntryList;
typedef EntryList::Iterator EntryIterator;
class File : public ::Node, public Entry {
public:
File(tar_header* header, const char* name);
virtual ~File();
virtual ssize_t ReadAt(void* cookie, off_t pos, void* buffer,
size_t bufferSize);
virtual ssize_t WriteAt(void* cookie, off_t pos,
const void* buffer, size_t bufferSize);
virtual status_t GetName(char* nameBuffer,
size_t bufferSize) const;
virtual int32 Type() const;
virtual off_t Size() const;
virtual ino_t Inode() const;
virtual ::Node* ToNode() { return this; }
private:
tar_header* fHeader;
off_t fSize;
};
class Directory : public ::Directory, public Entry {
public:
Directory(Directory* parent, const char* name);
virtual ~Directory();
virtual status_t Open(void** _cookie, int mode);
virtual status_t Close(void* cookie);
virtual status_t GetName(char* nameBuffer,
size_t bufferSize) const;
virtual TarFS::Entry* LookupEntry(const char* name);
virtual ::Node* LookupDontTraverse(const char* name);
virtual status_t GetNextEntry(void* cookie, char* nameBuffer,
size_t bufferSize);
virtual status_t GetNextNode(void* cookie, Node** _node);
virtual status_t Rewind(void* cookie);
virtual bool IsEmpty();
virtual ino_t Inode() const;
virtual ::Node* ToNode() { return this; };
virtual TarFS::Directory* ToTarDirectory() { return this; }
status_t AddDirectory(char* dirName,
TarFS::Directory** _dir = NULL);
status_t AddFile(tar_header* header);
private:
typedef ::Directory _inherited;
Directory* fParent;
EntryList fEntries;
};
class Symlink : public ::Node, public Entry {
public:
Symlink(tar_header* header, const char* name);
virtual ~Symlink();
virtual ssize_t ReadAt(void* cookie, off_t pos, void* buffer,
size_t bufferSize);
virtual ssize_t WriteAt(void* cookie, off_t pos,
const void* buffer, size_t bufferSize);
virtual status_t ReadLink(char* buffer, size_t bufferSize);
virtual status_t GetName(char* nameBuffer,
size_t bufferSize) const;
virtual int32 Type() const;
virtual off_t Size() const;
virtual ino_t Inode() const;
const char* LinkPath() const { return fHeader->linkname; }
virtual ::Node* ToNode() { return this; }
private:
tar_header* fHeader;
size_t fSize;
};
class Volume : public TarFS::Directory {
public:
Volume();
~Volume();
status_t Init(boot::Partition* partition);
TarFS::Directory* Root() { return this; }
private:
status_t _Inflate(boot::Partition* partition,
void* cookie, off_t offset,
RegionDeleter& regionDeleter,
size_t* inflatedBytes);
};
}
static int32 sNextID = 1;
bool
skip_gzip_header(z_stream* stream)
{
uint8* buffer = (uint8*)stream->next_in;
if (buffer[0] != 0x1f || buffer[1] != 0x8b)
return false;
int flags = buffer[3];
uint32 offset = 10;
if ((flags & 0x04) != 0) {
offset += (buffer[offset] | (buffer[offset + 1] << 8)) + 2;
if (offset >= stream->avail_in)
return false;
}
if ((flags & 0x08) != 0) {
while (buffer[offset++])
;
}
if ((flags & 0x10) != 0) {
while (buffer[offset++])
;
}
if ((flags & 0x02) != 0) {
offset += 2;
}
if (offset >= stream->avail_in)
return false;
stream->next_in += offset;
stream->avail_in -= offset;
return true;
}
TarFS::Entry::Entry(const char* name)
:
fName(name),
fID(sNextID++)
{
}
TarFS::File::File(tar_header* header, const char* name)
: TarFS::Entry(name),
fHeader(header)
{
fSize = strtol(header->size, NULL, 8);
}
TarFS::File::~File()
{
}
ssize_t
TarFS::File::ReadAt(void* cookie, off_t pos, void* buffer, size_t bufferSize)
{
TRACE(("tarfs: read at %" B_PRIdOFF ", %" B_PRIuSIZE " bytes, fSize = %"
B_PRIdOFF "\n", pos, bufferSize, fSize));
if (pos < 0 || !buffer)
return B_BAD_VALUE;
if (pos >= fSize || bufferSize == 0)
return 0;
size_t toRead = fSize - pos;
if (toRead > bufferSize)
toRead = bufferSize;
memcpy(buffer, (char*)fHeader + BLOCK_SIZE + pos, toRead);
return toRead;
}
ssize_t
TarFS::File::WriteAt(void* cookie, off_t pos, const void* buffer,
size_t bufferSize)
{
return B_NOT_ALLOWED;
}
status_t
TarFS::File::GetName(char* nameBuffer, size_t bufferSize) const
{
return strlcpy(nameBuffer, Name(), bufferSize) >= bufferSize
? B_BUFFER_OVERFLOW : B_OK;
}
int32
TarFS::File::Type() const
{
return S_IFREG;
}
off_t
TarFS::File::Size() const
{
return fSize;
}
ino_t
TarFS::File::Inode() const
{
return fID;
}
TarFS::Directory::Directory(Directory* parent, const char* name)
:
TarFS::Entry(name),
fParent(parent)
{
}
TarFS::Directory::~Directory()
{
while (TarFS::Entry* entry = fEntries.Head()) {
fEntries.Remove(entry);
delete entry;
}
}
status_t
TarFS::Directory::Open(void** _cookie, int mode)
{
_inherited::Open(_cookie, mode);
EntryIterator* iterator
= new(nothrow) EntryIterator(fEntries.GetIterator());
if (iterator == NULL)
return B_NO_MEMORY;
*_cookie = iterator;
return B_OK;
}
status_t
TarFS::Directory::Close(void* cookie)
{
_inherited::Close(cookie);
delete (EntryIterator*)cookie;
return B_OK;
}
status_t
TarFS::Directory::GetName(char* nameBuffer, size_t bufferSize) const
{
return strlcpy(nameBuffer, Name(), bufferSize) >= bufferSize
? B_BUFFER_OVERFLOW : B_OK;
}
TarFS::Entry*
TarFS::Directory::LookupEntry(const char* name)
{
if (strcmp(name, ".") == 0)
return this;
if (strcmp(name, "..") == 0)
return fParent;
EntryIterator iterator(fEntries.GetIterator());
while (iterator.HasNext()) {
TarFS::Entry* entry = iterator.Next();
if (strcmp(name, entry->Name()) == 0)
return entry;
}
return NULL;
}
::Node*
TarFS::Directory::LookupDontTraverse(const char* name)
{
TarFS::Entry* entry = LookupEntry(name);
if (!entry)
return NULL;
Node* node = entry->ToNode();
if (node)
node->Acquire();
return node;
}
status_t
TarFS::Directory::GetNextEntry(void* _cookie, char* name, size_t size)
{
EntryIterator* iterator = (EntryIterator*)_cookie;
TarFS::Entry* entry = iterator->Next();
if (entry != NULL) {
strlcpy(name, entry->Name(), size);
return B_OK;
}
return B_ENTRY_NOT_FOUND;
}
status_t
TarFS::Directory::GetNextNode(void* _cookie, Node** _node)
{
EntryIterator* iterator = (EntryIterator*)_cookie;
TarFS::Entry* entry = iterator->Next();
if (entry != NULL) {
*_node = entry->ToNode();
return B_OK;
}
return B_ENTRY_NOT_FOUND;
}
status_t
TarFS::Directory::Rewind(void* _cookie)
{
EntryIterator* iterator = (EntryIterator*)_cookie;
*iterator = fEntries.GetIterator();
return B_OK;
}
status_t
TarFS::Directory::AddDirectory(char* dirName, TarFS::Directory** _dir)
{
char* subDir = strchr(dirName, '/');
if (subDir) {
while (*subDir == '/') {
*subDir = '\0';
subDir++;
}
if (*subDir == '\0') {
subDir = NULL;
}
}
Entry* entry = LookupEntry(dirName);
TarFS::Directory* dir = (entry ? entry->ToTarDirectory() : NULL);
if (entry) {
if (!dir)
return B_ERROR;
} else {
dir = new(nothrow) TarFS::Directory(this, dirName);
if (!dir)
return B_NO_MEMORY;
fEntries.Add(dir);
}
if (subDir) {
status_t error = dir->AddDirectory(subDir, &dir);
if (error != B_OK)
return error;
}
if (_dir)
*_dir = dir;
return B_OK;
}
status_t
TarFS::Directory::AddFile(tar_header* header)
{
char* leaf = strrchr(header->name, '/');
char* dirName = NULL;
if (leaf) {
dirName = header->name;
*leaf = '\0';
leaf++;
} else
leaf = header->name;
TarFS::Directory* dir = this;
if (dirName) {
status_t error = AddDirectory(dirName, &dir);
if (error != B_OK)
return error;
}
TarFS::Entry* entry;
if (header->type == TAR_FILE || header->type == TAR_FILE2)
entry = new(nothrow) TarFS::File(header, leaf);
else if (header->type == TAR_SYMLINK)
entry = new(nothrow) TarFS::Symlink(header, leaf);
else
return B_BAD_VALUE;
if (!entry)
return B_NO_MEMORY;
dir->fEntries.Add(entry);
return B_OK;
}
bool
TarFS::Directory::IsEmpty()
{
return fEntries.IsEmpty();
}
ino_t
TarFS::Directory::Inode() const
{
return fID;
}
TarFS::Symlink::Symlink(tar_header* header, const char* name)
: TarFS::Entry(name),
fHeader(header)
{
fSize = strnlen(header->linkname, sizeof(header->linkname));
header->linkname[fSize++] = '\0';
}
TarFS::Symlink::~Symlink()
{
}
ssize_t
TarFS::Symlink::ReadAt(void* cookie, off_t pos, void* buffer, size_t bufferSize)
{
return B_NOT_ALLOWED;
}
ssize_t
TarFS::Symlink::WriteAt(void* cookie, off_t pos, const void* buffer,
size_t bufferSize)
{
return B_NOT_ALLOWED;
}
status_t
TarFS::Symlink::ReadLink(char* buffer, size_t bufferSize)
{
const char* path = fHeader->linkname;
size_t size = strlen(path) + 1;
if (size > bufferSize)
return B_BUFFER_OVERFLOW;
memcpy(buffer, path, size);
return B_OK;
}
status_t
TarFS::Symlink::GetName(char* nameBuffer, size_t bufferSize) const
{
return strlcpy(nameBuffer, Name(), bufferSize) >= bufferSize
? B_BUFFER_OVERFLOW : B_OK;
}
int32
TarFS::Symlink::Type() const
{
return S_IFLNK;
}
off_t
TarFS::Symlink::Size() const
{
return fSize;
}
ino_t
TarFS::Symlink::Inode() const
{
return fID;
}
TarFS::Volume::Volume()
:
TarFS::Directory(this, "Boot from CD-ROM")
{
}
TarFS::Volume::~Volume()
{
}
status_t
TarFS::Volume::Init(boot::Partition* partition)
{
void* cookie;
status_t error = partition->Open(&cookie, O_RDONLY);
if (error != B_OK)
return error;
struct PartitionCloser {
boot::Partition *partition;
void *cookie;
PartitionCloser(boot::Partition* partition, void* cookie)
: partition(partition),
cookie(cookie)
{
}
~PartitionCloser()
{
partition->Close(cookie);
}
} _(partition, cookie);
RegionDeleter regionDeleter;
size_t inflatedBytes;
status_t status = _Inflate(partition, cookie, 0, regionDeleter,
&inflatedBytes);
if (status != B_OK) {
status = _Inflate(partition, cookie, kFloppyArchiveOffset,
regionDeleter, &inflatedBytes);
}
if (status != B_OK)
return status;
char* block = (char*)regionDeleter.Get();
int blockCount = inflatedBytes / BLOCK_SIZE;
int blockIndex = 0;
while (blockIndex < blockCount) {
tar_header* header = (tar_header*)(block + blockIndex * BLOCK_SIZE);
if (header->magic[0] == '\0')
break;
if (strcmp(header->magic, kTarHeaderMagic) != 0) {
if (strcmp(header->magic, kOldTarHeaderMagic) != 0) {
dprintf("Bad tar header magic in block %d.\n", blockIndex);
status = B_BAD_DATA;
break;
}
}
off_t size = strtol(header->size, NULL, 8);
TRACE(("tarfs: \"%s\", %" B_PRIdOFF " bytes\n", header->name, size));
switch (header->type) {
case TAR_FILE:
case TAR_FILE2:
case TAR_SYMLINK:
status = AddFile(header);
break;
case TAR_DIRECTORY:
status = AddDirectory(header->name, NULL);
break;
case TAR_LONG_NAME:
default:
dprintf("tarfs: unsupported file type: %d ('%c')\n",
header->type, header->type);
status = B_ERROR;
break;
}
if (status != B_OK)
return status;
blockIndex += (size + 2 * BLOCK_SIZE - 1) / BLOCK_SIZE;
}
if (status != B_OK)
return status;
regionDeleter.Detach();
int32 bootMethod = gBootParams.GetInt32(BOOT_METHOD, BOOT_METHOD_DEFAULT);
switch (bootMethod) {
case BOOT_METHOD_CD:
fName = "CD-ROM";
break;
case BOOT_METHOD_NET:
fName = (char*)malloc(64);
get_node_from(partition->FD())->GetName((char*)fName, 64);
break;
}
return B_OK;
}
status_t
TarFS::Volume::_Inflate(boot::Partition* partition, void* cookie, off_t offset,
RegionDeleter& regionDeleter, size_t* inflatedBytes)
{
static const int kBufferSize = 2048;
char* in = (char*)malloc(kBufferSize);
if (in == NULL)
return B_NO_MEMORY;
MemoryDeleter deleter(in);
z_stream zStream = {
(Bytef*)in,
kBufferSize,
0,
NULL,
0,
0,
0,
0,
Z_NULL,
Z_NULL,
Z_NULL,
0,
0,
0,
};
int status;
char* out = (char*)regionDeleter.Get();
bool headerRead = false;
do {
ssize_t bytesRead = partition->ReadAt(cookie, offset, in, kBufferSize);
if (bytesRead != (ssize_t)sizeof(in)) {
if (bytesRead <= 0) {
status = Z_STREAM_ERROR;
break;
}
}
zStream.avail_in = bytesRead;
zStream.next_in = (Bytef*)in;
if (!headerRead) {
if (!skip_gzip_header(&zStream))
return B_BAD_DATA;
headerRead = true;
if (!out) {
if (platform_allocate_region((void**)&out, kTarRegionSize,
B_READ_AREA | B_WRITE_AREA) != B_OK) {
TRACE(("tarfs: allocating region failed!\n"));
return B_NO_MEMORY;
}
regionDeleter.SetTo(out);
}
zStream.avail_out = kTarRegionSize;
zStream.next_out = (Bytef*)out;
status = inflateInit2(&zStream, -15);
if (status != Z_OK)
return B_ERROR;
}
status = inflate(&zStream, Z_SYNC_FLUSH);
offset += bytesRead;
if (zStream.avail_in != 0 && status != Z_STREAM_END)
dprintf("tarfs: didn't read whole block: %s\n", zStream.msg);
} while (status == Z_OK);
inflateEnd(&zStream);
if (status != Z_STREAM_END) {
TRACE(("tarfs: inflating failed: %d!\n", status));
return B_BAD_DATA;
}
*inflatedBytes = zStream.total_out;
return B_OK;
}
static status_t
tarfs_get_file_system(boot::Partition* partition, ::Directory** _root)
{
TarFS::Volume* volume = new(nothrow) TarFS::Volume;
if (volume == NULL)
return B_NO_MEMORY;
if (volume->Init(partition) < B_OK) {
TRACE(("Initializing tarfs failed\n"));
delete volume;
return B_ERROR;
}
*_root = volume->Root();
return B_OK;
}
file_system_module_info gTarFileSystemModule = {
"file_systems/tarfs/v1",
kPartitionTypeTarFS,
NULL,
tarfs_get_file_system
};