package fs::type;

use diagnostics;
use strict;

use common;
use devices;

=head1 SYNOPSYS

B<fs::type> enables to perform various tests on filesystem types.

=head1 Functions

=over

=cut

our @ISA = qw(Exporter);
our @EXPORT = qw(
   isBlockCheckable isEmpty isExtended isFormatable isTrueLocalFS isTrueFS isDos isSwap isOtherAvailableFS isRawLVM isRawRAID isRawLUKS isRAID isLVM isLUKS isMountableRW isNonMountable isPartOfLVM isPartOfRAID isPartOfLoopback isLoopback isMounted isBusy isSpecial isApple isAppleBootstrap isBIOS_GRUB isESP isFat_or_NTFS isnormal_Fat_or_NTFS isRecovery
   maybeFormatted set_isFormatted defaultFS
);


my (%type_name2pt_type, %type_name2fs_type, %fs_type2pt_type, %pt_type2fs_type, %type_names);

{
    my @list_types = (
	important => [
  0x82 => 'swap',     'Linux swap',
  0x83 => 'ext2',     'Linux native',
  0x83 => 'ext3',     'Journalised FS: ext3',
  0x83 => 'ext4',     'Journalised FS: ext4',
  0x83 => 'btrfs',    'Journalised FS: Btrfs',
(is_uefi() ?
  (0xef => 'vfat',     'EFI System Partition') :
  ('BIOS_GRUB' => 'BIOS_GRUB',  'BIOS boot or Empty partition')
),
if_(arch() =~ /i.86|x86_64/,
  0x83 => 'xfs',      'Journalised FS: XFS',
  0x83 => 'jfs',      'Journalised FS: JFS',
  0x0b => 'vfat',     'FAT32',
  0x07 => 'ntfs-3g',  'NTFS-3G',
  0x07 => 'ntfs',     'NTFS',
  0x07 => 'ntfs3',    'NTFS3',
  0x07 => 'exfat',    'exFAT',
),
	],

        non_fs_type => [
  0x83 => '',         'Encrypted',
  0x8e => '',         'Linux Logical Volume Manager',
  0xfd => '',         'Linux RAID',
	],

	special => [
  0x0  => '',         'Empty',
  0x05 => '',         'Extended',
  0x0f => '',         'W95 Extended (LBA)',
  0x85 => '',         'Linux extended',
	],

	other => [
 if_(arch() =~ /^i.86|x86_64/,
  0x01 => 'vfat',     'FAT12',
  0x02 => '',         'XENIX root',
  0x03 => '',         'XENIX usr',
  0x04 => 'vfat',     'FAT16 <32M',
  0x06 => 'vfat',     'FAT16',
  0x07 => 'hpfs',     'HPFS',
  0x08 => '',         'AIX',
),
  0x09 => '',         'AIX bootable',
  0x0a => '',         'OS/2 Boot Manager',
  0x0c => 'vfat',     'W95 FAT32 (LBA)',
  0x0e => 'vfat',     'W95 FAT16 (LBA)',
  0x10 => '',         'OPUS',
  0x11 => '',         'Hidden FAT12',
  0x12 => '',         'Compaq diagnostics',
  0x14 => '',         'Hidden FAT16 <32M',
  0x16 => '',         'Hidden FAT16',
  0x17 => 'ntfs',     'Hidden HPFS/NTFS',
  0x18 => '',         'AST SmartSleep',
  0x1b => 'vfat',     'Hidden W95 FAT32',       # \ 
  0x1c => 'vfat',     'Hidden W95 FAT32 (LBA)', #  > don't change label, it's used to know if it's not a boot partition in bootloader.pm
  0x1e => 'vfat',     'Hidden W95 FAT16 (LBA)', # /
  0x24 => '',         'NEC DOS',
  0x39 => '',         'Plan 9',
  0x3c => '',         'PartitionMagic recovery',
  0x40 => '',         'Venix 80286',
  0x41 => '',         'PPC PReP Boot',
  0x42 => '',         'SFS',
  0x4d => '',         'QNX4.x',
  0x4e => '',         'QNX4.x 2nd part',
  0x4f => '',         'QNX4.x 3rd part',
  0x50 => '',         'OnTrack DM',
  0x51 => '',         'OnTrack DM6 Aux1',
  0x52 => '',         'CP/M',
  0x53 => '',         'OnTrack DM6 Aux3',
  0x54 => '',         'OnTrackDM6',
  0x55 => '',         'EZ-Drive',
  0x56 => '',         'Golden Bow',
  0x5c => '',         'Priam Edisk',
  0x61 => '',         'SpeedStor',
  0x63 => '',         'GNU HURD or SysV',
  0x64 => '',         'Novell Netware 286',
  0x65 => '',         'Novell Netware 386',
  0x70 => '',         'DiskSecure Multi-Boot',
  0x75 => '',         'PC/IX',
  0x80 => '',         'Old Minix',
  0x81 => '',         'Minix / old Linux',
  0x83 => 'f2fs',     'Journalised FS: F2FS',
  0x83 => 'reiserfs', 'Journalised FS: ReiserFS',
  0x83 => 'nilfs2',   'Journalised FS: NILFS2',
  0x84 => '',         'OS/2 hidden C: drive',
  0x86 => '',         'NTFS volume set (0x86)',
  0x87 => '',         'NTFS volume set (0x87)',
  0x93 => '',         'Amoeba',
  0x94 => '',         'Amoeba BBT',
  0x9f => '',         'BSD/OS',
  0xa0 => '',         'IBM Thinkpad hibernation',
  0xa5 => '',         'FreeBSD',
  0xa6 => '',         'OpenBSD',
  0xa7 => '',         'NeXTSTEP',
  0xa8 => '',         'Darwin UFS',
  0xa9 => '',         'NetBSD',
  0xab => '',         'Darwin boot',
  0xb7 => '',         'BSDI fs',
  0xb8 => '',         'BSDI swap',
  0xbb => '',         'Boot Wizard hidden',
  0xbe => '',         'Solaris boot',
  0xbf => '',         'Microsoft XBox OS Partitions',
  0xc1 => '',         'DRDOS/sec (FAT-12)',
  0xc4 => '',         'DRDOS/sec (FAT-16 < 32M)',
  0xc6 => '',         'DRDOS/sec (FAT-16)',
  0xc7 => '',         'Syrinx',
  0xda => '',         'Non-FS data',
  0xdb => '',         'CP/M / CTOS / ...',
  0xde => '',         'Dell Utility',
  0xdf => '',         'BootIt',
  0xe1 => '',         'SpeedStor (FAT-12)',
  0xe3 => '',         'DOS R/O',
  0xe4 => '',         'SpeedStor (FAT-16)',
  0xeb => 'befs',     'BeOS fs',
  0xee => '',         'EFI GPT',
  0xf0 => '',         'Linux/PA-RISC boot',
  0xf4 => '',         'SpeedStor (large part.)',
  0xf2 => '',         'DOS secondary',
  0xfe => '',         'LANstep',
  0xff => '',         'BBT',
	],
    );

    foreach (group_by2(@list_types)) {
	my ($name, $l) = @$_;
	for (my $i = 0; defined $l->[$i]; $i += 3) {
	    my $pt_type   = $l->[$i];
	    my $fs_type   = $l->[$i + 1];
	    my $type_name = $l->[$i + 2];
	    !exists $type_name2fs_type{$type_name} or internal_error("'$type_name' is not unique");
	    $type_name2fs_type{$type_name} = $fs_type;
	    $type_name2pt_type{$type_name} = $pt_type;

	    $fs_type2pt_type{$fs_type} ||= $pt_type;
	    $pt_type2fs_type{$pt_type} ||= $fs_type;
	    push @{$type_names{$name}}, $type_name;
	}
    }
}


