root/src/system/boot/loader/file_systems/tarfs/tarfs.cpp
/*
 * 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>


//#define TRACE_TARFS
#ifdef TRACE_TARFS
#       define TRACE(x) dprintf x
#else
#       define TRACE(x) ;
#endif


static const uint32 kFloppyArchiveOffset = BOOT_ARCHIVE_IMAGE_OFFSET * 1024;
        // defined at build time, see build/jam/BuildSetup
static const size_t kTarRegionSize = 11 * 1024 * 1024; // 11 MB


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);
};

}       // namespace TarFS


static int32 sNextID = 1;


// #pragma mark -


bool
skip_gzip_header(z_stream* stream)
{
        uint8* buffer = (uint8*)stream->next_in;

        // check magic and skip method
        if (buffer[0] != 0x1f || buffer[1] != 0x8b)
                return false;

        // we need the flags field to determine the length of the header
        int flags = buffer[3];

        uint32 offset = 10;

        if ((flags & 0x04) != 0) {
                // skip extra field
                offset += (buffer[offset] | (buffer[offset + 1] << 8)) + 2;
                if (offset >= stream->avail_in)
                        return false;
        }
        if ((flags & 0x08) != 0) {
                // skip original name
                while (buffer[offset++])
                        ;
        }
        if ((flags & 0x10) != 0) {
                // skip comment
                while (buffer[offset++])
                        ;
        }
        if ((flags & 0x02) != 0) {
                // skip CRC
                offset += 2;
        }

        if (offset >= stream->avail_in)
                return false;

        stream->next_in += offset;
        stream->avail_in -= offset;
        return true;
}


// #pragma mark -


TarFS::Entry::Entry(const char* name)
        :
        fName(name),
        fID(sNextID++)
{
}


// #pragma mark -


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;
}


// #pragma mark -

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) {
                // skip slashes
                while (*subDir == '/') {
                        *subDir = '\0';
                        subDir++;
                }

                if (*subDir == '\0') {
                        // a trailing slash
                        subDir = NULL;
                }
        }

        // check, whether the directory does already exist
        Entry* entry = LookupEntry(dirName);
        TarFS::Directory* dir = (entry ? entry->ToTarDirectory() : NULL);
        if (entry) {
                if (!dir)
                        return B_ERROR;
        } else {
                // doesn't exist yet -- create it
                dir = new(nothrow) TarFS::Directory(this, dirName);
                if (!dir)
                        return B_NO_MEMORY;

                fEntries.Add(dir);
        }

        // recursively create the subdirectories
        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;

        // create the parent directory
        TarFS::Directory* dir = this;
        if (dirName) {
                status_t error = AddDirectory(dirName, &dir);
                if (error != B_OK)
                        return error;
        }

        // create the entry
        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;
}


// #pragma mark -


TarFS::Symlink::Symlink(tar_header* header, const char* name)
        : TarFS::Entry(name),
        fHeader(header)
{
        fSize = strnlen(header->linkname, sizeof(header->linkname));
        // null-terminate for sure (might overwrite a byte of the magic)
        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;
}


// #pragma mark -


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);

        // inflate the tar file -- try offset 0 and the archive offset on a floppy
        // disk
        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;

        // parse the tar file
        char* block = (char*)regionDeleter.Get();
        int blockCount = inflatedBytes / BLOCK_SIZE;
        int blockIndex = 0;

        while (blockIndex < blockCount) {
                // check header
                tar_header* header = (tar_header*)(block + blockIndex * BLOCK_SIZE);
                //dump_header(*header);

                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));

                // TODO: this is old-style GNU tar which probably won't work with newer
                // ones...
                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:
                                // this is a long file name
                                // TODO: read long name
                        default:
                                dprintf("tarfs: unsupported file type: %d ('%c')\n",
                                        header->type, header->type);
                                // unsupported type
                                status = B_ERROR;
                                break;
                }

                if (status != B_OK)
                        return status;

                // next block
                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);
                                // Same size as in platform_add_boot_device() (file pxe_ia32/devices.cpp).
                        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,             // next in
                kBufferSize,    // avail in
                0,                              // total in
                NULL,                   // next out
                0,                              // avail out
                0,                              // total out
                0,                              // msg
                0,                              // state
                Z_NULL,                 // zalloc
                Z_NULL,                 // zfree
                Z_NULL,                 // opaque
                0,                              // data type
                0,                              // adler
                0,                              // reserved
        };

        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) {
                        // check and skip gzip header
                        if (!skip_gzip_header(&zStream))
                                return B_BAD_DATA;
                        headerRead = true;

                        if (!out) {
                                // allocate memory for the uncompressed data
                                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;
}


//      #pragma mark -


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,   // identify_file_system
        tarfs_get_file_system
};