/*
 * \brief  TAR file system
 * \author Norman Feske
 * \date   2011-02-17
 */

/*
 * Copyright (C) 2011-2017 Genode Labs GmbH
 *
 * This file is part of the Genode OS framework, which is distributed
 * under the terms of the GNU Affero General Public License version 3.
 */

#ifndef _INCLUDE__VFS__TAR_FILE_SYSTEM_H_
#define _INCLUDE__VFS__TAR_FILE_SYSTEM_H_

#include <rom_session/connection.h>
#include <vfs/file_system.h>
#include <vfs/vfs_handle.h>
#include <base/attached_rom_dataspace.h>

namespace Vfs_tar {

	using namespace Genode;
	using namespace Genode::Vfs;

	class Record;
	class File_system;
}


class Vfs_tar::Record
{
	private:

		char _name[100];
		char _mode[8];
		char _uid[8];
		char _gid[8];
		char _size[12];
		char _mtime[12];
		char _checksum[8];
		char _type[1];
		char _linked_name[100];

		/**
		 * Convert ASCII-encoded octal number to unsigned value
		 */
		template <typename T>
		unsigned long _read(T const &field) const
		{
			/*
			 * Copy-out ASCII string to temporary buffer that is
			 * large enough to host an additional zero.
			 */
			char buf[sizeof(field) + 1];
			copy_cstring(buf, field, sizeof(buf));

			unsigned long value = 0;
			ascii_to_unsigned(buf, value, 8);
			return value;
		}

		char const *_data_begin() const { return (char const *)this + BLOCK_LEN; }

		/*
		 * GNU extension for long path names, which support unlimited sizes using
		 * separate records
		 */
		bool _long_name() const
		{
			 return _type[0] == TYPE_LONG_LINK || _type[0] == TYPE_LONG_NAME;
		}

		/*
		 * Round up up next block
		 */
		file_size _block_align(file_size size) const {
			return align_addr(size, { .log2 = BLOCK_SHIFT }); }

		/*
		 * Next record header
		 */
		Record *_next() const {
			return (Record *)(_data_begin() + _block_align(_read(_size))); }

	public:

		/* length of one data block in tar */
		enum {
			BLOCK_SHIFT = 9, /* 512 bytes */
			BLOCK_LEN   = 1ul << BLOCK_SHIFT,
		};

		/* record type values */
		enum {
			TYPE_FILE = 0, TYPE_HARDLINK = 1, TYPE_SYMLINK = 2, TYPE_DIR = 5,
			/* GNU extensions */
			TYPE_LONG_LINK = 75, TYPE_LONG_NAME = 76
		};

		file_size  size() const { return _long_name() ? _next()->size()  : _read(_size);  }
		long long mtime() const { return _long_name() ? _next()->mtime() : _read(_mtime); }
		unsigned    uid() const { return _long_name() ? _next()->uid()   : (unsigned)_read(_uid); }
		unsigned    gid() const { return _long_name() ? _next()->gid()   : (unsigned)_read(_gid); }
		unsigned   mode() const { return _long_name() ? _next()->mode()  : (unsigned)_read(_mode); }
		unsigned   type() const { return _long_name() ? _next()->type()  : (unsigned)_read(_type); }
		void      *data() const { return _long_name() ? _next()->data()  : (void *)_data_begin(); }

		Node_rwx rwx() const
		{
			unsigned const mode_bits = mode();

			return { .readable   = (mode_bits & 0400) != 0,
			         .writeable  = (mode_bits & 0200) != 0,
			         .executable = (mode_bits & 0100) != 0 };
		}

		char const         *name() const { return _long_name() ? _data_begin() : _name;        }
		unsigned    max_name_len() const { return _long_name() ? MAX_PATH_LEN : 100;           }
		char const *linked_name()  const { return _long_name() ? _data_begin() : _linked_name; }

		file_size storage_size()
		{
			if (_long_name()) {
				/* this size + next header + next size */
				return _block_align(_read(_size)) + BLOCK_LEN + _block_align(_next()->size());
			}

			return _read(_size);
		}
};


class Vfs_tar::File_system : public Vfs::File_system
{
	Genode::Env &_env;
	Allocator   &_alloc;

	using Rom_name = String<64>;
	Rom_name _rom_name;