sub type_names { 
    my ($expert, $o_hd) = @_;
    my @l = @{$type_names{important}};
    push @l, grep { $_ ne 'Encrypted' } @{$type_names{non_fs_type}};
    push @l, sort @{$type_names{other}} if $expert;
    # not show partition types which have no associated filesystem for LVM LV:
    if ($o_hd && isLVM($o_hd)) {
	@l = grep { $type_name2fs_type{$_} } @l;
	@l = uniq_ { $type_name2fs_type{$_} } @l;
	(@l, @{$type_names{non_fs_type}});
    } else {
	@l;
    }
}

sub type_name2subpart {
    my ($name) = @_;
    exists $type_name2fs_type{$name} && 
      { type_name => $name,
	fs_type => $type_name2fs_type{$name}, pt_type => $type_name2pt_type{$name} };
}

sub part2type_name { 
    my ($part) = @_;
    my @names = keys %type_name2fs_type;
   
    my $pt_type = defined $part->{pt_type} ? $part->{pt_type} : $part->{fs_type} && $fs_type2pt_type{$part->{fs_type}};
    if (defined $pt_type) {
	@names = grep { $pt_type eq $type_name2pt_type{$_} } @names;
    }
    if (my $fs_type = $part->{fs_type} || $part->{pt_type} && $pt_type2fs_type{$part->{pt_type}}) {
	@names = grep { $fs_type eq $type_name2fs_type{$_} } @names;
    }
    if (@names > 1) {
	log::l("ERROR: (part2type_name) multiple match for $part->{pt_type} $part->{fs_type}");
    }
    first(@names);
}
sub type_name2pt_type { 
    local ($_) = @_;
    /0x(.*)/ ? hex $1 : $type_name2pt_type{$_} || $_;
}


