/*
 * Copyright (c) 2015, 2016, 2025 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * ext2.c
 * ext2 filesystem.
 */

#include <endian.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <time.h>

#include <mount/blockdevice.h>
#include <mount/ext2.h>
#include <mount/filesystem.h>

#include "util.h"

// These must be kept up to date with ext/extfs.cpp.
#define EXT2_FEATURE_COMPAT_SUPPORTED 0
#define EXT2_FEATURE_INCOMPAT_SUPPORTED \
        (EXT2_FEATURE_INCOMPAT_FILETYPE)
#define EXT2_FEATURE_RO_COMPAT_SUPPORTED \
        (EXT2_FEATURE_RO_COMPAT_LARGE_FILE)

void ext2_superblock_decode(struct ext2_superblock* sb)
{
	sb->s_inodes_count = le32toh(sb->s_inodes_count);
	sb->s_blocks_count = le32toh(sb->s_blocks_count);
	sb->s_r_blocks_count = le32toh(sb->s_r_blocks_count);
	sb->s_free_blocks_count = le32toh(sb->s_free_blocks_count);
	sb->s_free_inodes_count = le32toh(sb->s_free_inodes_count);
	sb->s_first_data_block = le32toh(sb->s_first_data_block);
	sb->s_log_block_size = le32toh(sb->s_log_block_size);
	sb->s_log_frag_size = (int32_t) le32toh((uint32_t) sb->s_log_frag_size);
	sb->s_blocks_per_group = le32toh(sb->s_blocks_per_group);
	sb->s_frags_per_group = le32toh(sb->s_frags_per_group);
	sb->s_inodes_per_group = le32toh(sb->s_inodes_per_group);
	sb->s_mtime = le32toh(sb->s_mtime);
	sb->s_wtime = le32toh(sb->s_wtime);
	sb->s_mnt_count = le16toh(sb->s_mnt_count);
	sb->s_max_mnt_count = le16toh(sb->s_max_mnt_count);
	sb->s_magic = le16toh(sb->s_magic);
	sb->s_state = le16toh(sb->s_state);
	sb->s_errors = le16toh(sb->s_errors);
	sb->s_minor_rev_level = le16toh(sb->s_minor_rev_level);
	sb->s_lastcheck = le32toh(sb->s_lastcheck);
	sb->s_checkinterval = le32toh(sb->s_checkinterval);
	sb->s_creator_os = le32toh(sb->s_creator_os);
	sb->s_rev_level = le32toh(sb->s_rev_level);
	sb->s_def_resuid = le16toh(sb->s_def_resuid);
	sb->s_def_resgid = le16toh(sb->s_def_resgid);
	sb->s_first_ino = le32toh(sb->s_first_ino);
	sb->s_inode_size = le16toh(sb->s_inode_size);
	sb->s_block_group_nr = le16toh(sb->s_block_group_nr);
	sb->s_feature_compat = le32toh(sb->s_feature_compat);
	sb->s_feature_incompat = le32toh(sb->s_feature_incompat);
	sb->s_feature_ro_compat = le32toh(sb->s_feature_ro_compat);
	// s_uuid is endian agnostic.
	// s_volume_name is endian agnostic.
	// s_last_mounted is endian agnostic.
	sb->s_algo_bitmap = le32toh(sb->s_algo_bitmap);
	// s_prealloc_blocks is endian agnostic.
	// s_prealloc_dir_blocks is endian agnostic.
	sb->alignment0 = le16toh(sb->alignment0);
	// s_journal_uuid is endian agnostic.
	sb->s_journal_inum = le32toh(sb->s_journal_inum);
	sb->s_journal_dev = le32toh(sb->s_journal_dev);
	sb->s_last_orphan = le32toh(sb->s_last_orphan);
	for ( size_t i = 0; i < 4; i++ )
		sb->s_hash_seed[i] = le32toh(sb->s_hash_seed[i]);
	// s_def_hash_version is endian agnostic.
	// alignment1 is endian agnostic.
	sb->s_default_mount_options = le32toh(sb->s_default_mount_options);
	sb->s_first_meta_bg = le32toh(sb->s_first_meta_bg);
}