	Attached_rom_dataspace _tar_ds { _env, _rom_name.string() };
	char                  *_tar_base = _tar_ds.local_addr<char>();
	file_size       const  _tar_size = _tar_ds.size();

	/*
	 * Noncopyable
	 */
	File_system(File_system const &);
	File_system &operator = (File_system const &);

	class Node;

	class Tar_vfs_handle : public Vfs_handle
	{
		private:

			/*
			 * Noncopyable
			 */
			Tar_vfs_handle(Tar_vfs_handle const &);
			Tar_vfs_handle &operator = (Tar_vfs_handle const &);

		protected:

			File_system &_fs;

			Node const *_node;

		public:

			Tar_vfs_handle(File_system &fs, Allocator &alloc, int status_flags,
			               Node const *node)
			: Vfs_handle(fs, alloc, status_flags), _fs(fs), _node(node)
			{ }

			virtual Read_result read(Byte_range_ptr const &dst, size_t &out_count) = 0;

			Read_result complete_read(Byte_range_ptr const &dst,
			                          size_t &out_count) override
			{
				out_count = 0;
				return read(dst, out_count);
			}

		bool read_ready () const override { return true; }
		bool write_ready() const override { return false; }
	};


	struct Tar_vfs_file_handle : Tar_vfs_handle
	{
		using Tar_vfs_handle::Tar_vfs_handle;

		Read_result read(Byte_range_ptr const &dst, size_t &out_count) override
		{
			file_size const record_size = _node->record->size();

			file_size const record_bytes_left = record_size >= seek()
			                                  ? record_size  - seek() : 0;

			size_t const count = min(size_t(record_bytes_left), dst.num_bytes);

			char const *data = (char *)_node->record->data() + seek();

			memcpy(dst.start, data, count);

			out_count = count;
			return READ_OK;
		}
	};

	struct Tar_vfs_dir_handle : Tar_vfs_handle
	{
		using Tar_vfs_handle::Tar_vfs_handle;

		Read_result read(Byte_range_ptr const &dst, size_t &out_count) override
		{
			if (dst.num_bytes < sizeof(Dirent))
				return READ_ERR_INVALID;

			Dirent &dirent = *(Dirent*)dst.start;

			unsigned const index = (unsigned)(seek() / sizeof(Dirent));

			Node const *node_ptr = _node->lookup_child(index);

			if (!node_ptr) {
				dirent = Dirent { };
				out_count = 0;
				return READ_OK;
			}

			Node const &node = *node_ptr;

			Record const *record_ptr = node.record;

			while (record_ptr && (record_ptr->type() == Record::TYPE_HARDLINK)) {
				Node const *target = _fs.dereference(record_ptr->linked_name());
				record_ptr = target ? target->record : 0;
			}

			using Dirent_type = Directory_service::Dirent_type;

			/* if no record exists, assume it is a directory */
			if (!record_ptr) {
				dirent = {
					.type = Dirent_type::DIRECTORY,
					.rwx  = Node_rwx::rx(),
					.name = { node.name }
				};
				out_count = sizeof(Dirent);
				return READ_OK;
			}

			Record const &record = *record_ptr;

			auto node_type = [&] ()
			{
				switch (record.type()) {
				case Record::TYPE_FILE:    return Dirent_type::CONTINUOUS_FILE;
				case Record::TYPE_SYMLINK: return Dirent_type::SYMLINK;
				case Record::TYPE_DIR:     return Dirent_type::DIRECTORY;
				};

				warning("unhandled record type ", record.type(), " for ", node.name);

				return Dirent_type::END;
			};

			dirent = {
				.type = node_type(),
				.rwx  = { .readable   = true,
				          .writeable  = false,
				          .executable = record.rwx().executable },
				.name = { node.name }
			};
			out_count = sizeof(Dirent);
			return READ_OK;
		}
	};

	struct Tar_vfs_symlink_handle : Tar_vfs_handle
	{
		using Tar_vfs_handle::Tar_vfs_handle;

		Read_result read(Byte_range_ptr const &dst, size_t &out_count) override
		{
			Record const *record = _node->record;

			size_t const count = min(dst.num_bytes, 100UL);

			memcpy(dst.start, record->linked_name(), count);

			out_count = count;

			return READ_OK;
		}
	};