sub pt_type2subpart {
    my ($pt_type) = @_;
    my $fs_type = $pt_type2fs_type{$pt_type};
    { pt_type => $pt_type, if_($fs_type, fs_type => $fs_type) };
}
sub fs_type2subpart {
    my ($fs_type) = @_;
    my $pt_type = $fs_type2pt_type{$fs_type};
    { fs_type => $fs_type, if_($pt_type, pt_type => $pt_type) };
}
sub set_fs_type {
    my ($part, $fs_type) = @_;
    put_in_hash($part, fs_type2subpart($fs_type));
}
sub set_pt_type {
    my ($part, $pt_type) = @_;
    put_in_hash($part, pt_type2subpart($pt_type));
}
sub suggest_fs_type {
    my ($part, $fs_type) = @_;
    set_fs_type($part, $fs_type) if !$part->{pt_type} && !$part->{fs_type};
}
sub set_type_subpart {
    my ($part, $subpart) = @_;
    if (exists $subpart->{pt_type} && exists $subpart->{fs_type}) {
	$part->{fs_type} = $subpart->{fs_type};
	$part->{pt_type} = $subpart->{pt_type};
    } elsif (exists $subpart->{pt_type}) {
	set_pt_type($part, $subpart->{pt_type});
    } elsif (exists $subpart->{fs_type}) {
	set_fs_type($part, $subpart->{fs_type});
    } else {
	log::l("ERROR: (set_type_subpart) subpart has no type");
    }
}

sub fs_type_from_magic {
    my ($part) = @_;
    if (exists $part->{fs_type_from_magic}) {
	$part->{fs_type_from_magic};
    } else {
	my $type = type_subpart_from_magic($part);
	$type && $type->{fs_type};
    }
}

sub call_blkid {
    my ($part) = @_;

    # IMPORTANT: Always use the -p argument with blkid. See r7324 commit msg
    my %h = map {
	if_(/(.*?)=(.*)/, $1 => $2);
    } run_program::get_stdout_raw({ timeout => 30 }, 'blkid', '2>', '/dev/null', '-o', 'udev', '-p', devices::make($part->{device}));

    \%h;
}

