#!/usr/bin/env python3
# SPDX-License-Identifier: LGPL-2.1-or-later

import os
import subprocess
import sys

NAME_TO_MAGIC = {
    'apparmorfs':      ['AAFS_MAGIC'],
    'adfs':            ['ADFS_SUPER_MAGIC'],
    'affs':            ['AFFS_SUPER_MAGIC'],
    'afs':             ['AFS_FS_MAGIC',
                        'AFS_SUPER_MAGIC'],
    'anon_inodefs':    ['ANON_INODE_FS_MAGIC'],
    'autofs':          ['AUTOFS_SUPER_MAGIC'],
    'balloon-kvm':     ['BALLOON_KVM_MAGIC'],
    'bcachefs':        ['BCACHEFS_SUPER_MAGIC'],
    'bdev':            ['BDEVFS_MAGIC'],
    'binder':          ['BINDERFS_SUPER_MAGIC'],
    'binfmt_misc':     ['BINFMTFS_MAGIC'],
    'bpf':             ['BPF_FS_MAGIC'],
    'btrfs':           ['BTRFS_SUPER_MAGIC'],
    'btrfs_test_fs':   ['BTRFS_TEST_MAGIC'],
    # cpuset's magic got reassigned to cgroupfs
    'cpuset':          ['CGROUP_SUPER_MAGIC'],
    'ceph':            ['CEPH_SUPER_MAGIC'],
    'cgroup2':         ['CGROUP2_SUPER_MAGIC'],
    # note that the cgroupfs magic got reassigned from cpuset
    'cgroup':          ['CGROUP_SUPER_MAGIC'],
    'cifs':            ['CIFS_SUPER_MAGIC',
                        'SMB2_SUPER_MAGIC'],
    'coda':            ['CODA_SUPER_MAGIC'],
    'configfs':        ['CONFIGFS_MAGIC'],
    'cramfs':          ['CRAMFS_MAGIC'],
    'dax':             ['DAXFS_MAGIC'],
    'debugfs':         ['DEBUGFS_MAGIC'],
    'devmem':          ['DEVMEM_MAGIC'],
    'devpts':          ['DEVPTS_SUPER_MAGIC'],
    # devtmpfs is just a special instance of tmpfs, hence it reports its magic
    'devtmpfs':        ['TMPFS_MAGIC'],
    'dmabuf':          ['DMA_BUF_MAGIC'],
    'ecryptfs':        ['ECRYPTFS_SUPER_MAGIC'],
    'efivarfs':        ['EFIVARFS_MAGIC'],
    'efs':             ['EFS_SUPER_MAGIC'],
    'erofs':           ['EROFS_SUPER_MAGIC_V1'],
    # ext2 + ext3 + ext4 use the same magic
    'ext2':            ['EXT2_SUPER_MAGIC'],
    'ext3':            ['EXT3_SUPER_MAGIC'],
    'ext4':            ['EXT4_SUPER_MAGIC'],
    'exfat':           ['EXFAT_SUPER_MAGIC'],
    'f2fs':            ['F2FS_SUPER_MAGIC'],
    # fuseblk is so closely related to fuse that it shares the same magic
    'fuseblk':         ['FUSE_SUPER_MAGIC'],
    'fuse':            ['FUSE_SUPER_MAGIC'],
    'fusectl':         ['FUSE_CTL_SUPER_MAGIC'],
    # gfs is an old version of gfs2 and reuses the magic
    'gfs':             ['GFS2_MAGIC'],
    'gfs2':            ['GFS2_MAGIC'],
    'gmem':            ['GUEST_MEMFD_MAGIC'],
    'hostfs':          ['HOSTFS_SUPER_MAGIC'],
    'hpfs':            ['HPFS_SUPER_MAGIC'],
    'hugetlbfs':       ['HUGETLBFS_MAGIC'],
    'iso9660':         ['ISOFS_SUPER_MAGIC'],
    'jffs2':           ['JFFS2_SUPER_MAGIC'],
    'minix':           ['MINIX_SUPER_MAGIC',
                        'MINIX_SUPER_MAGIC2',
                        'MINIX2_SUPER_MAGIC',
                        'MINIX2_SUPER_MAGIC2',
                        'MINIX3_SUPER_MAGIC'],
    'mqueue':          ['MQUEUE_MAGIC'],
    # msdos is an older legacy version of vfat, shares the magic
    'msdos':           ['MSDOS_SUPER_MAGIC'],
    # ncp/ncpfs have been removed from the kernel, but ncpfs was the official name
    'ncp':             ['NCP_SUPER_MAGIC'],
    'ncpfs':           ['NCP_SUPER_MAGIC'],
    # nfs is the old version of nfs4, and they share the same magic
    'nfs':             ['NFS_SUPER_MAGIC'],
    'nfs4':            ['NFS_SUPER_MAGIC'],
    'nilfs2':          ['NILFS_SUPER_MAGIC'],
    'nsfs':            ['NSFS_MAGIC'],
    'ntfs':            ['NTFS_SB_MAGIC'],
    'ntfs3':           ['NTFS3_SUPER_MAGIC'],
    'nullfs':          ['NULL_FS_MAGIC'],
    'ocfs2':           ['OCFS2_SUPER_MAGIC'],
    'openpromfs':      ['OPENPROM_SUPER_MAGIC'],
    'orangefs':        ['ORANGEFS_DEVREQ_MAGIC'],
    'overlay':         ['OVERLAYFS_SUPER_MAGIC'],
    'pidfs':           ['PID_FS_MAGIC'],
    'pipefs':          ['PIPEFS_MAGIC'],
    'ppc-cmm':         ['PPC_CMM_MAGIC'],
    'proc':            ['PROC_SUPER_MAGIC'],
    'pstore':          ['PSTOREFS_MAGIC'],
    # pvfs2 is the old version of orangefs
    'pvfs2':           ['ORANGEFS_DEVREQ_MAGIC'],
    'qnx4':            ['QNX4_SUPER_MAGIC'],
    'qnx6':            ['QNX6_SUPER_MAGIC'],
    'ramfs':           ['RAMFS_MAGIC'],
    'resctrl':         ['RDTGROUP_SUPER_MAGIC'],
    'reiserfs':        ['REISERFS_SUPER_MAGIC'],
    'rpc_pipefs':      ['RPC_PIPEFS_SUPER_MAGIC'],
    'secretmem':       ['SECRETMEM_MAGIC'],
    'securityfs':      ['SECURITYFS_MAGIC'],
    'selinuxfs':       ['SELINUX_MAGIC'],
    'shiftfs':         ['SHIFTFS_MAGIC'],
    'smackfs':         ['SMACK_MAGIC'],
    # smb3 is an alias for cifs
    'smb3':            ['CIFS_SUPER_MAGIC'],
    # smbfs was removed from the kernel in 2010, the magic remains
    'smbfs':           ['SMB_SUPER_MAGIC'],
    'sockfs':          ['SOCKFS_MAGIC'],
    'squashfs':        ['SQUASHFS_MAGIC'],
    'sysfs':           ['SYSFS_MAGIC'],
    # note that devtmpfs shares the same magic with tmpfs, given it is just a special named instance of it.
    'tmpfs':           ['TMPFS_MAGIC'],
    'tracefs':         ['TRACEFS_MAGIC'],
    'udf':             ['UDF_SUPER_MAGIC'],
    'usbdevfs':        ['USBDEVICE_SUPER_MAGIC'],
    'vboxsf':          ['VBOXSF_SUPER_MAGIC'],
    # note that msdos shares the same magic (and is the older version)
    'vfat':            ['MSDOS_SUPER_MAGIC'],
    'v9fs':            ['V9FS_MAGIC'],
    'xenfs':           ['XENFS_SUPER_MAGIC'],
    'xfs':             ['XFS_SUPER_MAGIC'],
    'z3fold':          ['Z3FOLD_MAGIC'],
    'zonefs':          ['ZONEFS_MAGIC'],
    'zsmalloc':        ['ZSMALLOC_MAGIC'],
}  # fmt: skip