	using Path_element_token = Token<Scanner_policy_path_element>;


	struct Node : List<Node>, List<Node>::Element
	{
		char const *name;
		Record const *record;

		Node(char const *name, Record const *record) : name(name), record(record) { }

		Node *lookup(char const *name)
		{
			Absolute_path lookup_path(name);

			Node *parent_node = this;
			Node *child_node;

			Path_element_token t(lookup_path.base());

			while (t) {

				if (t.type() != Path_element_token::IDENT) {
						t = t.next();
						continue;
				}

				char path_element[MAX_PATH_LEN];

				t.string(path_element, sizeof(path_element));

				for (child_node = parent_node->first(); child_node; child_node = child_node->next()) {
					if (strcmp(child_node->name, path_element) == 0) {
						parent_node = child_node;
						break;
					}
				}

				if (!child_node)
					return 0;

				t = t.next();
			}

			return parent_node;
		}


		Node const *lookup_child(int index) const
		{
			for (Node const *child_node = first(); child_node; child_node = child_node->next(), index--) {
				if (index == 0)
					return child_node;
			}

			return 0;
		}


		file_size num_dirent()
		{
			file_size count = 0;
			for (Node *child_node = first(); child_node; child_node = child_node->next(), count++) ;
			return count;
		}

	} _root_node;


	/*
	 *  Create a Node for a tar record and insert it into the node list
	 */
	class Add_node_action
	{
		private:

			Allocator &_alloc;

			Node &_root_node;

		public:

			Add_node_action(Allocator &alloc, Node &root_node)
			: _alloc(alloc), _root_node(root_node) { }

			void operator()(Record const *record)
			{
				Absolute_path current_path;

				char path_element[MAX_PATH_LEN];

				if (record->max_name_len() > 100 || record->name()[99] == 0)
					current_path.import(record->name());

				/*
				 * GNU tar does not null terminate names of length 100
				 */
				else {
					copy_cstring(path_element, record->name(), 101);
					current_path.import(path_element);
				}

				Path_element_token t(current_path.base());

				Node *parent_node = &_root_node;
				Node *child_node;

				while(t) {

					if (t.type() != Path_element_token::IDENT) {
							t = t.next();
							continue;
					}

					Absolute_path remaining_path(t.start());

					t.string(path_element, sizeof(path_element));

					for (child_node = parent_node->first(); child_node; child_node = child_node->next()) {
						if (strcmp(child_node->name, path_element) == 0)
							break;
					}

					if (child_node) {

						if (remaining_path.has_single_element()) {
							/* Found a node for the record to be inserted.
							 * This is usually a directory node without
							 * record. */
							child_node->record = record;
						}
					} else {
						if (remaining_path.has_single_element()) {

							/*
							 * TODO: find 'path_element' in 'record->name'
							 * and use the location in the record as name
							 * pointer to save some memory
							 */
							size_t name_size = strlen(path_element) + 1;
							char *name = (char*)_alloc.alloc(name_size);
							copy_cstring(name, path_element, name_size);
							child_node = new (_alloc) Node(name, record);
						} else {

							/* create a directory node without record */
							size_t name_size = strlen(path_element) + 1;
							char *name = (char*)_alloc.alloc(name_size);
							copy_cstring(name, path_element, name_size);
							child_node = new (_alloc) Node(name, 0);
						}
						parent_node->insert(child_node);
					}

					parent_node = child_node;
					t = t.next();
				}
			}
	};


	template <typename Tar_record_action>
	void _for_each_tar_record_do(Tar_record_action tar_record_action)
	{
		/* measure size of archive in blocks */
		unsigned block_id = 0, block_cnt = (unsigned)(_tar_size/Record::BLOCK_LEN);

		/* scan metablocks of archive */
		while (block_id < block_cnt) {

			Record *record = (Record *)(_tar_base + block_id*Record::BLOCK_LEN);

			tar_record_action(record);

			file_size size = record->storage_size();

			/* some datablocks */       /* one metablock */
			block_id = block_id + (unsigned)(size / Record::BLOCK_LEN) + 1;

			/* round up */
			if (size % Record::BLOCK_LEN != 0) block_id++;

			/* check for end of tar archive */
			if (block_id*Record::BLOCK_LEN >= _tar_size)
				break;

			/* lookout for empty eof-blocks */
			if (*(_tar_base + (block_id*Record::BLOCK_LEN)) == 0x00)
				if (*(_tar_base + (block_id*Record::BLOCK_LEN + 1)) == 0x00)
					break;
		}
	}