sub type_subpart_from_magic { 
    my ($part) = @_;
    my $ids = call_blkid($part);

    my $p;
    if ($ids->{ID_FS_USAGE} eq 'raid') {
	my $name = {
	    linux_raid_member => "Linux RAID",
	    LVM1_member => 'Linux Logical Volume Manager',
	    LVM2_member => 'Linux Logical Volume Manager',
	}->{$ids->{ID_FS_TYPE}};

	$p = type_name2subpart($name) if $name;
    } elsif ($ids->{ID_FS_USAGE} eq 'crypto') {
	$p = type_name2subpart('Encrypted');
    } elsif (my $fs_type = $ids->{ID_FS_TYPE}) {
	$fs_type = 'ntfs-3g' if $fs_type eq 'ntfs';
	$p = fs_type2subpart($fs_type) or log::l("unknown filesystem $fs_type returned by blkid");
    }

    if ($p) {
	$p->{fs_type} = '' if $part->{pt_type} eq 'BIOS_GRUB' && $p->{fs_type} ne 'iso9660';
	$part->{fs_type_from_magic} = $p->{fs_type};
	$p->{device_LABEL} = $ids->{ID_FS_LABEL} if $ids->{ID_FS_LABEL};
	$p->{device_UUID} = $ids->{ID_FS_UUID} if $ids->{ID_FS_UUID};
	log::l("blkid gave: $p->{fs_type} $p->{device_UUID} $p->{device_LABEL}");
    }
    $p;
}

# helpers
sub defaultFS() { 'ext4' }
sub true_local_fs_types() { qw(btrfs ext3 ext2 ext4 f2fs reiserfs xfs jfs) }

sub isEmpty { !$_[0]{fs_type} && !$_[0]{pt_type} }
sub isBIOS_GRUB { $_[0]{pt_type} eq 'BIOS_GRUB' }
sub isESP { $_[0]{pt_type} == 0xef && member($_[0]{fs_type}, qw(fat32 vfat)) }
sub isExtended { $_[0]{pt_type} == 5 || $_[0]{pt_type} == 0xf || $_[0]{pt_type} == 0x85 }
sub isBlockCheckable { !member($_[0]{fs_type}, qw(btrfs hfs ntfs ntfs-3g reiserfs xfs)) }
sub isRawLVM { $_[0]{pt_type} == 0x8e || $_[0]{type_name} eq 'Linux Logical Volume Manager' }
sub isRawRAID { $_[0]{pt_type} == 0xfd || $_[0]{type_name} eq 'Linux RAID' }
sub isRawLUKS { $_[0]{type_name} eq 'Encrypted' }
sub isSwap { $_[0]{fs_type} eq 'swap' }
sub isDos { ${{ 1 => 1, 4 => 1, 6 => 1 }}{$_[0]{pt_type}} }
sub isFat_or_NTFS { member($_[0]{fs_type}, qw(vfat ntfs ntfs3 ntfs-3g)) }
sub isnormal_Fat_or_NTFS { grep { isFat_or_NTFS($_) && !isESP($_) && !isRecovery($_) } @_ }
sub isApple { $_[0]{pt_type} == 0x401 && defined $_[0]{isDriver} }
sub isAppleBootstrap { $_[0]{pt_type} == 0x401 && defined $_[0]{isBoot} }
sub isRecovery { 
    isFat_or_NTFS($_[0]) && ($_[0]{type_name} =~ /^Hidden/ ||
      $_[0]{pt_type} == 0x12 || # "Compaq diagnostics"
        member($_[0]{device_LABEL} , 
            # Extracted from /usr/lib/udev/rules.d/80-udisks2.rules
            # Hopefuly we'll ask to udev/udisk2 someday
            # generated by grep Recovery /usr/lib/udev/rules.d/80-udisks2.rules :
            qw(Recovery RECOVERY Lenovo_Recovery HP_RECOVERY Recovery_Partition DellUtility DellRestore IBM_SERVICE SERVICEV001 SERVICEV002 SYSTEM_RESERVED System_Reserved WINRE_DRV DIAGS IntelRST),
            # gathered over the years (Hald, mga#1371, mga#15999):
            qw(PQSERVICE Packard_Bell Push_Button_Reset SYSTEM_DRV))
    );
}

=item isTrueLocalFS($part)

Like isTrueFS(), to make a distinction between ext3/reiserfs/... and NFS
 => allow /home on NFS

=cut

sub isTrueFS { isTrueLocalFS($_[0]) || $_[0]{fs_type} eq 'nfs' }

=item isTrueFS($part)

Is is a general purpose file system with the right Unix properties

=cut

sub isTrueLocalFS { member($_[0]{fs_type}, true_local_fs_types()) }

=item isOtherAvailableFS($part)

Is it another OS that linux can access its filesystem

=cut