# System magics are sometimes not unique, because file systems got new
# revisions or got renamed. Let's prefer newer over older here, and thus ignore
# the old names.
OBSOLETE_NAMES = {
    'cpuset',    # magic taken over by cgroupfs
    'devtmpfs',  # not a file system of its own, but just a "named superblock" of tmpfs
    'ext2',      # ext4 is the newest revision of ext2 + ext3
    'ext3',
    'fuseblk',   # closely related to fuse; they share a single magic, but the latter is more common
    'gfs',       # magic taken over by gfs2
    'msdos',     # vfat is the newest revision of msdos
    'ncp',       # ncpfs (not ncp) was the last name of the netware 'file_system_type'
                 # name before it was removed in 2018
    'nfs',       # nfs4 is the newest revision of nfs
    'pvfs2',     # orangefs is the new name of pvfs2
    'smb3',      # smb3 is an alias for cifs
}  # fmt: skip

FILESYSTEM_SETS = [
    (
        '@basic-api',
        'Basic filesystem API',
        'cgroup',
        'cgroup2',
        'devpts',
        'devtmpfs',
        'mqueue',
        'proc',
        'sysfs',
    ),
    (
        '@anonymous',
        'Anonymous inodes',
        'anon_inodefs',
        'pipefs',
        'sockfs',
    ),
    (
        '@application',
        'Application virtual filesystems',
        'autofs',
        'fuse',
        'overlay',
    ),
    (
        '@auxiliary-api',
        'Auxiliary filesystem API',
        'binfmt_misc',
        'configfs',
        'efivarfs',
        'fusectl',
        'hugetlbfs',
        'rpc_pipefs',
        'securityfs',
    ),
    (
        '@common-block',
        'Common block device filesystems',
        'btrfs',
        'erofs',
        'exfat',
        'ext4',
        'f2fs',
        'iso9660',
        'ntfs3',
        'squashfs',
        'udf',
        'vfat',
        'xfs',
    ),
    (
        '@historical-block',
        'Historical block device filesystems',
        'ext2',
        'ext3',
        'minix',
    ),
    (
        '@network',
        'Well-known network filesystems',
        'afs',
        'ceph',
        'cifs',
        'gfs',
        'gfs2',
        'ncp',
        'ncpfs',
        'nfs',
        'nfs4',
        'ocfs2',
        'orangefs',
        'pvfs2',
        'smb3',
        'smbfs',
    ),
    (
        '@privileged-api',
        'Privileged filesystem API',
        'bpf',
        'debugfs',
        'pstore',
        'tracefs',
    ),
    (
        '@security',
        'Security/MAC API VFS',
        'apparmorfs',
        'selinuxfs',
        'smackfs',
    ),
    (
        '@temporary',
        'Temporary filesystems',
        'ramfs',
        'tmpfs',
    ),
    (
        '@known',
        'All known filesystems declared in the kernel',
        *NAME_TO_MAGIC.keys(),
    ),
]