static size_t ext2_probe_amount(struct blockdevice* bdev)
{
	(void) bdev;
	return 1024 + sizeof(struct ext2_superblock);
}

static bool ext2_probe(struct blockdevice* bdev,
                       const unsigned char* leading,
                       size_t amount)
{
	(void) bdev;
	if ( amount < 1024 )
		return false;
	leading += 1024;
	amount -= 1024;
	if ( amount < sizeof(struct ext2_superblock) )
		return false;
	struct ext2_superblock sb;
	memcpy(&sb, leading, sizeof(sb));
	ext2_superblock_decode(&sb);
	if ( sb.s_magic != EXT2_SUPER_MAGIC )
		return false;
	return true;
}

struct ext2_private
{
	struct ext2_superblock sb;
};

static void ext2_release(struct filesystem* fs)
{
	if ( !fs )
		return;
	struct ext2_private* priv = (struct ext2_private*) fs->handler_private;
	free(priv);
	free(fs);
}

static enum filesystem_error ext2_inspect(struct filesystem** fs_ptr,
                                          struct blockdevice* bdev)
{
	*fs_ptr = NULL;
	struct filesystem* fs = CALLOC_TYPE(struct filesystem);
	if ( !fs )
		return FILESYSTEM_ERROR_ERRNO;
	struct ext2_private* priv = CALLOC_TYPE(struct ext2_private);
	if ( !priv )
		return free(fs), FILESYSTEM_ERROR_ERRNO;
	fs->bdev = bdev;
	fs->handler = &ext2_handler;
	fs->handler_private = priv;
	struct ext2_superblock* sb = &priv->sb;
	if ( blockdevice_preadall(bdev, sb, sizeof(*sb), 1024) != sizeof(*sb) )
		return ext2_release(fs), FILESYSTEM_ERROR_ERRNO;
	ext2_superblock_decode(sb);
	fs->fstype_name = "ext2";
	fs->fsck = "fsck.ext2";
	fs->driver = "extfs";
	if ( sb->s_feature_incompat & ~EXT2_FEATURE_INCOMPAT_SUPPORTED )
		fs->driver = NULL;
	else if ( !(sb->s_feature_compat & ~EXT2_FEATURE_COMPAT_SUPPORTED) &&
	          blockdevice_is_writable(bdev) )
		fs->flags |= FILESYSTEM_FLAG_WRITABLE;
	fs->flags |= FILESYSTEM_FLAG_UUID;
	memcpy(&fs->uuid, sb->s_uuid, 16);
	struct timespec now;
	clock_gettime(CLOCK_REALTIME, &now);
	time_t next_check = (time_t)
		((uintmax_t) sb->s_lastcheck + (uintmax_t) sb->s_checkinterval);
	if ( sb->s_state != EXT2_VALID_FS )
		fs->flags |= FILESYSTEM_FLAG_FSCK_SHOULD |
		             FILESYSTEM_FLAG_FSCK_MUST;
	else if ( sb->s_max_mnt_count && sb->s_max_mnt_count <= sb->s_mnt_count )
		fs->flags |= FILESYSTEM_FLAG_FSCK_SHOULD;
	else if ( sb->s_checkinterval && next_check <= now.tv_sec )
		fs->flags |= FILESYSTEM_FLAG_FSCK_SHOULD;
	return *fs_ptr = fs, FILESYSTEM_ERROR_NONE;
}

const struct filesystem_handler ext2_handler =
{
	.handler_name = "ext2",
	.probe_amount = ext2_probe_amount,
	.probe = ext2_probe,
	.inspect = ext2_inspect,
	.release = ext2_release,
};