sub isOtherAvailableFS { isESP($_[0]) || isFat_or_NTFS($_[0]) || member($_[0]{fs_type}, 'ufs', 'hfs', 'iso9660', 'nilfs2', 'exfat') }
sub isMountableRW { (isTrueFS($_[0]) || isOtherAvailableFS($_[0])) && $_[0]{fs_type} ne 'ntfs' }
sub cannotBeMountable { 
    my ($part) = @_;
    isRawRAID($part) || isRawLUKS($part) || isRawLVM($part) || isBIOS_GRUB($part);
}

=item isFormatable($part)

Is not a special sg that cannot be mounted/formatted (parts of RAID/LVM, BIOS_GRUB). Basically the reverse of cannotBeMountable().

=cut

sub isFormatable {
    my ($part) = @_;
    !cannotBeMountable($part);
}

sub isNonMountable { 
    my ($part) = @_;
    cannotBeMountable($part) || $part->{fs_type} eq 'ntfs' && !$part->{isFormatted} && $part->{notFormatted};
}

sub isPartOfLVM { defined $_[0]{lvm} }
sub isPartOfRAID { defined $_[0]{raid} }
sub isPartOfLoopback { defined $_[0]{loopback} }
sub isRAID { $_[0]{device} =~ /^md/ && defined $_[0]{level} }
sub isUBD { $_[0]{device} =~ /^ubd/ } #- should be always true during an $::uml_install
sub isLVM { $_[0]{VG_name} || $_[0]{lv_name} }
sub isLUKS { defined $_[0]{dmcrypt_name} }
sub isLoopback { defined $_[0]{loopback_file} }
sub isMounted { $_[0]{isMounted} }
sub isBusy { isMounted($_[0]) || isPartOfRAID($_[0]) || isPartOfLVM($_[0]) || $_[0]{dm_active} || isPartOfLoopback($_[0]) }
sub isSpecial { isRAID($_[0]) || isLVM($_[0]) || isLoopback($_[0]) || isUBD($_[0]) }

=item is_dmraid($hd)

Check that a disk (not a partition) is in a fake/soft RAID

=cut

sub is_dmraid { $_[0]{bus} =~ /^dmraid_/ }

sub can_be_this_fs_type {
    my ($part, $fs_type) = @_;
    can_be_one_of_those_fs_types($part, $fs_type);
}
sub can_be_one_of_those_fs_types {
    my ($part, @fs_types) = @_;
    $part->{fs_type} or return;
    $part->{fs_type} eq 'auto' || listlength(intersection(\@fs_types, [ split(':', $part->{fs_type}) ]));
}

sub maybeFormatted { 
    my ($part) = @_;
    $part->{isFormatted} || !$part->{notFormatted} && (!$part->{bad_fs_type_magic} || $part->{options} =~ /encrypted/);
}
sub set_isFormatted {
    my ($part, $val) = @_;
    $part->{isFormatted} = $val;
    $part->{notFormatted} = !$val;
    delete $part->{bad_fs_type_magic};
    delete $part->{fs_type_from_magic};
}

=item check($fs_type, $_hd, $part)

Called before before modifying $part->{fs_type}

=cut

sub check {
    my ($fs_type, $_hd, $part) = @_;
    $fs_type eq "jfs" && $part->{size} < MB(16) and die N("You cannot use JFS for partitions smaller than 16MB");
    $fs_type eq "reiserfs" && $part->{size} < MB(32) and die N("You cannot use ReiserFS for partitions smaller than 32MB");
    $fs_type eq "btrfs" && $part->{size} < MB(256) and die N("You cannot use btrfs for partitions smaller than 256MB");
}

sub guessed_by_mount() {
    grep { $_ && !/nodev/ } chomp_(cat_('/etc/filesystems'));
}

sub directories_needed_to_boot_not_ESP() {
    qw(/ /usr /var /boot /tmp);
}
sub directories_needed_to_boot() { 
    directories_needed_to_boot_not_ESP(), '/boot/EFI';
}

sub carry_root_loopback {
    my ($part) = @_;
    any { $_->{mntpoint} eq '/' } @{$part->{loopback} || []};
}

=back

=cut

1;