def generate_gperf():
    print('''\
/* SPDX-License-Identifier: LGPL-2.1-or-later */
%{
#if __GNUC__ >= 15
_Pragma("GCC diagnostic ignored \\"-Wzero-as-null-pointer-constant\\"")
#endif
#include <linux/magic.h>

#include "filesystems.h"
#include "stat-util.h"

struct FilesystemMagic {
        const char *name;
        statfs_f_type_t magic[FILESYSTEM_MAGIC_MAX];
};
%}
struct FilesystemMagic;
%language=ANSI-C
%define hash-function-name filesystems_gperf_hash
%define lookup-function-name filesystems_gperf_lookup
%define slot-name name
%readonly-tables
%omit-struct-type
%struct-type
%includes
%%''')
    for name, magics in NAME_TO_MAGIC.items():
        print(f'{name + ",":16} {{{", ".join(magics)}}}')


def generate_fs_type_to_string():
    print('''\
#include <linux/magic.h>
#include "filesystems.h"

/* PROJECT_FILE, which is used by log_xyz() thus also used by assert_not_reached(), cannot be used in
 * generated files, as the build directory may be outside of the source directory. */
#undef PROJECT_FILE
#define PROJECT_FILE __FILE__

const char* fs_type_to_string(statfs_f_type_t magic) {
        switch (magic) {''')

    for name, magics in NAME_TO_MAGIC.items():
        if name in OBSOLETE_NAMES:
            continue
        for magic in magics:
            print(f'        case (statfs_f_type_t) {magic}:')
        print(f'                return "{name}";')

    print('''\
        }
        return NULL;
}''')


def generate_fs_in_group():
    print('bool fs_in_group(const struct statfs *st, FilesystemGroups fs_group) {')
    print('        switch (fs_group) {')

    for name, _, *filesystems in FILESYSTEM_SETS:
        magics = sorted(set(sum((NAME_TO_MAGIC[fs] for fs in filesystems), start=[])))
        enum = 'FILESYSTEM_SET_' + name[1:].upper().replace('-', '_')
        print(f'        case {enum}:')
        opts = '\n                    || '.join(f'F_TYPE_EQUAL(st->f_type, {magic})' for magic in magics)
        print(f'                return {opts};')

    print('        default: assert_not_reached();')
    print('        }')
    print('}')


def generate_filesystem_sets():
    print('const FilesystemSet filesystem_sets[_FILESYSTEM_SET_MAX] = {')

    for name, desc, *filesystems in FILESYSTEM_SETS:
        enum = 'FILESYSTEM_SET_' + name[1:].upper().replace('-', '_')

        print(f'        [{enum}] = {{')
        print(f'                .name = "{name}",')
        print(f'                .help = "{desc}",')
        print('                .value =')
        for filesystem in filesystems:
            print(f'                "{filesystem}\\0"')
        print('        },')

    print('};')


def magic_defines():
    cpp = os.environ['CPP'].split()
    out = subprocess.check_output(
        [*cpp, '-dM', '-include', 'linux/magic.h', '-'],
        stdin=subprocess.DEVNULL,
        text=True,
    )
    for line in out.splitlines():
        _, name, *rest = line.split()
        if (
            '_MAGIC' in name
            and rest
            and rest[0].startswith('0x')
            and name
            not in {
                'STACK_END_MAGIC',
                'MTD_INODE_FS_MAGIC',
                'FUTEXFS_SUPER_MAGIC',
                'CRAMFS_MAGIC_WEND',
            }
        ):
            yield name


def check():
    kernel_magics = set(magic_defines())
    our_magics = set(sum(NAME_TO_MAGIC.values(), start=[]))
    extra = kernel_magics - our_magics
    if extra:
        sys.exit(f'kernel knows additional filesystem magics: {", ".join(sorted(extra))}')


if __name__ == '__main__':
    for arg in sys.argv[1:]:
        if arg == 'gperf':
            generate_gperf()
        elif arg == 'fs-type-to-string':
            generate_fs_type_to_string()
        elif arg == 'filesystem-sets':
            generate_filesystem_sets()
        elif arg == 'fs-in-group':
            generate_fs_in_group()
        elif arg == 'check':
            check()
        else:
            raise ValueError