	struct Num_dirent_cache
	{
		Node     &root_node;
		bool      valid;              /* true after first lookup */
		char      key[256];           /* key used for lookup */
		file_size cached_num_dirent;  /* cached value */

		Num_dirent_cache(Node &root_node)
		: root_node(root_node), valid(false), cached_num_dirent(0) { }

		file_size num_dirent(char const *path)
		{
			/* check for cache miss */
			if (!valid || strcmp(path, key) != 0) {
				Node *node = root_node.lookup(path);
				if (!node)
					return 0;
				copy_cstring(key, path, sizeof(key));
				cached_num_dirent = node->num_dirent();
				valid = true;
			}
			return cached_num_dirent;
		}
	} _cached_num_dirent;

	/**
	 * Walk hardlinks until we reach a file
	 */
	Node const *dereference(char const *path)
	{
		Node const *node = _root_node.lookup(path);
		Node const *slow_node = node;
		int i = 0;
		while (node) {
			Record const *record = node->record;
			if (!record || record->type() != Record::TYPE_HARDLINK)
				break; /* got it */

			/*
			 * The `node` pointer is followed every iteration and
			 * `slow_node` every-other iteration. If there is a
			 * loop then eventually we catch it as the faster
			 * laps the slower.
			 */
			node = _root_node.lookup(record->linked_name());
			if (i++ & 1) {
				slow_node = _root_node.lookup(slow_node->record->linked_name());
				if (node == slow_node) {
					error(_rom_name, " contains a hard-link loop at '", path, "'");
					node = nullptr;
				}
			}
		}
		return node;
	}

	public:

		File_system(Vfs::Env &env, Parent_fs &, Genode::Node const &config)
		:
			_env(env.env()), _alloc(env.alloc()),
			_rom_name(config.attribute_value("name", Rom_name())),
			_root_node("", 0),
			_cached_num_dirent(_root_node)
		{
			_for_each_tar_record_do(Add_node_action(_alloc, _root_node));
		}

		/*********************************
		 ** Directory-service interface **
		 *********************************/

		Dataspace_capability dataspace(char const *path) override
		{
			Node const *node = dereference(path);
			if (!node || !node->record)
				return Dataspace_capability();

			Record const *record = node->record;
			if (record->type() != Record::TYPE_FILE) {
				error("TAR record \"", path, "\" has unsupported type ", record->type());
				return Dataspace_capability();
			}

			size_t const len = size_t(record->size());

			return _env.ram().try_alloc(len).convert<Dataspace_capability>(
				[&] (Ram::Allocation &allocation) {
					return _env.rm().attach(allocation.cap, {
						.size = { },  .offset     = { },  .use_at    = { },
						.at   = { },  .executable = { },  .writeable = true
					}).convert<Dataspace_capability>(
						[&] (Genode::Env::Local_rm::Attachment &a) {
							memcpy(a.ptr, record->data(), len);
							allocation.deallocate = false;
							return allocation.cap;
						},
						[&] (Genode::Env::Local_rm::Error) {
							return Dataspace_capability();
						}
					);
				},
				[&] (Ram_allocator::Alloc_error) {
					return Dataspace_capability(); }
			);
		}

		void release(char const *, Dataspace_capability ds_cap) override
		{
			_env.ram().free(static_cap_cast<Ram_dataspace>(ds_cap));
		}

		Stat_result stat(char const *path, Stat &out) override
		{
			out = Stat { };

			Node const *node_ptr = dereference(path);
			if (!node_ptr)
				return STAT_ERR_NO_ENTRY;

			if (!node_ptr->record) {
				out = {
					.size              = 0,
					.type              = Node_type::DIRECTORY,
					.rwx               = Node_rwx::rx(),
					.device            = (addr_t)this,
					.modification_time = { }
				};
				return STAT_OK;
			}

			Record const &record = *node_ptr->record;

			auto node_type = [&] ()
			{
				switch (record.type()) {
				case Record::TYPE_FILE:     return Node_type::CONTINUOUS_FILE;
				case Record::TYPE_SYMLINK:  return Node_type::SYMLINK;
				case Record::TYPE_DIR:      return Node_type::DIRECTORY;
				};
				return Node_type::DIRECTORY;
			};

			auto timestamp_from_mtime = [] (auto mtime) -> Timestamp
			{
				return { .ms_since_1970 = mtime >= 0 ? uint64_t(mtime*1000) : 0 };
			};

			out = {
				.size              = record.size(),
				.type              = node_type(),
				.rwx               = { .readable   = true,
				                       .writeable  = false,
				                       .executable = record.rwx().executable },
				.device            = (addr_t)this,
				.modification_time = timestamp_from_mtime(record.mtime())
			};

			return STAT_OK;
		}

		Unlink_result unlink(char const *path) override
		{
			Node const *node = dereference(path);
			if (!node)
				return UNLINK_ERR_NO_ENTRY;
			else
				return UNLINK_ERR_NO_PERM;
		}

		Rename_result rename(char const *from, char const *to) override
		{
			if (_root_node.lookup(from) || _root_node.lookup(to))
				return RENAME_ERR_NO_PERM;
			return RENAME_ERR_NO_ENTRY;
		}

		file_size num_dirent(char const *path) override
		{
			return _cached_num_dirent.num_dirent(path);
		}

		bool directory(char const *path) override
		{
			Node const *node = dereference(path);

			if (!node)
				return false;

			Record const *record = node->record;

			return record ? (record->type() == Record::TYPE_DIR) : true;
		}

		bool dir_entry_exists(char const *path) override
		{
			/*
			 * Check if path exists within the file system. If this is the
			 * case, return the whole path, which is relative to the root
			 * of this file system.
			 */
			Node *node = _root_node.lookup(path);
			return node != nullptr;
		}

		Open_result open(char const *path, unsigned, Vfs_handle **out_handle,
		                 Allocator& alloc) override
		{
			Node const *node = dereference(path);
			if (!node || !node->record || node->record->type() != Record::TYPE_FILE)
				return OPEN_ERR_UNACCESSIBLE;

			try {
				*out_handle = new (alloc)
					Tar_vfs_file_handle(*this, alloc, 0, node);
				return OPEN_OK;
			}
			catch (Out_of_ram)  { return OPEN_ERR_OUT_OF_RAM; }
			catch (Out_of_caps) { return OPEN_ERR_OUT_OF_CAPS; }
		}

		Opendir_result opendir(char const *path, bool /* create */,
		                       Vfs_handle **out_handle,
		                       Allocator& alloc) override
		{
			Node const *node = dereference(path);

			if (!node ||
			    (node->record && (node->record->type() != Record::TYPE_DIR)))
				return OPENDIR_ERR_LOOKUP_FAILED;

			try {
				*out_handle = new (alloc)
					Tar_vfs_dir_handle(*this, alloc, 0, node);
				return OPENDIR_OK;
			}
			catch (Out_of_ram)  { return OPENDIR_ERR_OUT_OF_RAM; }
			catch (Out_of_caps) { return OPENDIR_ERR_OUT_OF_CAPS; }
		}

		Openlink_result openlink(char const *path, bool /* create */,
		                         Vfs_handle **out_handle, Allocator &alloc) override
		{
			Node const *node = dereference(path);
			if (!node || !node->record ||
			    node->record->type() != Record::TYPE_SYMLINK)
				return OPENLINK_ERR_LOOKUP_FAILED;

			try {
				*out_handle = new (alloc)
					Tar_vfs_symlink_handle(*this, alloc, 0, node);
				return OPENLINK_OK;
			}
			catch (Out_of_ram)  { return OPENLINK_ERR_OUT_OF_RAM; }
			catch (Out_of_caps) { return OPENLINK_ERR_OUT_OF_CAPS; }
		}

		void close(Vfs_handle *vfs_handle) override
		{
			Tar_vfs_handle *tar_handle =
				static_cast<Tar_vfs_handle *>(vfs_handle);

			if (tar_handle)
				destroy(vfs_handle->alloc(), tar_handle);
		}


		/***************************
		 ** File_system interface **
		 ***************************/

		static char const *name()   { return "tar"; }
		char const *type() override { return "tar"; }
};

#endif /* _INCLUDE__VFS__TAR_FILE_SYSTEM_H_ */
