From a9f4fdc8ca8c79236f233b1eac7cc60bedee3d17 Mon Sep 17 00:00:00 2001 From: karmantyu Date: Sun, 8 Feb 2026 08:34:32 +0100 Subject: [PATCH] Corrected escape, removed broad, unanchored regex checks . --- bsdfdisk/bsdfdisk-lib.pl | 3654 +++++++++++++++++++------------------ bsdfdisk/delete_part.cgi | 14 +- bsdfdisk/delete_slice.cgi | 2 +- bsdfdisk/edit_part.cgi | 6 +- bsdfdisk/edit_slice.cgi | 6 +- 5 files changed, 1844 insertions(+), 1838 deletions(-) diff --git a/bsdfdisk/bsdfdisk-lib.pl b/bsdfdisk/bsdfdisk-lib.pl index 767fc9afc..977b00f43 100644 --- a/bsdfdisk/bsdfdisk-lib.pl +++ b/bsdfdisk/bsdfdisk-lib.pl @@ -1,178 +1,178 @@ -BEGIN { push( @INC, ".." ); } -use WebminCore; -init_config(); -foreign_require( "mount", "mount-lib.pl" ); -foreign_require( "fdisk", "fdisk-lib.pl" ); - -#--------------------------------------------------------------------- -# Helper: Cache mount info -# Returns a hash reference of device => mount point and an arrayref -# containing mount entries. -sub get_all_mount_points_cached { - my %mount_info; - my @mount_list = mount::list_mounted(); - foreach my $m (@mount_list) { - $mount_info{ $m->[0] } = $m->[1]; - } - my $swapinfo = backquote_command("swapinfo -k 2>/dev/null"); - foreach my $line ( split( /\n/, $swapinfo ) ) { - if ( $line =~ /^(\/dev\/\S+)\s+\d+\s+\d+\s+\d+/ ) { - $mount_info{$1} = "swap"; - } - } - - # ZFS, GEOM, glabel, geli remain the same as the original. - if ( has_command("zpool") ) { - my $zpool_out = backquote_command("zpool status 2>/dev/null"); - my $current_pool = ""; - foreach my $line ( split( /\n/, $zpool_out ) ) { - if ( $line =~ /^\s*pool:\s+(\S+)/ ) { - $current_pool = $1; - } - elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) { - my $dev = $1; - $mount_info{$dev} = "ZFS pool: $current_pool"; - } - } - } - if ( has_command("geom") ) { - - # gmirror - my $gmirror_out = backquote_command("gmirror status 2>/dev/null"); - my $current_mirror = ""; - foreach my $line ( split( /\n/, $gmirror_out ) ) { - if ( $line =~ /^(\S+):/ ) { - $current_mirror = $1; - } - elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) { - my $dev = $1; - $mount_info{$dev} = "gmirror: $current_mirror"; - } - } - - # gstripe - my $gstripe_out = backquote_command("gstripe status 2>/dev/null"); - my $current_stripe = ""; - foreach my $line ( split( /\n/, $gstripe_out ) ) { - if ( $line =~ /^(\S+):/ ) { - $current_stripe = $1; - } - elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) { - my $dev = $1; - $mount_info{$dev} = "gstripe: $current_stripe"; - } - } - - # graid - my $graid_out = backquote_command("graid status 2>/dev/null"); - my $current_raid = ""; - foreach my $line ( split( /\n/, $graid_out ) ) { - if ( $line =~ /^(\S+):/ ) { - $current_raid = $1; - } - elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) { - my $dev = $1; - $mount_info{$dev} = "graid: $current_raid"; - } - } - } - if ( has_command("glabel") ) { - my $glabel_out = backquote_command("glabel status 2>/dev/null"); - foreach my $line ( split( /\n/, $glabel_out ) ) { - if ( $line =~ /^\s*(\S+)\s+(\S+)\s+(\S+)/ ) { - my $label = $1; - my $dev = $3; - if ( $dev =~ /^\/dev\// ) { - $mount_info{$dev} = "glabel: $label"; - } - } - } - } - if ( has_command("geli") ) { - my $geli_out = backquote_command("geli status 2>/dev/null"); - foreach my $line ( split( /\n/, $geli_out ) ) { - if ( $line =~ /^(\/dev\/\S+)\s+/ ) { - my $dev = $1; - $mount_info{$dev} = "geli encrypted"; - } - } - } - return ( \%mount_info, \@mount_list ); -} - -#--------------------------------------------------------------------- -# Helper: Get file statistics for a device (cached per device) -sub get_dev_stat { - my ($dev) = @_; - if ( -e $dev ) { - my @st = stat($dev); - if (@st) { - my $size = $st[7]; - my $blocks = int( $size / 512 ); - return ( $size, $blocks ); - } - } - return ( undef, undef ); -} - -#--------------------------------------------------------------------- -# is_boot_partition() +BEGIN { push( @INC, ".." ); } +use WebminCore; +init_config(); +foreign_require( "mount", "mount-lib.pl" ); +foreign_require( "fdisk", "fdisk-lib.pl" ); + +#--------------------------------------------------------------------- +# Helper: Cache mount info +# Returns a hash reference of device => mount point and an arrayref +# containing mount entries. +sub get_all_mount_points_cached { + my %mount_info; + my @mount_list = mount::list_mounted(); + foreach my $m (@mount_list) { + $mount_info{ $m->[0] } = $m->[1]; + } + my $swapinfo = backquote_command("swapinfo -k 2>/dev/null"); + foreach my $line ( split( /\n/, $swapinfo ) ) { + if ( $line =~ /^(\/dev\/\S+)\s+\d+\s+\d+\s+\d+/ ) { + $mount_info{$1} = "swap"; + } + } + + # ZFS, GEOM, glabel, geli remain the same as the original. + if ( has_command("zpool") ) { + my $zpool_out = backquote_command("zpool status 2>/dev/null"); + my $current_pool = ""; + foreach my $line ( split( /\n/, $zpool_out ) ) { + if ( $line =~ /^\s*pool:\s+(\S+)/ ) { + $current_pool = $1; + } + elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) { + my $dev = $1; + $mount_info{$dev} = "ZFS pool: $current_pool"; + } + } + } + if ( has_command("geom") ) { + + # gmirror + my $gmirror_out = backquote_command("gmirror status 2>/dev/null"); + my $current_mirror = ""; + foreach my $line ( split( /\n/, $gmirror_out ) ) { + if ( $line =~ /^(\S+):/ ) { + $current_mirror = $1; + } + elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) { + my $dev = $1; + $mount_info{$dev} = "gmirror: $current_mirror"; + } + } + + # gstripe + my $gstripe_out = backquote_command("gstripe status 2>/dev/null"); + my $current_stripe = ""; + foreach my $line ( split( /\n/, $gstripe_out ) ) { + if ( $line =~ /^(\S+):/ ) { + $current_stripe = $1; + } + elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) { + my $dev = $1; + $mount_info{$dev} = "gstripe: $current_stripe"; + } + } + + # graid + my $graid_out = backquote_command("graid status 2>/dev/null"); + my $current_raid = ""; + foreach my $line ( split( /\n/, $graid_out ) ) { + if ( $line =~ /^(\S+):/ ) { + $current_raid = $1; + } + elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) { + my $dev = $1; + $mount_info{$dev} = "graid: $current_raid"; + } + } + } + if ( has_command("glabel") ) { + my $glabel_out = backquote_command("glabel status 2>/dev/null"); + foreach my $line ( split( /\n/, $glabel_out ) ) { + if ( $line =~ /^\s*(\S+)\s+(\S+)\s+(\S+)/ ) { + my $label = $1; + my $dev = $3; + if ( $dev =~ /^\/dev\// ) { + $mount_info{$dev} = "glabel: $label"; + } + } + } + } + if ( has_command("geli") ) { + my $geli_out = backquote_command("geli status 2>/dev/null"); + foreach my $line ( split( /\n/, $geli_out ) ) { + if ( $line =~ /^(\/dev\/\S+)\s+/ ) { + my $dev = $1; + $mount_info{$dev} = "geli encrypted"; + } + } + } + return ( \%mount_info, \@mount_list ); +} + +#--------------------------------------------------------------------- +# Helper: Get file statistics for a device (cached per device) +sub get_dev_stat { + my ($dev) = @_; + if ( -e $dev ) { + my @st = stat($dev); + if (@st) { + my $size = $st[7]; + my $blocks = int( $size / 512 ); + return ( $size, $blocks ); + } + } + return ( undef, undef ); +} + +#--------------------------------------------------------------------- +# is_boot_partition() # Accepts a partition hash and an optional mount list to avoid # re-calling mount::list_mounted() -sub is_boot_partition { - my ( $part, $mount_list_ref ) = @_; - return 1 - if ( $part->{'type'} eq 'freebsd-boot' or $part->{'type'} eq 'efi' ); - return 1 if ( $part->{'active'} ); - my @mounts = $mount_list_ref ? @$mount_list_ref : mount::list_mounted(); - foreach my $m (@mounts) { - if ( $m->[1] eq '/boot' and $m->[0] eq $part->{'device'} ) { - return 1; - } - } - if ( $part->{'type'} eq 'freebsd-zfs' ) { - my $out = backquote_command("zpool get bootfs 2>/dev/null"); - if ( $out =~ /\s+bootfs\s+\S+\/boot\s+/ ) { - my $pool_out = backquote_command("zpool status 2>/dev/null"); - if ( $pool_out =~ /\Q$part->{'device'}\E/ ) { - return 1; - } - } - } - return 0; -} - -#--------------------------------------------------------------------- -# list_disks_partitions() -# Returns a list of all disks, slices and partitions (optimized) -sub list_disks_partitions { - my @results; - my %dev_stat_cache; # cache stat info per /dev device - my @disk_devices; - - # Get disk devices from /dev directory - if ( opendir( my $dh, "/dev" ) ) { - my @all_devs = readdir($dh); - closedir($dh); - foreach my $dev (@all_devs) { +sub is_boot_partition { + my ( $part, $mount_list_ref ) = @_; + return 1 + if ( $part->{'type'} eq 'freebsd-boot' or $part->{'type'} eq 'efi' ); + return 1 if ( $part->{'active'} ); + my @mounts = $mount_list_ref ? @$mount_list_ref : mount::list_mounted(); + foreach my $m (@mounts) { + if ( $m->[1] eq '/boot' and $m->[0] eq $part->{'device'} ) { + return 1; + } + } + if ( $part->{'type'} eq 'freebsd-zfs' ) { + my $out = backquote_command("zpool get bootfs 2>/dev/null"); + if ( $out =~ /\s+bootfs\s+\S+\/boot\s+/ ) { + my $pool_out = backquote_command("zpool status 2>/dev/null"); + if ( $pool_out =~ /\Q$part->{'device'}\E/ ) { + return 1; + } + } + } + return 0; +} + +#--------------------------------------------------------------------- +# list_disks_partitions() +# Returns a list of all disks, slices and partitions (optimized) +sub list_disks_partitions { + my @results; + my %dev_stat_cache; # cache stat info per /dev device + my @disk_devices; + + # Get disk devices from /dev directory + if ( opendir( my $dh, "/dev" ) ) { + my @all_devs = readdir($dh); + closedir($dh); + foreach my $dev (@all_devs) { if ( $dev =~ /^(ada|ad|da|amrd|nvd|vtbd|nda)(\d+)$/ ) { push( @disk_devices, $dev ); } } } - - # Fallback: sysctl - if ( !@disk_devices ) { - my $sysctl_out = backquote_command("sysctl -n kern.disks 2>/dev/null"); - if ($sysctl_out) { - chomp($sysctl_out); - @disk_devices = split( /\s+/, $sysctl_out ); - } - } - - # Fallback: dmesg - if ( !@disk_devices ) { - my $dmesg_out = backquote_command( + + # Fallback: sysctl + if ( !@disk_devices ) { + my $sysctl_out = backquote_command("sysctl -n kern.disks 2>/dev/null"); + if ($sysctl_out) { + chomp($sysctl_out); + @disk_devices = split( /\s+/, $sysctl_out ); + } + } + + # Fallback: dmesg + if ( !@disk_devices ) { + my $dmesg_out = backquote_command( "dmesg | grep -E '(ada|ad|da|amrd|nvd|vtbd|nda)[0-9]+:' 2>/dev/null" ); while ( $dmesg_out =~ /\b(ada|ad|da|amrd|nvd|vtbd|nda)(\d+):/g ) { @@ -180,275 +180,275 @@ sub list_disks_partitions { push( @disk_devices, $disk ) if ( -e "/dev/$disk" ); } } - - # Fallback: geom - if ( !@disk_devices ) { - my $geom_out = backquote_command("geom disk list 2>/dev/null"); - while ( $geom_out =~ /Name:\s+(\S+)/g ) { - my $disk = $1; - push( @disk_devices, $disk ) if ( -e "/dev/$disk" ); - } - } - - # Get mount information once for all devices - my ( $mount_info, $mount_list ) = get_all_mount_points_cached(); - - foreach my $disk (@disk_devices) { - my $disk_device = "/dev/$disk"; - my $diskinfo = { 'device' => $disk_device, 'name' => $disk }; - - # Determine sector size once per disk (4K-aware) - my $sectorsz = get_disk_sectorsize($disk_device) || 512; - $diskinfo->{'sectorsize'} = $sectorsz; - - # Cache stat information for the disk device - unless ( exists $dev_stat_cache{$disk_device} ) { - my ( $size, $blocks ) = get_dev_stat($disk_device); - $dev_stat_cache{$disk_device} = - ( defined $size ) ? [ $size, $blocks || 0 ] : [ 0, 0 ]; - } - my ( $size, $blocks_cached ) = @{ $dev_stat_cache{$disk_device} }; - if ( $size > 0 ) { - $diskinfo->{'size'} = $size; - $diskinfo->{'blocks'} = int( $size / $sectorsz ); - } - else { - my $diskinfo_out = backquote_command( - "diskinfo " . quote_path($disk) . " 2>/dev/null" ); - if ( $diskinfo_out =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(.*)/ ) { - $diskinfo->{'size'} = $1; - $diskinfo->{'blocks'} = int( $1 / $sectorsz ); - $diskinfo->{'cylinders'} = $2; - $diskinfo->{'heads'} = $3; - $diskinfo->{'sectors'} = $4; - if ( defined $5 && $5 ne '' && $5 !~ /^\d+$/ ) { - $diskinfo->{'model'} = $5; - } - } - if ( !$diskinfo->{'size'} ) { - my $diskinfo_v_out = backquote_command( - "diskinfo -v " . quote_path($disk) . " 2>/dev/null" ); - if ( $diskinfo_v_out =~ /sectorsize:\s*(\d+)/i ) { - $sectorsz = $1; - $diskinfo->{'sectorsize'} = $sectorsz; - } - if ( $diskinfo_v_out =~ /mediasize in bytes:\s+(\d+)/i ) { - $diskinfo->{'size'} = $1; - $diskinfo->{'blocks'} = int( $1 / $sectorsz ); - } - if ( $diskinfo_v_out =~ /descr:\s+(.*)/ ) { - $diskinfo->{'model'} = $1; - } - } - if ( !$diskinfo->{'model'} ) { - my $cam_id_out = - backquote_command( "camcontrol identify " - . quote_path($disk) - . " 2>/dev/null" ); - if ( $cam_id_out =~ /model\s+(.*)/i ) { - my $m = $1; - $m =~ s/^\s+|\s+$//g; - $diskinfo->{'model'} = $m; - } - } - if ( !$diskinfo->{'model'} ) { - my $inq_out = - backquote_command( "camcontrol inquiry " - . quote_path($disk) - . " 2>/dev/null" ); - if ( $inq_out =~ /<([^>]+)>/ ) { - $diskinfo->{'model'} = $1; - } - else { - my ($vendor) = - ( $inq_out =~ /Vendor:\s*(\S.*?)(?:\s{2,}|$)/i ); - my ($product) = - ( $inq_out =~ /Product:\s*(\S.*?)(?:\s{2,}|$)/i ); - if ( $vendor || $product ) { - $diskinfo->{'model'} = join( ' ', - grep { defined && length } ( $vendor, $product ) ); - } - } - } - if ( !$diskinfo->{'model'} ) { - my $geom = get_detailed_disk_info($disk_device); - if ( $geom && $geom->{'descr'} ) { - $diskinfo->{'model'} = $geom->{'descr'}; - } - elsif ( $geom && $geom->{'ident'} ) { - $diskinfo->{'model'} = $geom->{'ident'}; - } - } - - # If size still not known, try GEOM mediasize - if ( !$diskinfo->{'size'} ) { - my $geom2 = get_detailed_disk_info($disk_device); - if ( $geom2 && $geom2->{'mediasize_bytes'} ) { - $diskinfo->{'size'} = $geom2->{'mediasize_bytes'}; - $diskinfo->{'blocks'} = int( $diskinfo->{'size'} / - ( $diskinfo->{'sectorsize'} || 512 ) ); - } - } - } - - # Determine disk type - if ( $disk =~ /^ada/ or $disk =~ /^ad/ ) { - $diskinfo->{'type'} = 'ide'; - } - elsif ( $disk =~ /^da/ ) { - $diskinfo->{'type'} = 'scsi'; - } - elsif ( $disk =~ /^amrd/ ) { - $diskinfo->{'type'} = 'memdisk'; - } + + # Fallback: geom + if ( !@disk_devices ) { + my $geom_out = backquote_command("geom disk list 2>/dev/null"); + while ( $geom_out =~ /Name:\s+(\S+)/g ) { + my $disk = $1; + push( @disk_devices, $disk ) if ( -e "/dev/$disk" ); + } + } + + # Get mount information once for all devices + my ( $mount_info, $mount_list ) = get_all_mount_points_cached(); + + foreach my $disk (@disk_devices) { + my $disk_device = "/dev/$disk"; + my $diskinfo = { 'device' => $disk_device, 'name' => $disk }; + + # Determine sector size once per disk (4K-aware) + my $sectorsz = get_disk_sectorsize($disk_device) || 512; + $diskinfo->{'sectorsize'} = $sectorsz; + + # Cache stat information for the disk device + unless ( exists $dev_stat_cache{$disk_device} ) { + my ( $size, $blocks ) = get_dev_stat($disk_device); + $dev_stat_cache{$disk_device} = + ( defined $size ) ? [ $size, $blocks || 0 ] : [ 0, 0 ]; + } + my ( $size, $blocks_cached ) = @{ $dev_stat_cache{$disk_device} }; + if ( $size > 0 ) { + $diskinfo->{'size'} = $size; + $diskinfo->{'blocks'} = int( $size / $sectorsz ); + } + else { + my $diskinfo_out = backquote_command( + "diskinfo " . quote_path($disk) . " 2>/dev/null" ); + if ( $diskinfo_out =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(.*)/ ) { + $diskinfo->{'size'} = $1; + $diskinfo->{'blocks'} = int( $1 / $sectorsz ); + $diskinfo->{'cylinders'} = $2; + $diskinfo->{'heads'} = $3; + $diskinfo->{'sectors'} = $4; + if ( defined $5 && $5 ne '' && $5 !~ /^\d+$/ ) { + $diskinfo->{'model'} = $5; + } + } + if ( !$diskinfo->{'size'} ) { + my $diskinfo_v_out = backquote_command( + "diskinfo -v " . quote_path($disk) . " 2>/dev/null" ); + if ( $diskinfo_v_out =~ /sectorsize:\s*(\d+)/i ) { + $sectorsz = $1; + $diskinfo->{'sectorsize'} = $sectorsz; + } + if ( $diskinfo_v_out =~ /mediasize in bytes:\s+(\d+)/i ) { + $diskinfo->{'size'} = $1; + $diskinfo->{'blocks'} = int( $1 / $sectorsz ); + } + if ( $diskinfo_v_out =~ /descr:\s+(.*)/ ) { + $diskinfo->{'model'} = $1; + } + } + if ( !$diskinfo->{'model'} ) { + my $cam_id_out = + backquote_command( "camcontrol identify " + . quote_path($disk) + . " 2>/dev/null" ); + if ( $cam_id_out =~ /model\s+(.*)/i ) { + my $m = $1; + $m =~ s/^\s+|\s+$//g; + $diskinfo->{'model'} = $m; + } + } + if ( !$diskinfo->{'model'} ) { + my $inq_out = + backquote_command( "camcontrol inquiry " + . quote_path($disk) + . " 2>/dev/null" ); + if ( $inq_out =~ /<([^>]+)>/ ) { + $diskinfo->{'model'} = $1; + } + else { + my ($vendor) = + ( $inq_out =~ /Vendor:\s*(\S.*?)(?:\s{2,}|$)/i ); + my ($product) = + ( $inq_out =~ /Product:\s*(\S.*?)(?:\s{2,}|$)/i ); + if ( $vendor || $product ) { + $diskinfo->{'model'} = join( ' ', + grep { defined && length } ( $vendor, $product ) ); + } + } + } + if ( !$diskinfo->{'model'} ) { + my $geom = get_detailed_disk_info($disk_device); + if ( $geom && $geom->{'descr'} ) { + $diskinfo->{'model'} = $geom->{'descr'}; + } + elsif ( $geom && $geom->{'ident'} ) { + $diskinfo->{'model'} = $geom->{'ident'}; + } + } + + # If size still not known, try GEOM mediasize + if ( !$diskinfo->{'size'} ) { + my $geom2 = get_detailed_disk_info($disk_device); + if ( $geom2 && $geom2->{'mediasize_bytes'} ) { + $diskinfo->{'size'} = $geom2->{'mediasize_bytes'}; + $diskinfo->{'blocks'} = int( $diskinfo->{'size'} / + ( $diskinfo->{'sectorsize'} || 512 ) ); + } + } + } + + # Determine disk type + if ( $disk =~ /^ada/ or $disk =~ /^ad/ ) { + $diskinfo->{'type'} = 'ide'; + } + elsif ( $disk =~ /^da/ ) { + $diskinfo->{'type'} = 'scsi'; + } + elsif ( $disk =~ /^amrd/ ) { + $diskinfo->{'type'} = 'memdisk'; + } elsif ( $disk =~ /^(?:nvd|nda)/ ) { $diskinfo->{'type'} = 'nvme'; } - elsif ( $disk =~ /^vtbd/ ) { - $diskinfo->{'type'} = 'virtio'; - } - - # Process slices and partitions - $diskinfo->{'slices'} = []; - if ( has_command("gpart") ) { - my $gpart_out = backquote_command( - "gpart show " . quote_path($disk) . " 2>/dev/null" ); - my @lines = split( /\n/, $gpart_out ); - my $in_disk = 0; - my $disk_scheme = undef; # GPT, MBR, etc. - foreach my $line (@lines) { - if ( $line =~ /=>/ ) { - $in_disk = 1; - - # Try to extract scheme from header line - if ( $line =~ /=>.*?\b$disk\b\s+(\S+)/ ) { - $disk_scheme = $1; - } - next; - } - if ( $in_disk and $line =~ /^\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)/ ) - { - my ( $start, $num_blocks, $name_or_idx, $raw_type ) = - ( $1, $2, $3, $4 ); - next if ( $name_or_idx eq '-' or $raw_type eq 'free' ); - my $slice_type = - ( $raw_type eq '-' ) ? "freebsd" : $raw_type; - - # Determine slice index and device name - my ( $slice_index, $slice_devname ); - if ( $name_or_idx =~ /^$disk(?:p|s)(\d+)$/ ) { - $slice_index = $1; - $slice_devname = $name_or_idx; - } - elsif ( $name_or_idx =~ /^\d+$/ ) { - $slice_index = $name_or_idx; - my $sep = - ( defined $disk_scheme && $disk_scheme =~ /GPT/i ) - ? 'p' - : 's'; - $slice_devname = $disk . $sep . $slice_index; - } - else { - # Fallback: use as provided - $slice_devname = $name_or_idx; - - # Try to extract index from suffix if possible - ($slice_index) = ( $slice_devname =~ /(?:p|s)(\d+)$/ ); - $slice_index ||= $name_or_idx; - } - my $slice_device = "/dev/$slice_devname"; - - my $slice = { - 'number' => $slice_index, - 'startblock' => $start, - 'blocks' => $num_blocks, - 'size' => $num_blocks * $sectorsz, - 'type' => $slice_type, - 'device' => $slice_device, - 'parts' => [] - }; - $slice->{'used'} = $mount_info{$slice_device}; - - # Get partitions for this slice once, using the correct provider name - my $gpart_slice_out = - backquote_command( "gpart show " - . quote_path($slice_devname) - . " 2>/dev/null" ); - my @slice_lines = split( /\n/, $gpart_slice_out ); - my $in_slice = 0; - my $slice_scheme; - foreach my $slice_line (@slice_lines) { - if ( $slice_line =~ /=>.*?\s+$slice_devname\s+(\S+)/ ) { - $in_slice = 1; - $slice_scheme = $1; # e.g., BSD, GPT - next; - } - if ( $in_slice - and $slice_line =~ - /^\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)/ ) - { - my ( - $p_start, $p_blocks, - $part_idx_or_name, $raw_ptype - ) = ( $1, $2, $3, $4 ); - next - if ( $part_idx_or_name eq '-' - or $raw_ptype eq 'free' ); - my $part_type = - ( $raw_ptype eq '-' and $part_idx_or_name ne '-' ) - ? "freebsd-ufs" - : $raw_ptype; - - # For BSD disklabel, third column is index (1-based), convert to letter - my ( $part_letter, $part_device ); - if ( $slice_scheme - && $slice_scheme =~ /BSD/i - && $part_idx_or_name =~ /^\d+$/ ) - { - my $idx = int($part_idx_or_name); - $part_letter = chr( ord('a') + $idx - 1 ) - ; # 1 -> 'a', 2 -> 'b', etc. - $part_device = $slice_device . $part_letter; - } - else { - # For other schemes or if already a name - $part_device = "/dev/$part_idx_or_name"; - $part_letter = substr( $part_idx_or_name, -1 ); - } - my $part = { - 'letter' => $part_letter, - 'startblock' => $p_start, - 'blocks' => $p_blocks, - 'size' => $p_blocks * $sectorsz, - 'type' => $part_type, - 'device' => $part_device, - }; - $part->{'used'} = $mount_info{$part_device}; - push( @{ $slice->{'parts'} }, $part ); - } - } - push( @{ $diskinfo->{'slices'} }, $slice ); - } - } - } - else { + elsif ( $disk =~ /^vtbd/ ) { + $diskinfo->{'type'} = 'virtio'; + } + + # Process slices and partitions + $diskinfo->{'slices'} = []; + if ( has_command("gpart") ) { + my $gpart_out = backquote_command( + "gpart show " . quote_path($disk) . " 2>/dev/null" ); + my @lines = split( /\n/, $gpart_out ); + my $in_disk = 0; + my $disk_scheme = undef; # GPT, MBR, etc. + foreach my $line (@lines) { + if ( $line =~ /=>/ ) { + $in_disk = 1; + + # Try to extract scheme from header line + if ( $line =~ /=>.*?\b$disk\b\s+(\S+)/ ) { + $disk_scheme = $1; + } + next; + } + if ( $in_disk and $line =~ /^\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)/ ) + { + my ( $start, $num_blocks, $name_or_idx, $raw_type ) = + ( $1, $2, $3, $4 ); + next if ( $name_or_idx eq '-' or $raw_type eq 'free' ); + my $slice_type = + ( $raw_type eq '-' ) ? "freebsd" : $raw_type; + + # Determine slice index and device name + my ( $slice_index, $slice_devname ); + if ( $name_or_idx =~ /^$disk(?:p|s)(\d+)$/ ) { + $slice_index = $1; + $slice_devname = $name_or_idx; + } + elsif ( $name_or_idx =~ /^\d+$/ ) { + $slice_index = $name_or_idx; + my $sep = + ( defined $disk_scheme && $disk_scheme =~ /GPT/i ) + ? 'p' + : 's'; + $slice_devname = $disk . $sep . $slice_index; + } + else { + # Fallback: use as provided + $slice_devname = $name_or_idx; + + # Try to extract index from suffix if possible + ($slice_index) = ( $slice_devname =~ /(?:p|s)(\d+)$/ ); + $slice_index ||= $name_or_idx; + } + my $slice_device = "/dev/$slice_devname"; + + my $slice = { + 'number' => $slice_index, + 'startblock' => $start, + 'blocks' => $num_blocks, + 'size' => $num_blocks * $sectorsz, + 'type' => $slice_type, + 'device' => $slice_device, + 'parts' => [] + }; + $slice->{'used'} = $mount_info{$slice_device}; + + # Get partitions for this slice once, using the correct provider name + my $gpart_slice_out = + backquote_command( "gpart show " + . quote_path($slice_devname) + . " 2>/dev/null" ); + my @slice_lines = split( /\n/, $gpart_slice_out ); + my $in_slice = 0; + my $slice_scheme; + foreach my $slice_line (@slice_lines) { + if ( $slice_line =~ /=>.*?\s+$slice_devname\s+(\S+)/ ) { + $in_slice = 1; + $slice_scheme = $1; # e.g., BSD, GPT + next; + } + if ( $in_slice + and $slice_line =~ + /^\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)/ ) + { + my ( + $p_start, $p_blocks, + $part_idx_or_name, $raw_ptype + ) = ( $1, $2, $3, $4 ); + next + if ( $part_idx_or_name eq '-' + or $raw_ptype eq 'free' ); + my $part_type = + ( $raw_ptype eq '-' and $part_idx_or_name ne '-' ) + ? "freebsd-ufs" + : $raw_ptype; + + # For BSD disklabel, third column is index (1-based), convert to letter + my ( $part_letter, $part_device ); + if ( $slice_scheme + && $slice_scheme =~ /BSD/i + && $part_idx_or_name =~ /^\d+$/ ) + { + my $idx = int($part_idx_or_name); + $part_letter = chr( ord('a') + $idx - 1 ) + ; # 1 -> 'a', 2 -> 'b', etc. + $part_device = $slice_device . $part_letter; + } + else { + # For other schemes or if already a name + $part_device = "/dev/$part_idx_or_name"; + $part_letter = substr( $part_idx_or_name, -1 ); + } + my $part = { + 'letter' => $part_letter, + 'startblock' => $p_start, + 'blocks' => $p_blocks, + 'size' => $p_blocks * $sectorsz, + 'type' => $part_type, + 'device' => $part_device, + }; + $part->{'used'} = $mount_info{$part_device}; + push( @{ $slice->{'parts'} }, $part ); + } + } + push( @{ $diskinfo->{'slices'} }, $slice ); + } + } + } + else { # If no slices found with gpart, use fdisk if available # (similar caching ideas apply) - if ( has_command("fdisk") ) { - my $fdisk_out = backquote_command( - "fdisk " . quote_path("/dev/$disk") . " 2>/dev/null" ); - foreach my $line ( split( /\n/, $fdisk_out ) ) { - if ( $line =~ /^\s*(\d+):\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)/ ) - { - my $slice_device = "/dev/${disk}s$1"; - my $slice = { - 'number' => $1, - 'startblock' => $2, - 'blocks' => ( $4 - $2 + 1 ), - 'size' => ( $4 - $2 + 1 ) * $sectorsz, - 'type' => $5, - 'device' => $slice_device, - 'parts' => [] + if ( has_command("fdisk") ) { + my $fdisk_out = backquote_command( + "fdisk " . quote_path("/dev/$disk") . " 2>/dev/null" ); + foreach my $line ( split( /\n/, $fdisk_out ) ) { + if ( $line =~ /^\s*(\d+):\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)/ ) + { + my $slice_device = "/dev/${disk}s$1"; + my $slice = { + 'number' => $1, + 'startblock' => $2, + 'blocks' => ( $4 - $2 + 1 ), + 'size' => ( $4 - $2 + 1 ) * $sectorsz, + 'type' => $5, + 'device' => $slice_device, + 'parts' => [] }; $slice->{'used'} = $mount_info{$slice_device}; my $dlcmd = _disklabel_cmd(); @@ -463,94 +463,94 @@ sub list_disks_partitions { if ( $label_line =~ /^(\s*)([a-h]):\s+(\d+)\s+(\d+)\s+(\S+)/ ) { - my $part_device = "${slice_device}$2"; - my $part = { - 'letter' => $2, - 'startblock' => $3, - 'blocks' => $4, - 'size' => $4 * $sectorsz, - 'type' => $5, - 'device' => $part_device - }; - $part->{'used'} = $mount_info{$part_device}; - push( @{ $slice->{'parts'} }, $part ); - } - } - push( @{ $diskinfo->{'slices'} }, $slice ); - } - } - } - } - - # If size was not determined, estimate from slices if available - if ( !$diskinfo->{'size'} and @{ $diskinfo->{'slices'} } ) { - my $total_size = 0; - $total_size += $_->{'size'} for @{ $diskinfo->{'slices'} }; - if ( $total_size > 0 ) { - $diskinfo->{'size'} = $total_size; - $diskinfo->{'blocks'} = - int( $total_size / ( $diskinfo->{'sectorsize'} || 512 ) ); - } - } - - # Finally, add this disk (dummy size set if necessary) - if ( $diskinfo->{'size'} or -e $disk_device ) { - $diskinfo->{'size'} = $diskinfo->{'size'} || 0; - $diskinfo->{'blocks'} = $diskinfo->{'blocks'} || 0; - push( @results, $diskinfo ); - } - } - return @results; -} - -# check_fdisk() unchanged -sub check_fdisk { - if ( !has_command("fdisk") and !has_command("gpart") ) { - return text( 'index_efdisk', "fdisk", "gpart" ); - } - return undef; -} - -# is_using_gpart() -sub is_using_gpart { - return has_command("gpart") ? 1 : 0; -} - -# disk_name(device) extracts name from /dev/device -sub disk_name { - my ($device) = @_; - $device =~ s/^\/dev\///; - return $device; -} - -# slice_name(slice) -sub slice_name { - my ($slice) = @_; - if ( $slice->{'device'} =~ /\/dev\/(\S+)/ ) { - return $1; - } - return $slice->{'number'}; -} - -# slice_number(slice) -sub slice_number { - my ($slice) = @_; - if ( $slice->{'device'} =~ /\/dev\/\S+s(\d+)/ ) { - return $1; - } - if ( $slice->{'device'} =~ /\/dev\/\S+p(\d+)/ ) { - return $1; - } - return $slice->{'number'}; -} - -sub _safe_uint { - my ($v) = @_; - return undef unless defined $v; - return undef unless $v =~ /^\d+$/; - return int($v); -} - + my $part_device = "${slice_device}$2"; + my $part = { + 'letter' => $2, + 'startblock' => $3, + 'blocks' => $4, + 'size' => $4 * $sectorsz, + 'type' => $5, + 'device' => $part_device + }; + $part->{'used'} = $mount_info{$part_device}; + push( @{ $slice->{'parts'} }, $part ); + } + } + push( @{ $diskinfo->{'slices'} }, $slice ); + } + } + } + } + + # If size was not determined, estimate from slices if available + if ( !$diskinfo->{'size'} and @{ $diskinfo->{'slices'} } ) { + my $total_size = 0; + $total_size += $_->{'size'} for @{ $diskinfo->{'slices'} }; + if ( $total_size > 0 ) { + $diskinfo->{'size'} = $total_size; + $diskinfo->{'blocks'} = + int( $total_size / ( $diskinfo->{'sectorsize'} || 512 ) ); + } + } + + # Finally, add this disk (dummy size set if necessary) + if ( $diskinfo->{'size'} or -e $disk_device ) { + $diskinfo->{'size'} = $diskinfo->{'size'} || 0; + $diskinfo->{'blocks'} = $diskinfo->{'blocks'} || 0; + push( @results, $diskinfo ); + } + } + return @results; +} + +# check_fdisk() unchanged +sub check_fdisk { + if ( !has_command("fdisk") and !has_command("gpart") ) { + return text( 'index_efdisk', "fdisk", "gpart" ); + } + return undef; +} + +# is_using_gpart() +sub is_using_gpart { + return has_command("gpart") ? 1 : 0; +} + +# disk_name(device) extracts name from /dev/device +sub disk_name { + my ($device) = @_; + $device =~ s/^\/dev\///; + return $device; +} + +# slice_name(slice) +sub slice_name { + my ($slice) = @_; + if ( $slice->{'device'} =~ /\/dev\/(\S+)/ ) { + return $1; + } + return $slice->{'number'}; +} + +# slice_number(slice) +sub slice_number { + my ($slice) = @_; + if ( $slice->{'device'} =~ /\/dev\/\S+s(\d+)/ ) { + return $1; + } + if ( $slice->{'device'} =~ /\/dev\/\S+p(\d+)/ ) { + return $1; + } + return $slice->{'number'}; +} + +sub _safe_uint { + my ($v) = @_; + return undef unless defined $v; + return undef unless $v =~ /^\d+$/; + return int($v); +} + sub _safe_letter { my ($v) = @_; return undef unless defined $v; @@ -684,65 +684,65 @@ sub _disklabel_update_partition_line { #--------------------------------------------------------------------- # Filesystem command generation and slice/partition modification functions sub create_slice { - my ( $disk, $slice ) = @_; - my $cmd; - if ( is_using_gpart() ) { - + my ( $disk, $slice ) = @_; + my $cmd; + if ( is_using_gpart() ) { + # Ensure a partitioning scheme exists (default to MBR for non-GPT, # GPT if new) before adding - my $base = disk_name( $disk->{'device'} ); - my $ds = get_disk_structure($base); - my $scheme = - 'MBR'; # default for existing disks or when type suggests MBR - if ( !$ds || !$ds->{'scheme'} ) { - - # No scheme exists - decide based on partition type - if ( $slice->{'type'} =~ /^(freebsd|fat32|ntfs|linux)$/i ) { - $scheme = 'MBR'; - } - else { - $scheme = 'GPT'; - } - my $init = "gpart create -s $scheme " . quote_path($base); - my $init_out = backquote_command("$init 2>&1"); - if ( $? != 0 && $init_out !~ /File exists|already exists/i ) { - return $init_out; - } - - # Refresh disk structure after creation - $ds = get_disk_structure($base); - } - else { - $scheme = $ds->{'scheme'}; - } - - # Defense-in-depth: validate type against allowed list for scheme - my %allowed = - map { $_->[0] => 1 } list_partition_types($scheme); - return $text{'nslice_etype'} - unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } ); - - $cmd = "gpart add -t " . $slice->{'type'}; - if ( defined $slice->{'startblock'} - && $slice->{'startblock'} =~ /^\d+$/ ) - { - $cmd .= " -b " . int( $slice->{'startblock'} ); - } - if ( defined $slice->{'blocks'} && $slice->{'blocks'} =~ /^\d+$/ ) { - $cmd .= " -s " . int( $slice->{'blocks'} ); - } - $cmd .= " " . quote_path($base); - my $out = backquote_command("$cmd 2>&1"); - if ($?) { - return $out; - } - - # After successful creation, populate the device field for the slice - # Determine the separator based on scheme - my $sep = ( $scheme =~ /GPT/i ) ? 'p' : 's'; - my $slice_num = $slice->{'number'}; - $slice->{'device'} = "/dev/${base}${sep}${slice_num}"; - return undef; + my $base = disk_name( $disk->{'device'} ); + my $ds = get_disk_structure($base); + my $scheme = + 'MBR'; # default for existing disks or when type suggests MBR + if ( !$ds || !$ds->{'scheme'} ) { + + # No scheme exists - decide based on partition type + if ( $slice->{'type'} =~ /^(freebsd|fat32|ntfs|linux)$/i ) { + $scheme = 'MBR'; + } + else { + $scheme = 'GPT'; + } + my $init = "gpart create -s $scheme " . quote_path($base); + my $init_out = backquote_command("$init 2>&1"); + if ( $? != 0 && $init_out !~ /File exists|already exists/i ) { + return $init_out; + } + + # Refresh disk structure after creation + $ds = get_disk_structure($base); + } + else { + $scheme = $ds->{'scheme'}; + } + + # Defense-in-depth: validate type against allowed list for scheme + my %allowed = + map { $_->[0] => 1 } list_partition_types($scheme); + return $text{'nslice_etype'} + unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } ); + + $cmd = "gpart add -t " . $slice->{'type'}; + if ( defined $slice->{'startblock'} + && $slice->{'startblock'} =~ /^\d+$/ ) + { + $cmd .= " -b " . int( $slice->{'startblock'} ); + } + if ( defined $slice->{'blocks'} && $slice->{'blocks'} =~ /^\d+$/ ) { + $cmd .= " -s " . int( $slice->{'blocks'} ); + } + $cmd .= " " . quote_path($base); + my $out = backquote_command("$cmd 2>&1"); + if ($?) { + return $out; + } + + # After successful creation, populate the device field for the slice + # Determine the separator based on scheme + my $sep = ( $scheme =~ /GPT/i ) ? 'p' : 's'; + my $slice_num = $slice->{'number'}; + $slice->{'device'} = "/dev/${base}${sep}${slice_num}"; + return undef; } else { my %allowed = map { $_ => 1 } fdisk::list_tags(); @@ -771,23 +771,23 @@ sub create_slice { return undef; } } - -sub delete_slice { - my ( $disk, $slice ) = @_; - if ( is_boot_partition($slice) ) { return $text{'slice_eboot'}; } - foreach my $p ( @{ $slice->{'parts'} } ) { - if ( is_boot_partition($p) ) { return $text{'slice_eboot'}; } - } - my $cmd; + +sub delete_slice { + my ( $disk, $slice ) = @_; + if ( is_boot_partition($slice) ) { return $text{'slice_eboot'}; } + foreach my $p ( @{ $slice->{'parts'} } ) { + if ( is_boot_partition($p) ) { return $text{'slice_eboot'}; } + } + my $cmd; if ( is_using_gpart() ) { - my $idx = _safe_uint( slice_number($slice) ); - return $text{'slice_egone'} unless defined $idx; - $cmd = - "gpart delete -i " - . $idx . " " - . quote_path( disk_name( $disk->{'device'} ) ); - my $out = backquote_command("$cmd 2>&1"); - return ($?) ? $out : undef; + my $idx = _safe_uint( slice_number($slice) ); + return $text{'slice_egone'} unless defined $idx; + $cmd = + "gpart delete -i " + . $idx . " " + . quote_path( disk_name( $disk->{'device'} ) ); + my $out = backquote_command("$cmd 2>&1"); + return ($?) ? $out : undef; } else { my $sn = _safe_uint( $slice->{'number'} ); @@ -801,20 +801,20 @@ sub delete_slice { return undef; } } - -sub delete_partition { - my ( $disk, $slice, $part ) = @_; - if ( is_boot_partition($part) ) { return $text{'part_eboot'}; } - my $cmd; - if ( is_using_gpart() ) { - - # BSD disklabel uses 1-based indexing: 'a' = 1, 'b' = 2, etc. - my $pl = _safe_letter( $part->{'letter'} ); - return $text{'part_egone'} unless defined $pl; - my $idx = ( ord($pl) - ord('a') ) + 1; - $cmd = "gpart delete -i $idx " . quote_path( slice_name($slice) ); - my $out = backquote_command("$cmd 2>&1"); - return ($?) ? $out : undef; + +sub delete_partition { + my ( $disk, $slice, $part ) = @_; + if ( is_boot_partition($part) ) { return $text{'part_eboot'}; } + my $cmd; + if ( is_using_gpart() ) { + + # BSD disklabel uses 1-based indexing: 'a' = 1, 'b' = 2, etc. + my $pl = _safe_letter( $part->{'letter'} ); + return $text{'part_egone'} unless defined $pl; + my $idx = ( ord($pl) - ord('a') ) + 1; + $cmd = "gpart delete -i $idx " . quote_path( slice_name($slice) ); + my $out = backquote_command("$cmd 2>&1"); + return ($?) ? $out : undef; } else { my $pl = _safe_letter( $part->{'letter'} ); @@ -834,32 +834,32 @@ sub delete_partition { return undef; } } - + sub modify_slice { my ( $disk, $oldslice, $slice, $part ) = @_; if ( is_boot_partition($slice) ) { return $text{'slice_eboot'}; } foreach my $p ( @{ $slice->{'parts'} } ) { if ( is_boot_partition($p) ) { return $text{'slice_eboot'}; } } - my $cmd; + my $cmd; if ( is_using_gpart() ) { - my $base = disk_name( $disk->{'device'} ); - my $ds = get_disk_structure($base); - my $scheme = ( $ds && $ds->{'scheme'} ) ? $ds->{'scheme'} : 'GPT'; - my %allowed = - map { $_->[0] => 1 } list_partition_types($scheme); - return $text{'nslice_etype'} - unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } ); - - my $idx = _safe_uint( slice_number($slice) ); - return $text{'slice_egone'} unless defined $idx; - $cmd = - "gpart modify -i " - . $idx . " -t " - . $slice->{'type'} . " " - . quote_path($base); - my $out = backquote_command("$cmd 2>&1"); - return ($?) ? $out : undef; + my $base = disk_name( $disk->{'device'} ); + my $ds = get_disk_structure($base); + my $scheme = ( $ds && $ds->{'scheme'} ) ? $ds->{'scheme'} : 'GPT'; + my %allowed = + map { $_->[0] => 1 } list_partition_types($scheme); + return $text{'nslice_etype'} + unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } ); + + my $idx = _safe_uint( slice_number($slice) ); + return $text{'slice_egone'} unless defined $idx; + $cmd = + "gpart modify -i " + . $idx . " -t " + . $slice->{'type'} . " " + . quote_path($base); + my $out = backquote_command("$cmd 2>&1"); + return ($?) ? $out : undef; } else { my %allowed = map { $_ => 1 } fdisk::list_tags(); @@ -887,42 +887,42 @@ sub modify_slice { return undef; } } - -sub save_partition { - my ( $disk, $slice, $part ) = @_; - my $cmd; - if ( is_using_gpart() ) { - my $provider = slice_name($slice); - - # Detect if this provider is a BSD label (sub-partitions) or GPT/MBR - my $show = - backquote_command( "gpart show " . quote_path($provider) . " 2>&1" ); - if ( $show =~ /\bBSD\b/ ) { - + +sub save_partition { + my ( $disk, $slice, $part ) = @_; + my $cmd; + if ( is_using_gpart() ) { + my $provider = slice_name($slice); + + # Detect if this provider is a BSD label (sub-partitions) or GPT/MBR + my $show = + backquote_command( "gpart show " . quote_path($provider) . " 2>&1" ); + if ( $show =~ /\bBSD\b/ ) { + # Inner BSD label: index is 1-based a->1, b->2, etc. Only # FreeBSD partition types are valid here. - my $pl = _safe_letter( $part->{'letter'} ); - return $text{'part_egone'} unless defined $pl; - my $idx = ( ord($pl) - ord('a') ) + 1; - my %allowed = - map { $_->[0] => 1 } list_partition_types('BSD'); - return $text{'part_etype'} - unless ( defined $part->{'type'} && $allowed{ $part->{'type'} } ); - $cmd = - "gpart modify -i $idx -t " - . $part->{'type'} . " " - . quote_path($provider); - } - else { + my $pl = _safe_letter( $part->{'letter'} ); + return $text{'part_egone'} unless defined $pl; + my $idx = ( ord($pl) - ord('a') ) + 1; + my %allowed = + map { $_->[0] => 1 } list_partition_types('BSD'); + return $text{'part_etype'} + unless ( defined $part->{'type'} && $allowed{ $part->{'type'} } ); + $cmd = + "gpart modify -i $idx -t " + . $part->{'type'} . " " + . quote_path($provider); + } + else { # Not a BSD label; modifying a top-level partition by letter is # invalid. Return an error with guidance. - return + return "Invalid operation: attempting to modify non-BSD sub-partition " . "by letter. Use slice editing for top-level partitions."; - } - my $out = backquote_command("$cmd 2>&1"); - return ($?) ? $out : undef; - } + } + my $out = backquote_command("$cmd 2>&1"); + return ($?) ? $out : undef; + } else { my $pl = _safe_letter( $part->{'letter'} ); return $text{'part_egone'} unless defined $pl; @@ -948,8 +948,8 @@ sub save_partition { return undef; } } - -# Create a new BSD partition inside an MBR slice (gpart BSD label) + +# Create a new BSD partition inside an MBR slice (gpart BSD label) sub create_partition { my ( $disk, $slice, $part ) = @_; @@ -1037,34 +1037,34 @@ sub create_partition { $part->{'device'} = $slice->{'device'} . $pl; return undef; } - -sub get_create_filesystem_command { - my ( $disk, $slice, $part, $options ) = @_; - my $device = $part ? $part->{'device'} : $slice->{'device'}; - my @cmd = ("newfs"); - if ( defined $options->{'free'} && $options->{'free'} =~ /^\d+$/ ) { - push( @cmd, "-m", $options->{'free'} ); - } - if ( defined $options->{'label'} && length $options->{'label'} ) { - push( @cmd, "-L", quote_path( $options->{'label'} ) ); - } - push( @cmd, "-t" ) if ( $options->{'trim'} ); - push( @cmd, quote_path($device) ); - return join( " ", @cmd ); -} - -# Helper to set a GPT or BSD partition label after filesystem creation -sub set_partition_label { - my (%args) = @_; - my $disk = $args{'disk'}; - my $slice = $args{'slice'}; - my $part = $args{'part'}; # optional - my $label = $args{'label'}; - return if ( !defined $label || $label eq '' ); - my $base = $disk->{'device'}; - $base =~ s{^/dev/}{}; - my $ds = get_disk_structure($base); - + +sub get_create_filesystem_command { + my ( $disk, $slice, $part, $options ) = @_; + my $device = $part ? $part->{'device'} : $slice->{'device'}; + my @cmd = ("newfs"); + if ( defined $options->{'free'} && $options->{'free'} =~ /^\d+$/ ) { + push( @cmd, "-m", $options->{'free'} ); + } + if ( defined $options->{'label'} && length $options->{'label'} ) { + push( @cmd, "-L", quote_path( $options->{'label'} ) ); + } + push( @cmd, "-t" ) if ( $options->{'trim'} ); + push( @cmd, quote_path($device) ); + return join( " ", @cmd ); +} + +# Helper to set a GPT or BSD partition label after filesystem creation +sub set_partition_label { + my (%args) = @_; + my $disk = $args{'disk'}; + my $slice = $args{'slice'}; + my $part = $args{'part'}; # optional + my $label = $args{'label'}; + return if ( !defined $label || $label eq '' ); + my $base = $disk->{'device'}; + $base =~ s{^/dev/}{}; + my $ds = get_disk_structure($base); + # GPT: label at disk level via gpart modify if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) { my $idx = @@ -1074,41 +1074,41 @@ sub set_partition_label { "gpart modify -i $idx -l " . quote_path($label) . " " . quote_path($base); - my $out = backquote_command("$cmd 2>&1"); - return ( $? ? $out : undef ); - } - return undef; - } - - # MBR: use glabel for slice-level or partition-level labels - my $device = $part ? $part->{'device'} : $slice->{'device'}; - if ( $device && has_command('glabel') ) { - - # Remove existing glabel if present, then add new one - my $existing = backquote_command( - "glabel status 2>/dev/null | grep " . quote_path($device) ); - if ( $existing =~ /^(\S+)\s+/ ) { - my $old_label = $1; - my $destroy_out = backquote_command( - "glabel destroy " . quote_path($old_label) . " 2>&1" ); - } - my $cmd = - "glabel label " . quote_path($label) . " " . quote_path($device); - my $out = backquote_command("$cmd 2>&1"); - return ( $? ? $out : undef ); - } - return undef; -} - -sub remove_partition_label { - my (%args) = @_; - my $disk = $args{'disk'}; - my $slice = $args{'slice'}; - my $part = $args{'part'}; - my $base = $disk->{'device'}; - $base =~ s{^/dev/}{}; - my $ds = get_disk_structure($base); - + my $out = backquote_command("$cmd 2>&1"); + return ( $? ? $out : undef ); + } + return undef; + } + + # MBR: use glabel for slice-level or partition-level labels + my $device = $part ? $part->{'device'} : $slice->{'device'}; + if ( $device && has_command('glabel') ) { + + # Remove existing glabel if present, then add new one + my $existing = backquote_command( + "glabel status 2>/dev/null | grep " . quote_path($device) ); + if ( $existing =~ /^(\S+)\s+/ ) { + my $old_label = $1; + my $destroy_out = backquote_command( + "glabel destroy " . quote_path($old_label) . " 2>&1" ); + } + my $cmd = + "glabel label " . quote_path($label) . " " . quote_path($device); + my $out = backquote_command("$cmd 2>&1"); + return ( $? ? $out : undef ); + } + return undef; +} + +sub remove_partition_label { + my (%args) = @_; + my $disk = $args{'disk'}; + my $slice = $args{'slice'}; + my $part = $args{'part'}; + my $base = $disk->{'device'}; + $base =~ s{^/dev/}{}; + my $ds = get_disk_structure($base); + # GPT: remove label via gpart modify -l "" if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) { my $idx = $part ? undef : _safe_uint( $slice->{'number'} ); @@ -1117,162 +1117,166 @@ sub remove_partition_label { my $out = backquote_command("$cmd 2>&1"); return ( $? ? $out : undef ); } - return undef; - } - - # MBR: remove glabel - my $device = $part ? $part->{'device'} : $slice->{'device'}; - if ( $device && has_command('glabel') ) { - my $existing = backquote_command( - "glabel status 2>/dev/null | grep " . quote_path($device) ); + return undef; + } + + # MBR: remove glabel + my $device = $part ? $part->{'device'} : $slice->{'device'}; + if ( $device && has_command('glabel') ) { + my $existing = backquote_command( + "glabel status 2>/dev/null | grep " . quote_path($device) ); if ( $existing =~ /^(\S+)\s+/ ) { my $label = $1; my $cmd = "glabel destroy " . quote_path($label); my $out = backquote_command("$cmd 2>&1"); return ( $? ? $out : undef ); } - } - return undef; -} - -# Get the current label for a slice/partition device (GPT label or glabel) -sub get_device_label_name { - my (%args) = @_; - my $disk = $args{'disk'}; - my $slice = $args{'slice'}; - my $device = $args{'device'} || ( $slice ? $slice->{'device'} : undef ); - return undef unless $device; - - my $label; - - # Prefer GPT labels when applicable - my $base = $disk ? $disk->{'device'} : base_disk_device($device); - if ($base) { - $base =~ s{^/dev/}{}; - my $ds = $args{'disk_structure'} || get_disk_structure($base); - if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) { - my $idx = $slice ? slice_number($slice) : undef; - if ( $idx && $ds->{'partitions'} && $ds->{'partitions'}->{$idx} ) { - my $pl = $ds->{'partitions'}->{$idx}->{'label'}; - $label = $pl if ( $pl && $pl ne '(null)' ); - } - if ( !$label && $idx && $ds->{'entries'} ) { - foreach my $e ( @{ $ds->{'entries'} } ) { - next unless ( $e->{'type'} && $e->{'type'} eq 'partition' ); - next - unless ( defined $e->{'index'} && $e->{'index'} == $idx ); - if ( $e->{'label'} && $e->{'label'} ne '(null)' ) { - $label = $e->{'label'}; - last; - } - } - } - } - } - - # Fallback: GEOM label (glabel) - if ( !$label && has_command('glabel') ) { - my $glabel_out = backquote_command("glabel status 2>/dev/null"); - foreach my $line ( split( /\n/, $glabel_out ) ) { - if ( $line =~ /^\s*(\S+)\s+\S+\s+(\S+)/ ) { - my ( $lab, $prov ) = ( $1, $2 ); - $prov = "/dev/$prov" if ( $prov !~ m{^/dev/} ); - if ( $prov eq $device ) { - $label = $lab; - last; - } - } - } - } - return $label; -} - -sub preferred_device_path { - my ($device) = @_; - return $device unless $device; - - # Check for GPT label first - if ( -e "/dev/gpt" ) { - my $gpt_label = - backquote_command( "gpart list 2>/dev/null | grep -A 10 " - . quote_path($device) - . " | grep 'label:' | head -1" ); - if ( $gpt_label =~ /label:\s*(\S+)/ && $1 ne '(null)' ) { - my $label_path = "/dev/gpt/$1"; - return $label_path if ( -e $label_path ); - } - } - - # Check for glabel label - if ( has_command('glabel') ) { - my $glabel_out = backquote_command("glabel status 2>/dev/null"); - foreach my $line ( split( /\n/, $glabel_out ) ) { - if ( $line =~ /^(\S+)\s+\S+\s+(.+)$/ ) { - my ( $label, $provider ) = ( $1, $2 ); - $provider =~ s/^\s+|\s+$//g; - if ( $provider eq $device || "/dev/$provider" eq $device ) { - my $label_path = "/dev/label/$label"; - return $label_path if ( -e $label_path ); - } - } - } - } - - # Check for UFS label - my $ufs_label = - backquote_command( "tunefs -p " - . quote_path($device) - . " 2>/dev/null | grep 'volume label'" ); - if ( $ufs_label =~ /volume label.*\[([^\]]+)\]/ && $1 ne '' ) { - my $label_path = "/dev/ufs/$1"; - return $label_path if ( -e $label_path ); - } - - # Default: return original device - return $device; -} - -sub detect_filesystem_type { - my ( $device, $hint ) = @_; - my $t; - if ( has_command('fstyp') ) { - $t = - backquote_command( "fstyp " . quote_path($device) . " 2>/dev/null" ); - $t =~ s/[\r\n]+$//; - } - $t ||= $hint || ''; - $t = lc($t); - - # Normalize common variants - if ( $t =~ /ufs/ ) { return 'ufs'; } - if ( $t =~ /msdos|fat/ ) { return 'msdosfs'; } - if ( $t =~ /ext[234]|ext2fs/ ) { return 'ext2fs'; } - if ( $t =~ /zfs/ ) { return 'zfs'; } - if ( $t =~ /swap/ ) { return 'swap'; } - if ( $t =~ /^(ufs|ffs)$/ ) { return 'ufs'; } - if ( $t =~ /^(msdos|msdosfs|fat|fat32)$/ ) { return 'msdosfs'; } - if ( $t =~ /^(ext2|ext2fs)$/ ) { return 'ext2fs'; } - if ( $t =~ /^zfs$/ ) { return 'zfs'; } - if ( $t =~ /^swap/ ) { return 'swap'; } - return $t || undef; -} - -# Apply recommended ACL inherit flags for NFSv4 datasets (best-effort) -sub acl_inherit_flags_cmd { - my ($dataset) = @_; - return undef if ( !$dataset ); - my $ds = quote_path($dataset); - my $cmd = - 'acltype=$(zfs get -H -o value acltype ' - . $ds - . ' 2>/dev/null); ' - . 'mp=$(zfs get -H -o value mountpoint ' - . $ds - . ' 2>/dev/null); ' - . 'if [ "$acltype" = "nfsv4" ] && [ -n "$mp" ] && ' - . '[ "$mp" != "-" ] && [ "$mp" != "none" ]; then '; - if ( $^O eq 'freebsd' ) { - $cmd .= + } + return undef; +} + +# Get the current label for a slice/partition device (GPT label or glabel) +sub get_device_label_name { + my (%args) = @_; + my $disk = $args{'disk'}; + my $slice = $args{'slice'}; + my $device = $args{'device'} || ( $slice ? $slice->{'device'} : undef ); + return undef unless $device; + + my $label; + + # Prefer GPT labels when applicable + my $base = $disk ? $disk->{'device'} : base_disk_device($device); + if ($base) { + $base =~ s{^/dev/}{}; + my $ds = $args{'disk_structure'} || get_disk_structure($base); + if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) { + my $idx = $slice ? slice_number($slice) : undef; + if ( $idx && $ds->{'partitions'} && $ds->{'partitions'}->{$idx} ) { + my $pl = $ds->{'partitions'}->{$idx}->{'label'}; + $label = $pl if ( $pl && $pl ne '(null)' ); + } + if ( !$label && $idx && $ds->{'entries'} ) { + foreach my $e ( @{ $ds->{'entries'} } ) { + next unless ( $e->{'type'} && $e->{'type'} eq 'partition' ); + next + unless ( defined $e->{'index'} && $e->{'index'} == $idx ); + if ( $e->{'label'} && $e->{'label'} ne '(null)' ) { + $label = $e->{'label'}; + last; + } + } + } + } + } + + # Fallback: GEOM label (glabel) + if ( !$label && has_command('glabel') ) { + my $glabel_out = backquote_command("glabel status 2>/dev/null"); + foreach my $line ( split( /\n/, $glabel_out ) ) { + if ( $line =~ /^\s*(\S+)\s+\S+\s+(\S+)/ ) { + my ( $lab, $prov ) = ( $1, $2 ); + $prov = "/dev/$prov" if ( $prov !~ m{^/dev/} ); + if ( $prov eq $device ) { + $label = $lab; + last; + } + } + } + } + return $label; +} + +sub preferred_device_path { + my ($device) = @_; + return $device unless $device; + + # Check for GPT label first + if ( -e "/dev/gpt" ) { + my $gpt_label = + backquote_command( "gpart list 2>/dev/null | grep -A 10 " + . quote_path($device) + . " | grep 'label:' | head -1" ); + if ( $gpt_label =~ /label:\s*(\S+)/ && $1 ne '(null)' ) { + my $label_path = "/dev/gpt/$1"; + return $label_path if ( -e $label_path ); + } + } + + # Check for glabel label + if ( has_command('glabel') ) { + my $glabel_out = backquote_command("glabel status 2>/dev/null"); + foreach my $line ( split( /\n/, $glabel_out ) ) { + if ( $line =~ /^(\S+)\s+\S+\s+(.+)$/ ) { + my ( $label, $provider ) = ( $1, $2 ); + $provider =~ s/^\s+|\s+$//g; + if ( $provider eq $device || "/dev/$provider" eq $device ) { + my $label_path = "/dev/label/$label"; + return $label_path if ( -e $label_path ); + } + } + } + } + + # Check for UFS label + my $ufs_label = + backquote_command( "tunefs -p " + . quote_path($device) + . " 2>/dev/null | grep 'volume label'" ); + if ( $ufs_label =~ /volume label.*\[([^\]]+)\]/ && $1 ne '' ) { + my $label_path = "/dev/ufs/$1"; + return $label_path if ( -e $label_path ); + } + + # Default: return original device + return $device; +} + +sub detect_filesystem_type { + my ( $device, $hint ) = @_; + my $t; + if ( has_command('fstyp') ) { + $t = + backquote_command( "fstyp " . quote_path($device) . " 2>/dev/null" ); + $t =~ s/[\r\n]+$//; + } + $t ||= $hint || ''; + $t = lc($t); + $t =~ s/^\s+|\s+$//g; + + # Prefer exact matches first + return 'ufs' if ( $t =~ /^(ufs|ffs)$/ ); + return 'msdosfs' if ( $t =~ /^(msdos|msdosfs|fat|fat32)$/ ); + return 'ext2fs' if ( $t =~ /^(ext2|ext2fs|ext3|ext4)$/ ); + return 'zfs' if ( $t eq 'zfs' ); + return 'swap' if ( $t eq 'swap' ); + + # Then allow bounded substring matches to avoid false positives + return 'ufs' if ( $t =~ /\b(ufs|ffs)\b/ ); + return 'msdosfs' if ( $t =~ /\b(msdos|msdosfs|fat|fat32|vfat|exfat)\b/ ); + return 'ext2fs' if ( $t =~ /\b(ext2|ext3|ext4|ext2fs)\b/ ); + return 'zfs' if ( $t =~ /\bzfs\b/ ); + return 'swap' if ( $t =~ /\bswap\b/ ); + + return $t || undef; +} + +# Apply recommended ACL inherit flags for NFSv4 datasets (best-effort) +sub acl_inherit_flags_cmd { + my ($dataset) = @_; + return undef if ( !$dataset ); + my $ds = quote_path($dataset); + my $cmd = + 'acltype=$(zfs get -H -o value acltype ' + . $ds + . ' 2>/dev/null); ' + . 'mp=$(zfs get -H -o value mountpoint ' + . $ds + . ' 2>/dev/null); ' + . 'if [ "$acltype" = "nfsv4" ] && [ -n "$mp" ] && ' + . '[ "$mp" != "-" ] && [ "$mp" != "none" ]; then '; + if ( $^O eq 'freebsd' ) { + $cmd .= 'if command -v getfacl >/dev/null 2>&1 && ' . 'command -v setfacl >/dev/null 2>&1; then ' . 'po=$(getfacl "$mp" 2>/dev/null | awk -F: ' @@ -1281,15 +1285,15 @@ sub acl_inherit_flags_cmd { . '"/^[[:space:]]*group@/{print \\$2; exit}"); ' . 'pe=$(getfacl "$mp" 2>/dev/null | awk -F: ' . '"/^[[:space:]]*everyone@/{print \\$2; exit}"); ' - . 'if [ -n "$po" ] && [ -n "$pg" ] && [ -n "$pe" ]; then ' + . 'if [ -n "$po" ] && [ -n "$pg" ] && [ -n "$pe" ]; then ' . 'setfacl -m "owner@:$po:fd-----:allow" ' . '-m "group@:$pg:fd-----:allow" ' . '-m "everyone@:$pe:fd-----:allow" "$mp"; ' - . 'fi; ' - . 'fi; '; - } - else { - $cmd .= + . 'fi; ' + . 'fi; '; + } + else { + $cmd .= 'if command -v nfs4_getfacl >/dev/null 2>&1 && ' . 'command -v nfs4_setfacl >/dev/null 2>&1; then ' . 'po=$(nfs4_getfacl "$mp" 2>/dev/null | awk -F: ' @@ -1298,915 +1302,915 @@ sub acl_inherit_flags_cmd { . '"/GROUP@/{print \\$4; exit}"); ' . 'pe=$(nfs4_getfacl "$mp" 2>/dev/null | awk -F: ' . '"/EVERYONE@/{print \\$4; exit}"); ' - . 'if [ -n "$po" ] && [ -n "$pg" ] && [ -n "$pe" ]; then ' + . 'if [ -n "$po" ] && [ -n "$pg" ] && [ -n "$pe" ]; then ' . 'nfs4_setfacl -a "A::OWNER@:$po:fd-----:allow" ' . '-a "A::GROUP@:$pg:fd-----:allow" ' . '-a "A::EVERYONE@:$pe:fd-----:allow" "$mp"; ' - . 'fi; ' - . 'fi; '; - } - $cmd .= "fi"; - return $cmd; -} - -sub get_zfs_device_info { - my ($object) = @_; - return undef unless ( $object && $object->{'device'} ); - my ( $pools, $zfs_devices ) = build_zfs_devices_cache(); - my $device = $object->{'device'}; - my @ids = ($device); - ( my $short = $device ) =~ s{^/dev/}{}; - push( @ids, $short, lc($device), lc($short) ); - - # If this is a top-level partition, include GPT label aliases if present - if ( $device =~ m{^/dev/([a-z]+[0-9]+)([ps])(\d+)$}i ) { - my ( $base, $sep, $num ) = ( $1, $2, $3 ); - my $ds = get_disk_structure($base); - my $part_label; - if ( $ds && $ds->{'partitions'} && $ds->{'partitions'}->{$num} ) { - my $pl = $ds->{'partitions'}->{$num}->{'label'}; - $part_label = $pl if ( $pl && $pl ne '(null)' ); - } - if ( !$part_label && $ds && $ds->{'entries'} ) { - foreach my $e ( @{ $ds->{'entries'} } ) { - next unless ( $e->{'type'} && $e->{'type'} eq 'partition' ); - next unless ( defined $e->{'index'} && $e->{'index'} == $num ); - if ( $e->{'label'} && $e->{'label'} ne '(null)' ) { - $part_label = $e->{'label'}; - last; - } - } - } - my $part_name = $base . $sep . $num; - my $scheme = ( $sep eq 'p' ) ? 'GPT' : ''; - push( - @ids, - _possible_partition_ids( - $base, $scheme, $num, $part_name, $part_label - ) - ); - } - return _find_in_zfs( $zfs_devices, @ids ); -} - -sub is_zfs_device { - my ($object) = @_; - return 0 unless ( $object && $object->{'device'} ); - - # Trust explicit type hints first - if ( defined $object->{'type'} && $object->{'type'} =~ /zfs/i ) { - return 1; - } - return get_zfs_device_info($object) ? 1 : 0; -} - -sub get_check_filesystem_command { - my ( $disk, $slice, $part ) = @_; - my $device = $part ? $part->{'device'} : $slice->{'device'}; - my $hint = $part ? $part->{'type'} : $slice->{'type'}; - my $fstype = detect_filesystem_type( $device, $hint ); - my $qdev = quote_path($device); - - # Map to specific fsck tools when available; else use fsck -t - if ( $fstype && $fstype eq 'ufs' ) { - return has_command('fsck_ufs') - ? "fsck_ufs -y $qdev" - : "fsck -t ufs -y $qdev"; - } - if ( $fstype && $fstype eq 'msdosfs' ) { - return has_command('fsck_msdosfs') - ? "fsck_msdosfs -y $qdev" - : "fsck -t msdosfs -y $qdev"; - } - if ( $fstype && $fstype eq 'ext2fs' ) { - return has_command('fsck_ext2fs') - ? "fsck_ext2fs -y $qdev" - : "fsck -t ext2fs -y $qdev"; - } - if ( $fstype && $fstype eq 'zfs' ) { - return - "zpool status 2>&1" - ; # caller should avoid fsck for ZFS, but safe fallback - } - if ( $fstype && $fstype eq 'swap' ) { - return "echo 'swap device - fsck not applicable'"; - } - - # Generic fallback - return "fsck -y $qdev"; -} - -sub show_filesystem_buttons { - my ( $hiddens, $st, $object, $return_url ) = @_; - if ( $return_url && $return_url !~ m{^/} ) { - $return_url = "/$module_name/$return_url"; - } - - # Use preferred device path (label-based if available) - my $preferred_dev = preferred_device_path( $object->{'device'} ); - print ui_buttons_row( "newfs_form.cgi", $text{'part_newfs'}, - $text{'part_newfsdesc'}, $hiddens ); - - # Offer fsck only when a filesystem is detectable and supported - my $is_swap = ( @$st && $st->[1] eq 'swap' ) - || ( $object->{'type'} && $object->{'type'} =~ /freebsd-swap|^82$/i ); - my $is_zfs = is_zfs_device($object); - my $fsck_type; - if ( has_command('fstyp') ) { - $fsck_type = detect_filesystem_type( $object->{'device'}, undef ); - if ( !$fsck_type ) { - $fsck_type = - detect_filesystem_type( $object->{'device'}, $object->{'type'} ); - } - } - else { - $fsck_type = - detect_filesystem_type( $object->{'device'}, $object->{'type'} ); - } - if ( $fsck_type - && $fsck_type ne 'swap' - && $fsck_type ne 'zfs' - && !$is_swap - && !$is_zfs ) - { - print ui_buttons_row( "fsck.cgi", $text{'part_fsck'}, - $text{'part_fsckdesc'}, $hiddens ); - } - if ( !@$st ) { - if ( $object->{'type'} eq 'swap' - or $object->{'type'} eq '82' - or $object->{'type'} eq 'freebsd-swap' ) - { - my $mount_h = ui_hidden( "newdev", $preferred_dev ) - . ui_hidden( "type", "swap" ); - $mount_h .= ui_hidden( "return", $return_url ) if ($return_url); - print ui_buttons_row( "../mount/edit_mount.cgi", - $text{'part_newmount2'}, $text{'part_mountmsg2'}, $mount_h ); - } - else { - my $fstype = - detect_filesystem_type( $object->{'device'}, $object->{'type'} ); - my $can_mount = - ( $fstype && $fstype ne 'zfs' && $fstype ne 'swap' ) ? 1 : 0; - if ($can_mount) { - my $mount_h = ui_hidden( "newdev", $preferred_dev ) - . ui_hidden( "type", $fstype ); - $mount_h .= ui_hidden( "return", $return_url ) if ($return_url); - print ui_buttons_row( "../mount/edit_mount.cgi", - $text{'part_newmount'}, $text{'part_mountmsg'}, $mount_h ); - } - } - } -} - -#--------------------------------------------------------------------- -# ZFS and GEOM related functions are largely unchanged. -sub get_all_zfs_info { - - # Wrapper built from the structured ZFS devices cache - my ( $pools, $devices ) = build_zfs_devices_cache(); - my %zfs_info; - foreach my $id ( keys %$devices ) { - next unless $id =~ /^\/dev\//; # focus on canonical /dev/* keys - my $dev = $devices->{$id}; - my $suffix = - ( $dev->{'vdev_type'} && $dev->{'vdev_type'} eq 'log' ) - ? '(log)' - : '(data)'; - $zfs_info{$id} = $dev->{'pool'} . ' ' . $suffix; - } - return \%zfs_info; -} - -sub get_type_description { - my ($type) = @_; - my %type_map = ( - 'freebsd' => 'FreeBSD', - 'freebsd-ufs' => 'FreeBSD UFS', - 'freebsd-swap' => 'FreeBSD Swap', - 'freebsd-vinum' => 'FreeBSD Vinum', - 'freebsd-zfs' => 'FreeBSD ZFS', - 'freebsd-boot' => 'FreeBSD Boot', - 'efi' => 'EFI System', - 'bios-boot' => 'BIOS Boot', - 'ms-basic-data' => 'Microsoft Basic Data (FAT/NTFS/exFAT)', - 'ms-reserved' => 'Microsoft Reserved', - 'ms-recovery' => 'Microsoft Recovery', - 'apple-ufs' => 'Apple UFS', - 'apple-hfs' => 'Apple HFS', - 'apple-boot' => 'Apple Boot', - 'apple-raid' => 'Apple RAID', - 'apple-label' => 'Apple Label', - 'linux-data' => 'Linux Data (ext2/3/4, xfs, btrfs, etc.)', - 'linux-swap' => 'Linux Swap', - 'linux-lvm' => 'Linux LVM', - 'linux-raid' => 'Linux RAID', - ); - return $type_map{$type} || $type; -} - -sub get_disk_structure { - my ($device) = @_; - my $result = { 'entries' => [], 'partitions' => {} }; - my $cmd = "gpart show -l " . quote_path($device) . " 2>&1"; - my $out = backquote_command($cmd); - if ( $out =~ /=>\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+\(([^)]+)\)/ ) { - my $start_block = $1; # starting block - my $size_blocks = $2; # number of blocks - $result->{'total_blocks'} = - $start_block + $size_blocks; # last addressable block + 1 - $result->{'device_name'} = $3; # device name (e.g., da0) - $result->{'scheme'} = $4; # GPT/MBR - $result->{'size_human'} = $5; # human size from header - } - foreach my $line ( split( /\n/, $out ) ) { - - # Free space rows - if ( $line =~ /^\s+(\d+)\s+(\d+)\s+-\s+free\s+-\s+\(([^)]+)\)/ ) { - push @{ $result->{'entries'} }, - { - 'start' => $1, - 'size' => $2, - 'size_human' => $3, - 'type' => 'free' - }; - next; - } - + . 'fi; ' + . 'fi; '; + } + $cmd .= "fi"; + return $cmd; +} + +sub get_zfs_device_info { + my ($object) = @_; + return undef unless ( $object && $object->{'device'} ); + my ( $pools, $zfs_devices ) = build_zfs_devices_cache(); + my $device = $object->{'device'}; + my @ids = ($device); + ( my $short = $device ) =~ s{^/dev/}{}; + push( @ids, $short, lc($device), lc($short) ); + + # If this is a top-level partition, include GPT label aliases if present + if ( $device =~ m{^/dev/([a-z]+[0-9]+)([ps])(\d+)$}i ) { + my ( $base, $sep, $num ) = ( $1, $2, $3 ); + my $ds = get_disk_structure($base); + my $part_label; + if ( $ds && $ds->{'partitions'} && $ds->{'partitions'}->{$num} ) { + my $pl = $ds->{'partitions'}->{$num}->{'label'}; + $part_label = $pl if ( $pl && $pl ne '(null)' ); + } + if ( !$part_label && $ds && $ds->{'entries'} ) { + foreach my $e ( @{ $ds->{'entries'} } ) { + next unless ( $e->{'type'} && $e->{'type'} eq 'partition' ); + next unless ( defined $e->{'index'} && $e->{'index'} == $num ); + if ( $e->{'label'} && $e->{'label'} ne '(null)' ) { + $part_label = $e->{'label'}; + last; + } + } + } + my $part_name = $base . $sep . $num; + my $scheme = ( $sep eq 'p' ) ? 'GPT' : ''; + push( + @ids, + _possible_partition_ids( + $base, $scheme, $num, $part_name, $part_label + ) + ); + } + return _find_in_zfs( $zfs_devices, @ids ); +} + +sub is_zfs_device { + my ($object) = @_; + return 0 unless ( $object && $object->{'device'} ); + + # Trust explicit type hints first + if ( defined $object->{'type'} && $object->{'type'} =~ /zfs/i ) { + return 1; + } + return get_zfs_device_info($object) ? 1 : 0; +} + +sub get_check_filesystem_command { + my ( $disk, $slice, $part ) = @_; + my $device = $part ? $part->{'device'} : $slice->{'device'}; + my $hint = $part ? $part->{'type'} : $slice->{'type'}; + my $fstype = detect_filesystem_type( $device, $hint ); + my $qdev = quote_path($device); + + # Map to specific fsck tools when available; else use fsck -t + if ( $fstype && $fstype eq 'ufs' ) { + return has_command('fsck_ufs') + ? "fsck_ufs -y $qdev" + : "fsck -t ufs -y $qdev"; + } + if ( $fstype && $fstype eq 'msdosfs' ) { + return has_command('fsck_msdosfs') + ? "fsck_msdosfs -y $qdev" + : "fsck -t msdosfs -y $qdev"; + } + if ( $fstype && $fstype eq 'ext2fs' ) { + return has_command('fsck_ext2fs') + ? "fsck_ext2fs -y $qdev" + : "fsck -t ext2fs -y $qdev"; + } + if ( $fstype && $fstype eq 'zfs' ) { + return + "zpool status 2>&1" + ; # caller should avoid fsck for ZFS, but safe fallback + } + if ( $fstype && $fstype eq 'swap' ) { + return "echo 'swap device - fsck not applicable'"; + } + + # Generic fallback + return "fsck -y $qdev"; +} + +sub show_filesystem_buttons { + my ( $hiddens, $st, $object, $return_url ) = @_; + if ( $return_url && $return_url !~ m{^/} ) { + $return_url = "/$module_name/$return_url"; + } + + # Use preferred device path (label-based if available) + my $preferred_dev = preferred_device_path( $object->{'device'} ); + print ui_buttons_row( "newfs_form.cgi", $text{'part_newfs'}, + $text{'part_newfsdesc'}, $hiddens ); + + # Offer fsck only when a filesystem is detectable and supported + my $is_swap = ( @$st && $st->[1] eq 'swap' ) + || ( $object->{'type'} && $object->{'type'} =~ /freebsd-swap|^82$/i ); + my $is_zfs = is_zfs_device($object); + my $fsck_type; + if ( has_command('fstyp') ) { + $fsck_type = detect_filesystem_type( $object->{'device'}, undef ); + if ( !$fsck_type ) { + $fsck_type = + detect_filesystem_type( $object->{'device'}, $object->{'type'} ); + } + } + else { + $fsck_type = + detect_filesystem_type( $object->{'device'}, $object->{'type'} ); + } + if ( $fsck_type + && $fsck_type ne 'swap' + && $fsck_type ne 'zfs' + && !$is_swap + && !$is_zfs ) + { + print ui_buttons_row( "fsck.cgi", $text{'part_fsck'}, + $text{'part_fsckdesc'}, $hiddens ); + } + if ( !@$st ) { + if ( $object->{'type'} eq 'swap' + or $object->{'type'} eq '82' + or $object->{'type'} eq 'freebsd-swap' ) + { + my $mount_h = ui_hidden( "newdev", $preferred_dev ) + . ui_hidden( "type", "swap" ); + $mount_h .= ui_hidden( "return", $return_url ) if ($return_url); + print ui_buttons_row( "../mount/edit_mount.cgi", + $text{'part_newmount2'}, $text{'part_mountmsg2'}, $mount_h ); + } + else { + my $fstype = + detect_filesystem_type( $object->{'device'}, $object->{'type'} ); + my $can_mount = + ( $fstype && $fstype ne 'zfs' && $fstype ne 'swap' ) ? 1 : 0; + if ($can_mount) { + my $mount_h = ui_hidden( "newdev", $preferred_dev ) + . ui_hidden( "type", $fstype ); + $mount_h .= ui_hidden( "return", $return_url ) if ($return_url); + print ui_buttons_row( "../mount/edit_mount.cgi", + $text{'part_newmount'}, $text{'part_mountmsg'}, $mount_h ); + } + } + } +} + +#--------------------------------------------------------------------- +# ZFS and GEOM related functions are largely unchanged. +sub get_all_zfs_info { + + # Wrapper built from the structured ZFS devices cache + my ( $pools, $devices ) = build_zfs_devices_cache(); + my %zfs_info; + foreach my $id ( keys %$devices ) { + next unless $id =~ /^\/dev\//; # focus on canonical /dev/* keys + my $dev = $devices->{$id}; + my $suffix = + ( $dev->{'vdev_type'} && $dev->{'vdev_type'} eq 'log' ) + ? '(log)' + : '(data)'; + $zfs_info{$id} = $dev->{'pool'} . ' ' . $suffix; + } + return \%zfs_info; +} + +sub get_type_description { + my ($type) = @_; + my %type_map = ( + 'freebsd' => 'FreeBSD', + 'freebsd-ufs' => 'FreeBSD UFS', + 'freebsd-swap' => 'FreeBSD Swap', + 'freebsd-vinum' => 'FreeBSD Vinum', + 'freebsd-zfs' => 'FreeBSD ZFS', + 'freebsd-boot' => 'FreeBSD Boot', + 'efi' => 'EFI System', + 'bios-boot' => 'BIOS Boot', + 'ms-basic-data' => 'Microsoft Basic Data (FAT/NTFS/exFAT)', + 'ms-reserved' => 'Microsoft Reserved', + 'ms-recovery' => 'Microsoft Recovery', + 'apple-ufs' => 'Apple UFS', + 'apple-hfs' => 'Apple HFS', + 'apple-boot' => 'Apple Boot', + 'apple-raid' => 'Apple RAID', + 'apple-label' => 'Apple Label', + 'linux-data' => 'Linux Data (ext2/3/4, xfs, btrfs, etc.)', + 'linux-swap' => 'Linux Swap', + 'linux-lvm' => 'Linux LVM', + 'linux-raid' => 'Linux RAID', + ); + return $type_map{$type} || $type; +} + +sub get_disk_structure { + my ($device) = @_; + my $result = { 'entries' => [], 'partitions' => {} }; + my $cmd = "gpart show -l " . quote_path($device) . " 2>&1"; + my $out = backquote_command($cmd); + if ( $out =~ /=>\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+\(([^)]+)\)/ ) { + my $start_block = $1; # starting block + my $size_blocks = $2; # number of blocks + $result->{'total_blocks'} = + $start_block + $size_blocks; # last addressable block + 1 + $result->{'device_name'} = $3; # device name (e.g., da0) + $result->{'scheme'} = $4; # GPT/MBR + $result->{'size_human'} = $5; # human size from header + } + foreach my $line ( split( /\n/, $out ) ) { + + # Free space rows + if ( $line =~ /^\s+(\d+)\s+(\d+)\s+-\s+free\s+-\s+\(([^)]+)\)/ ) { + push @{ $result->{'entries'} }, + { + 'start' => $1, + 'size' => $2, + 'size_human' => $3, + 'type' => 'free' + }; + next; + } + # Partition rows from `gpart show -l` have: start size index label # [flags] (size_human) # Some systems include optional tokens like "[active]" after the label. # Accept them. - if ( $line =~ - /^\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)(?:\s+\[[^\]]+\])?\s+\(([^)]+)\)/ - ) - { - push @{ $result->{'entries'} }, - { - 'start' => $1, - 'size' => $2, - 'index' => $3, - 'label' => $4, - 'size_human' => $5, - 'type' => 'partition' - }; - } - } - - # Merge additional info from 'gpart list' directly, keyed by Name -> index - my $list_out = - backquote_command( "gpart list " . quote_path($device) . " 2>&1" ); - my ( %parts, $current_idx ); - foreach my $line ( split( /\n/, $list_out ) ) { - if ( $line =~ /^\s*(?:\d+\.\s*)?Name:\s*(\S+)/i ) { - my $name = $1; # e.g., da0p2 or da0s2 - if ( $name =~ /[ps](\d+)$/ ) { - $current_idx = int($1); - $parts{$current_idx} ||= { name => $name }; - } - else { - undef $current_idx; # not a partition provider line - } - } - elsif ( defined $current_idx && $line =~ /^\s*Index:\s*(\d+)/i ) { - - # Optional cross-check; ignore value and trust Name-derived index - next; - } - elsif ( defined $current_idx && $line =~ /^\s*label:\s*(\S+)/i ) { - $parts{$current_idx}->{'label'} = $1; - } - elsif ( defined $current_idx && $line =~ /^\s*type:\s*(\S+)/i ) { - $parts{$current_idx}->{'type'} = $1; - } - elsif ( defined $current_idx && $line =~ /^\s*rawtype:\s*(\S+)/i ) { - $parts{$current_idx}->{'rawtype'} = $1; - } - elsif ( defined $current_idx && $line =~ /^\s*length:\s*(\d+)/i ) { - $parts{$current_idx}->{'length'} = $1; - } - elsif ( defined $current_idx && $line =~ /^\s*offset:\s*(\d+)/i ) { - $parts{$current_idx}->{'offset'} = $1; - } - elsif ( $line =~ /Sectorsize:\s*(\d+)/i ) { - $result->{'sectorsize'} = int($1); - } - elsif ( $line =~ /Mediasize:\s*(\d+)/i ) { - $result->{'mediasize'} = int($1); - } - } - $result->{'partitions'} = \%parts; - foreach my $entry ( @{ $result->{'entries'} } ) { - next unless ( $entry->{'type'} eq 'partition' && $entry->{'index'} ); - my $idx = $entry->{'index'}; - if ( $parts{$idx} ) { - - # Prefer label from gpart list if present and meaningful - if ( $parts{$idx}->{'label'} - && $parts{$idx}->{'label'} ne '(null)' ) - { - $entry->{'label'} = $parts{$idx}->{'label'}; - } - - # Attach resolved type (rawtype/type) for downstream consumers - $entry->{'part_type'} = - $parts{$idx}->{'type'} - || $parts{$idx}->{'rawtype'} - || $entry->{'part_type'}; - - # Also store rawtype for downstream consumers - $entry->{'rawtype'} = $parts{$idx}->{'rawtype'} - if ( $parts{$idx}->{'rawtype'} ); - } - } - return $result; -} - -sub get_disk_sectorsize { - my ($device) = @_; - - # Normalize device for diskinfo (expects provider name like da0) - my $dev = $device; - $dev =~ s{^/dev/}{}; - - # Prefer verbose output which explicitly lists sectorsize - my $outv = - backquote_command( "diskinfo -v " . quote_path($dev) . " 2>/dev/null" ); - if ( $outv =~ /sectorsize:\s*(\d+)/i ) { - return int($1); - } - - # Fallback to non-verbose; actual format: name sectorsize mediasize ... - my $out = - backquote_command( "diskinfo " . quote_path($dev) . " 2>/dev/null" ); - if ( $out =~ /^\S+\s+(\d+)\s+\d+/ ) { - return int($1); # second field is sectorsize - } - - # Last resort: ask gpart list for sectorsize - my $base = $dev; - my $ds = get_disk_structure($base); - if ( $ds && $ds->{'sectorsize'} ) { return int( $ds->{'sectorsize'} ); } - return undef; -} - -# Derive the base disk device (e.g., /dev/da0 from /dev/da0p2) -sub base_disk_device { - my ($device) = @_; - return undef unless $device; - my $d = $device; - $d =~ s{^/dev/}{}; - $d =~ s{(p|s)\d+.*$}{}; # strip partition/slice suffix - return "/dev/$d"; -} - -# Compute bytes from a block count for a given device -sub bytes_from_blocks { - my ( $device, $blocks ) = @_; - return undef unless defined $blocks; - my $base = base_disk_device($device) || $device; - my $ss = get_disk_sectorsize($base) || 512; - return $blocks * $ss; -} - -# Safe wrapper for nice_size that ensures bytes input -# Accepts either raw bytes or (device, blocks) pair -sub safe_nice_size { - my ( $arg1, $arg2 ) = @_; - my $bytes; - if ( defined $arg2 ) { - - # Called as (device, blocks) - $bytes = bytes_from_blocks( $arg1, $arg2 ); - } - else { - # Called as (bytes) - $bytes = $arg1; - } - return '-' unless defined $bytes && $bytes >= 0; - my $s = nice_size($bytes); - - # Normalize IEC suffixes to SI-style labels if present - $s =~ s/\bKiB\b/KB/g; - $s =~ s/\bMiB\b/MB/g; - $s =~ s/\bGiB\b/GB/g; - $s =~ s/\bTiB\b/TB/g; - $s =~ s/\bPiB\b/PB/g; - $s =~ s/\bEiB\b/EB/g; - return $s; -} - -sub build_zfs_devices_cache { - my %pools; - my %devices; - my $cmd = "zpool status 2>&1"; - my $out = backquote_command($cmd); - my ( - $current_pool, $in_config, $current_vdev_type, - $current_vdev_group, $is_mirrored, $is_raidz, - $raidz_level, $is_single, $is_striped, - $vdev_count - ); - $current_vdev_type = 'data'; - foreach my $line ( split( /\n/, $out ) ) { - if ( $line =~ /^\s*pool:\s+(\S+)/ ) { - $current_pool = $1; - $pools{$current_pool} = 1; - $in_config = 0; - $current_vdev_type = 'data'; - } - elsif ( $line =~ /^\s*config:/ ) { - $in_config = 1; - $current_vdev_group = undef; - $is_mirrored = 0; - $is_raidz = 0; - $raidz_level = 0; - $is_single = 0; - $is_striped = 0; - $vdev_count = 0; - } - elsif ( $in_config and $line =~ /^\s+logs/ ) { - $current_vdev_type = 'log'; - $current_vdev_group = undef; - } - elsif ( $in_config and $line =~ /^\s+cache/ ) { - $current_vdev_type = 'cache'; - $current_vdev_group = undef; - } - elsif ( $in_config and $line =~ /^\s+spares/ ) { - $current_vdev_type = 'spare'; - $current_vdev_group = undef; - } - elsif ( $in_config and $line =~ /^\s+mirror-(\d+)/ ) { - $current_vdev_group = "mirror-$1"; - $is_mirrored = 1; - $is_raidz = 0; - $is_single = 0; - $is_striped = 0; - $vdev_count = 0; - } - elsif ( $in_config and $line =~ /^\s+raidz(\d+)?-(\d+)/ ) { - $current_vdev_group = "raidz" . ( $1 || "1" ) . "-$2"; - $is_mirrored = 0; - $is_raidz = 1; - $raidz_level = $1 || 1; - $is_single = 0; - $is_striped = 0; - $vdev_count = 0; - } - elsif ( $in_config and $line =~ /^\s+(\S+)\s+(\S+)/ ) { - my $device = $1; - my $state = $2; - next - if ( $device eq $current_pool - or $device =~ /^mirror-/ - or $device =~ /^raidz\d*-/ ); - if ($current_vdev_group) { $vdev_count++; } - else { $is_single = 1; } - my $device_id = $device; - $device_id = $1 if ( $device =~ /^gpt\/(.*)/ ); - $devices{$device} = { - 'pool' => $current_pool, - 'vdev_type' => $current_vdev_type, - 'is_mirrored' => $is_mirrored, - 'is_raidz' => $is_raidz, - 'raidz_level' => $raidz_level, - 'is_single' => $is_single, - 'is_striped' => $is_striped, - 'vdev_group' => $current_vdev_group, - 'vdev_count' => $vdev_count - }; - $devices{"gpt/$device"} = $devices{$device} - if ( $device !~ /^gpt\// ); - $devices{"/dev/$device"} = $devices{$device}; - - if ( $device !~ /^gpt\// ) { - $devices{"/dev/gpt/$device"} = $devices{$device}; - } - $devices{ lc($device) } = $devices{$device}; - if ( $device !~ /^gpt\// ) { - $devices{ "gpt/" . lc($device) } = $devices{$device}; - $devices{ "/dev/gpt/" . lc($device) } = $devices{$device}; - } - } - } - return ( \%pools, \%devices ); -} - -sub get_format_type { - my ($part) = @_; - if ( $part->{'type'} =~ /^freebsd-/ ) { - return get_type_description( $part->{'type'} ); - } - return get_type_description( $part->{'type'} ) || $part->{'type'}; -} - -# Build possible ids for a partition given base device, scheme and metadata -sub _label_conflicts_with_device { - my ( $label, $base_device, $scheme, $part_num, $part_name ) = @_; - return 0 unless defined $label && $label ne '-' && $label ne '(null)'; - my $sep = ( $scheme && $scheme eq 'GPT' ) ? 'p' : 's'; - my $expected = - ( defined $base_device && defined $part_num && length($base_device) ) - ? "$base_device$sep$part_num" - : undef; - if ( defined $expected && lc($label) eq lc($expected) ) { - return 0; - } - if ( defined $part_name - && $part_name ne '-' - && lc($label) eq lc($part_name) ) - { - return 0; - } - -# If a label looks like a real disk partition but doesn't match this partition, + if ( $line =~ + /^\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)(?:\s+\[[^\]]+\])?\s+\(([^)]+)\)/ + ) + { + push @{ $result->{'entries'} }, + { + 'start' => $1, + 'size' => $2, + 'index' => $3, + 'label' => $4, + 'size_human' => $5, + 'type' => 'partition' + }; + } + } + + # Merge additional info from 'gpart list' directly, keyed by Name -> index + my $list_out = + backquote_command( "gpart list " . quote_path($device) . " 2>&1" ); + my ( %parts, $current_idx ); + foreach my $line ( split( /\n/, $list_out ) ) { + if ( $line =~ /^\s*(?:\d+\.\s*)?Name:\s*(\S+)/i ) { + my $name = $1; # e.g., da0p2 or da0s2 + if ( $name =~ /[ps](\d+)$/ ) { + $current_idx = int($1); + $parts{$current_idx} ||= { name => $name }; + } + else { + undef $current_idx; # not a partition provider line + } + } + elsif ( defined $current_idx && $line =~ /^\s*Index:\s*(\d+)/i ) { + + # Optional cross-check; ignore value and trust Name-derived index + next; + } + elsif ( defined $current_idx && $line =~ /^\s*label:\s*(\S+)/i ) { + $parts{$current_idx}->{'label'} = $1; + } + elsif ( defined $current_idx && $line =~ /^\s*type:\s*(\S+)/i ) { + $parts{$current_idx}->{'type'} = $1; + } + elsif ( defined $current_idx && $line =~ /^\s*rawtype:\s*(\S+)/i ) { + $parts{$current_idx}->{'rawtype'} = $1; + } + elsif ( defined $current_idx && $line =~ /^\s*length:\s*(\d+)/i ) { + $parts{$current_idx}->{'length'} = $1; + } + elsif ( defined $current_idx && $line =~ /^\s*offset:\s*(\d+)/i ) { + $parts{$current_idx}->{'offset'} = $1; + } + elsif ( $line =~ /Sectorsize:\s*(\d+)/i ) { + $result->{'sectorsize'} = int($1); + } + elsif ( $line =~ /Mediasize:\s*(\d+)/i ) { + $result->{'mediasize'} = int($1); + } + } + $result->{'partitions'} = \%parts; + foreach my $entry ( @{ $result->{'entries'} } ) { + next unless ( $entry->{'type'} eq 'partition' && $entry->{'index'} ); + my $idx = $entry->{'index'}; + if ( $parts{$idx} ) { + + # Prefer label from gpart list if present and meaningful + if ( $parts{$idx}->{'label'} + && $parts{$idx}->{'label'} ne '(null)' ) + { + $entry->{'label'} = $parts{$idx}->{'label'}; + } + + # Attach resolved type (rawtype/type) for downstream consumers + $entry->{'part_type'} = + $parts{$idx}->{'type'} + || $parts{$idx}->{'rawtype'} + || $entry->{'part_type'}; + + # Also store rawtype for downstream consumers + $entry->{'rawtype'} = $parts{$idx}->{'rawtype'} + if ( $parts{$idx}->{'rawtype'} ); + } + } + return $result; +} + +sub get_disk_sectorsize { + my ($device) = @_; + + # Normalize device for diskinfo (expects provider name like da0) + my $dev = $device; + $dev =~ s{^/dev/}{}; + + # Prefer verbose output which explicitly lists sectorsize + my $outv = + backquote_command( "diskinfo -v " . quote_path($dev) . " 2>/dev/null" ); + if ( $outv =~ /sectorsize:\s*(\d+)/i ) { + return int($1); + } + + # Fallback to non-verbose; actual format: name sectorsize mediasize ... + my $out = + backquote_command( "diskinfo " . quote_path($dev) . " 2>/dev/null" ); + if ( $out =~ /^\S+\s+(\d+)\s+\d+/ ) { + return int($1); # second field is sectorsize + } + + # Last resort: ask gpart list for sectorsize + my $base = $dev; + my $ds = get_disk_structure($base); + if ( $ds && $ds->{'sectorsize'} ) { return int( $ds->{'sectorsize'} ); } + return undef; +} + +# Derive the base disk device (e.g., /dev/da0 from /dev/da0p2) +sub base_disk_device { + my ($device) = @_; + return undef unless $device; + my $d = $device; + $d =~ s{^/dev/}{}; + $d =~ s{(p|s)\d+.*$}{}; # strip partition/slice suffix + return "/dev/$d"; +} + +# Compute bytes from a block count for a given device +sub bytes_from_blocks { + my ( $device, $blocks ) = @_; + return undef unless defined $blocks; + my $base = base_disk_device($device) || $device; + my $ss = get_disk_sectorsize($base) || 512; + return $blocks * $ss; +} + +# Safe wrapper for nice_size that ensures bytes input +# Accepts either raw bytes or (device, blocks) pair +sub safe_nice_size { + my ( $arg1, $arg2 ) = @_; + my $bytes; + if ( defined $arg2 ) { + + # Called as (device, blocks) + $bytes = bytes_from_blocks( $arg1, $arg2 ); + } + else { + # Called as (bytes) + $bytes = $arg1; + } + return '-' unless defined $bytes && $bytes >= 0; + my $s = nice_size($bytes); + + # Normalize IEC suffixes to SI-style labels if present + $s =~ s/\bKiB\b/KB/g; + $s =~ s/\bMiB\b/MB/g; + $s =~ s/\bGiB\b/GB/g; + $s =~ s/\bTiB\b/TB/g; + $s =~ s/\bPiB\b/PB/g; + $s =~ s/\bEiB\b/EB/g; + return $s; +} + +sub build_zfs_devices_cache { + my %pools; + my %devices; + my $cmd = "zpool status 2>&1"; + my $out = backquote_command($cmd); + my ( + $current_pool, $in_config, $current_vdev_type, + $current_vdev_group, $is_mirrored, $is_raidz, + $raidz_level, $is_single, $is_striped, + $vdev_count + ); + $current_vdev_type = 'data'; + foreach my $line ( split( /\n/, $out ) ) { + if ( $line =~ /^\s*pool:\s+(\S+)/ ) { + $current_pool = $1; + $pools{$current_pool} = 1; + $in_config = 0; + $current_vdev_type = 'data'; + } + elsif ( $line =~ /^\s*config:/ ) { + $in_config = 1; + $current_vdev_group = undef; + $is_mirrored = 0; + $is_raidz = 0; + $raidz_level = 0; + $is_single = 0; + $is_striped = 0; + $vdev_count = 0; + } + elsif ( $in_config and $line =~ /^\s+logs/ ) { + $current_vdev_type = 'log'; + $current_vdev_group = undef; + } + elsif ( $in_config and $line =~ /^\s+cache/ ) { + $current_vdev_type = 'cache'; + $current_vdev_group = undef; + } + elsif ( $in_config and $line =~ /^\s+spares/ ) { + $current_vdev_type = 'spare'; + $current_vdev_group = undef; + } + elsif ( $in_config and $line =~ /^\s+mirror-(\d+)/ ) { + $current_vdev_group = "mirror-$1"; + $is_mirrored = 1; + $is_raidz = 0; + $is_single = 0; + $is_striped = 0; + $vdev_count = 0; + } + elsif ( $in_config and $line =~ /^\s+raidz(\d+)?-(\d+)/ ) { + $current_vdev_group = "raidz" . ( $1 || "1" ) . "-$2"; + $is_mirrored = 0; + $is_raidz = 1; + $raidz_level = $1 || 1; + $is_single = 0; + $is_striped = 0; + $vdev_count = 0; + } + elsif ( $in_config and $line =~ /^\s+(\S+)\s+(\S+)/ ) { + my $device = $1; + my $state = $2; + next + if ( $device eq $current_pool + or $device =~ /^mirror-/ + or $device =~ /^raidz\d*-/ ); + if ($current_vdev_group) { $vdev_count++; } + else { $is_single = 1; } + my $device_id = $device; + $device_id = $1 if ( $device =~ /^gpt\/(.*)/ ); + $devices{$device} = { + 'pool' => $current_pool, + 'vdev_type' => $current_vdev_type, + 'is_mirrored' => $is_mirrored, + 'is_raidz' => $is_raidz, + 'raidz_level' => $raidz_level, + 'is_single' => $is_single, + 'is_striped' => $is_striped, + 'vdev_group' => $current_vdev_group, + 'vdev_count' => $vdev_count + }; + $devices{"gpt/$device"} = $devices{$device} + if ( $device !~ /^gpt\// ); + $devices{"/dev/$device"} = $devices{$device}; + + if ( $device !~ /^gpt\// ) { + $devices{"/dev/gpt/$device"} = $devices{$device}; + } + $devices{ lc($device) } = $devices{$device}; + if ( $device !~ /^gpt\// ) { + $devices{ "gpt/" . lc($device) } = $devices{$device}; + $devices{ "/dev/gpt/" . lc($device) } = $devices{$device}; + } + } + } + return ( \%pools, \%devices ); +} + +sub get_format_type { + my ($part) = @_; + if ( $part->{'type'} =~ /^freebsd-/ ) { + return get_type_description( $part->{'type'} ); + } + return get_type_description( $part->{'type'} ) || $part->{'type'}; +} + +# Build possible ids for a partition given base device, scheme and metadata +sub _label_conflicts_with_device { + my ( $label, $base_device, $scheme, $part_num, $part_name ) = @_; + return 0 unless defined $label && $label ne '-' && $label ne '(null)'; + my $sep = ( $scheme && $scheme eq 'GPT' ) ? 'p' : 's'; + my $expected = + ( defined $base_device && defined $part_num && length($base_device) ) + ? "$base_device$sep$part_num" + : undef; + if ( defined $expected && lc($label) eq lc($expected) ) { + return 0; + } + if ( defined $part_name + && $part_name ne '-' + && lc($label) eq lc($part_name) ) + { + return 0; + } + +# If a label looks like a real disk partition but doesn't match this partition, # Do not use it for ZFS membership detection (avoids cross-disk # misidentification). if ( $label =~ m{^(ada|ad|da|amrd|nvd|vtbd|nda)\d+(?:p|s)\d+$}i ) { return 1; } - return 0; -} - -sub _possible_partition_ids { - my ( $base_device, $scheme, $part_num, $part_name, $part_label ) = @_; - my @ids; - if ( defined $base_device && defined $part_num && length($base_device) ) { - my $sep = ( $scheme && $scheme eq 'GPT' ) ? 'p' : 's'; - my $device_path = "/dev/$base_device" . $sep . $part_num; - push( @ids, $device_path ); - ( my $short = $device_path ) =~ s/^\/dev\///; - push( @ids, $short ); - } - if ( $part_name && $part_name ne '-' ) { - push( @ids, $part_name, "/dev/$part_name" ); - } - if ( defined $part_label && $part_label ne '-' && $part_label ne '(null)' ) - { - my $label_conflict = - _label_conflicts_with_device( $part_label, $base_device, $scheme, - $part_num, $part_name ); - if ($label_conflict) { - - # Allow only GPT label aliases to avoid false matches to device-like labels - push( @ids, - "gpt/$part_label", "/dev/gpt/$part_label", - "gpt/" . lc($part_label), "/dev/gpt/" . lc($part_label) ); - } - else { - push( @ids, - $part_label, "gpt/$part_label", - "/dev/gpt/$part_label", lc($part_label), - "gpt/" . lc($part_label), "/dev/gpt/" . lc($part_label) ); - } - if ( $part_label =~ /^(sLOG\w+)$/ ) { - push( @ids, $1, "gpt/$1", "/dev/gpt/$1" ); - } - } - return @ids; -} - -# Given ids, find if present in ZFS devices cache -sub _find_in_zfs { - my ( $zfs_devices, @ids ) = @_; - foreach my $id (@ids) { - my $nid = lc($id); - if ( $zfs_devices->{$nid} ) { - return $zfs_devices->{$nid}; - } - } - return undef; -} - -# Classify a partition row: returns (format, usage, role) -sub classify_partition_row { - my (%args) = @_; - my $ids = [ - _possible_partition_ids( - @args{qw/base_device scheme part_num part_name part_label/} - ) - ]; - my $zdev = _find_in_zfs( $args{'zfs_devices'}, @$ids ); - - # Derive type description, avoid label-as-type - my $type_desc = $args{'entry_part_type'}; - if ( !defined $type_desc || $type_desc eq '-' || $type_desc eq 'unknown' ) { - - # leave undef - } - + return 0; +} + +sub _possible_partition_ids { + my ( $base_device, $scheme, $part_num, $part_name, $part_label ) = @_; + my @ids; + if ( defined $base_device && defined $part_num && length($base_device) ) { + my $sep = ( $scheme && $scheme eq 'GPT' ) ? 'p' : 's'; + my $device_path = "/dev/$base_device" . $sep . $part_num; + push( @ids, $device_path ); + ( my $short = $device_path ) =~ s/^\/dev\///; + push( @ids, $short ); + } + if ( $part_name && $part_name ne '-' ) { + push( @ids, $part_name, "/dev/$part_name" ); + } + if ( defined $part_label && $part_label ne '-' && $part_label ne '(null)' ) + { + my $label_conflict = + _label_conflicts_with_device( $part_label, $base_device, $scheme, + $part_num, $part_name ); + if ($label_conflict) { + + # Allow only GPT label aliases to avoid false matches to device-like labels + push( @ids, + "gpt/$part_label", "/dev/gpt/$part_label", + "gpt/" . lc($part_label), "/dev/gpt/" . lc($part_label) ); + } + else { + push( @ids, + $part_label, "gpt/$part_label", + "/dev/gpt/$part_label", lc($part_label), + "gpt/" . lc($part_label), "/dev/gpt/" . lc($part_label) ); + } + if ( $part_label =~ /^(sLOG\w+)$/ ) { + push( @ids, $1, "gpt/$1", "/dev/gpt/$1" ); + } + } + return @ids; +} + +# Given ids, find if present in ZFS devices cache +sub _find_in_zfs { + my ( $zfs_devices, @ids ) = @_; + foreach my $id (@ids) { + my $nid = lc($id); + if ( $zfs_devices->{$nid} ) { + return $zfs_devices->{$nid}; + } + } + return undef; +} + +# Classify a partition row: returns (format, usage, role) +sub classify_partition_row { + my (%args) = @_; + my $ids = [ + _possible_partition_ids( + @args{qw/base_device scheme part_num part_name part_label/} + ) + ]; + my $zdev = _find_in_zfs( $args{'zfs_devices'}, @$ids ); + + # Derive type description, avoid label-as-type + my $type_desc = $args{'entry_part_type'}; + if ( !defined $type_desc || $type_desc eq '-' || $type_desc eq 'unknown' ) { + + # leave undef + } + # Avoid clearing real types (like 'efi' or 'freebsd-boot') when label # text matches by case. # Only drop if the "type" clearly looks like a provider/label path that # mirrors the label. - if ( defined $type_desc && defined $args{'part_label'} ) { - my $pl = $args{'part_label'}; - if ( $type_desc =~ m{^(?:/dev/)?gpt(?:id)?/\Q$pl\E$}i ) { - undef $type_desc; - } - } - - my ( $format, $usage, $role ) = ( '-', $text{'part_nouse'}, '-' ); - + if ( defined $type_desc && defined $args{'part_label'} ) { + my $pl = $args{'part_label'}; + if ( $type_desc =~ m{^(?:/dev/)?gpt(?:id)?/\Q$pl\E$}i ) { + undef $type_desc; + } + } + + my ( $format, $usage, $role ) = ( '-', $text{'part_nouse'}, '-' ); + # Explicit boot detection based on GPT GUIDs and MBR hex codes or # human-readable type - my $raw = lc( $args{'entry_rawtype'} || '' ); - my $t = lc( $type_desc || '' ); - my %boot_guid = map { $_ => 1 } qw( - c12a7328-f81f-11d2-ba4b-00a0c93ec93b # EFI System - 21686148-6449-6e6f-744e-656564454649 # BIOS Boot (GRUB BIOS) - 83bd6b9d-7f41-11dc-be0b-001560b84f0f # FreeBSD Boot - 49f48d5a-b10e-11dc-b99b-0019d1879648 # NetBSD boot - 824cc7a0-36a8-11e3-890a-952519ad3f61 # OpenBSD boot - 426f6f74-0000-11aa-aa11-00306543ecac # Apple Boot - ); - my %boot_mbr = map { $_ => 1 } qw( 0xef 0xa0 0xa5 0xa6 0xa9 0xab ); - my $is_boot_type = - ( $t =~ -/\b(efi|bios-?boot|freebsd-boot|netbsd-boot|openbsd-boot|apple-boot)\b/ - ); - my $is_boot_raw = ( $raw && ( $boot_guid{$raw} || $boot_mbr{$raw} ) ); - - if ( $is_boot_type || $is_boot_raw ) { - my $fmt = - ( $t =~ /efi/ - || $raw eq 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b' - || lc($raw) eq '0xef' ) - ? get_type_description('efi') - : get_type_description('freebsd-boot'); - -# Access text properly - %text is in main namespace when this is called from CGI - my $boot_txt = $text{'disk_boot'}; - my $role_txt = $text{'disk_boot_role'}; - return ( $fmt, $boot_txt, $role_txt ); - } - - # Heuristic fallback only if no explicit identifiers are present - if ( - ( - !$args{'entry_part_type'} - || $args{'entry_part_type'} eq '-' - || $args{'entry_part_type'} eq 'unknown' - ) - && ( $args{'part_num'} || '' ) eq '1' - ) - { - my $sb = $args{'size_blocks'} || 0; - if ( $sb > 0 ) { - my $base = - base_disk_device( '/dev/' . ( $args{'base_device'} || '' ) ); - my $ss = get_disk_sectorsize($base) || 512; - my $bytes = $sb * $ss; - if ( $bytes <= 2 * 1024 * 1024 ) { # <= 2MiB - return ( get_type_description('freebsd-boot'), - $text{'disk_boot'}, $text{'disk_boot_role'} ); - } - } - elsif ($args{'size_human'} - && $args{'size_human'} =~ /^(?:512k|1m|1\.0m)$/i ) - { - return ( get_type_description('freebsd-boot'), - $text{'disk_boot'}, $text{'disk_boot_role'} ); - } - } - if ($zdev) { - $format = 'FreeBSD ZFS'; - my $inzfs_txt = $text{'disk_inzfs'}; - my $z_mirror = $text{'disk_zfs_mirror'}; - my $z_stripe = $text{'disk_zfs_stripe'}; - my $z_single = $text{'disk_zfs_single'}; - my $z_data = $text{'disk_zfs_data'}; - my $z_log = $text{'disk_zfs_log'}; - my $z_cache = $text{'disk_zfs_cache'}; - my $z_spare = $text{'disk_zfs_spare'}; - $usage = $inzfs_txt . ' ' . $zdev->{'pool'}; - my $vt = $zdev->{'vdev_type'}; - my $cnt = $zdev->{'vdev_count'} || 0; - if ( $vt eq 'log' ) { $role = $z_log; } - elsif ( $vt eq 'cache' ) { $role = $z_cache; } - elsif ( $vt eq 'spare' ) { $role = $z_spare; } - elsif ( $zdev->{'is_mirrored'} ) { - $role = $z_mirror; - $role .= " ($cnt in group)" if $cnt; - } - elsif ( $zdev->{'is_raidz'} ) { - my $lvl = $zdev->{'raidz_level'} || 1; - $role = 'RAID-Z' . $lvl; - $role .= " ($cnt in group)" if $cnt; - } - elsif ( $zdev->{'is_striped'} ) { - $role = $z_stripe; - $role .= " ($cnt in group)" if $cnt; - } - elsif ( $zdev->{'is_single'} ) { $role = $z_single; } - else { $role = $z_data; } - return ( $format, $usage, $role ); - } - + my $raw = lc( $args{'entry_rawtype'} || '' ); + my $t = lc( $type_desc || '' ); + my %boot_guid = map { $_ => 1 } qw( + c12a7328-f81f-11d2-ba4b-00a0c93ec93b # EFI System + 21686148-6449-6e6f-744e-656564454649 # BIOS Boot (GRUB BIOS) + 83bd6b9d-7f41-11dc-be0b-001560b84f0f # FreeBSD Boot + 49f48d5a-b10e-11dc-b99b-0019d1879648 # NetBSD boot + 824cc7a0-36a8-11e3-890a-952519ad3f61 # OpenBSD boot + 426f6f74-0000-11aa-aa11-00306543ecac # Apple Boot + ); + my %boot_mbr = map { $_ => 1 } qw( 0xef 0xa0 0xa5 0xa6 0xa9 0xab ); + my $is_boot_type = + ( $t =~ +/\b(efi|bios-?boot|freebsd-boot|netbsd-boot|openbsd-boot|apple-boot)\b/ + ); + my $is_boot_raw = ( $raw && ( $boot_guid{$raw} || $boot_mbr{$raw} ) ); + + if ( $is_boot_type || $is_boot_raw ) { + my $fmt = + ( $t =~ /efi/ + || $raw eq 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b' + || lc($raw) eq '0xef' ) + ? get_type_description('efi') + : get_type_description('freebsd-boot'); + +# Access text properly - %text is in main namespace when this is called from CGI + my $boot_txt = $text{'disk_boot'}; + my $role_txt = $text{'disk_boot_role'}; + return ( $fmt, $boot_txt, $role_txt ); + } + + # Heuristic fallback only if no explicit identifiers are present + if ( + ( + !$args{'entry_part_type'} + || $args{'entry_part_type'} eq '-' + || $args{'entry_part_type'} eq 'unknown' + ) + && ( $args{'part_num'} || '' ) eq '1' + ) + { + my $sb = $args{'size_blocks'} || 0; + if ( $sb > 0 ) { + my $base = + base_disk_device( '/dev/' . ( $args{'base_device'} || '' ) ); + my $ss = get_disk_sectorsize($base) || 512; + my $bytes = $sb * $ss; + if ( $bytes <= 2 * 1024 * 1024 ) { # <= 2MiB + return ( get_type_description('freebsd-boot'), + $text{'disk_boot'}, $text{'disk_boot_role'} ); + } + } + elsif ($args{'size_human'} + && $args{'size_human'} =~ /^(?:512k|1m|1\.0m)$/i ) + { + return ( get_type_description('freebsd-boot'), + $text{'disk_boot'}, $text{'disk_boot_role'} ); + } + } + if ($zdev) { + $format = 'FreeBSD ZFS'; + my $inzfs_txt = $text{'disk_inzfs'}; + my $z_mirror = $text{'disk_zfs_mirror'}; + my $z_stripe = $text{'disk_zfs_stripe'}; + my $z_single = $text{'disk_zfs_single'}; + my $z_data = $text{'disk_zfs_data'}; + my $z_log = $text{'disk_zfs_log'}; + my $z_cache = $text{'disk_zfs_cache'}; + my $z_spare = $text{'disk_zfs_spare'}; + $usage = $inzfs_txt . ' ' . $zdev->{'pool'}; + my $vt = $zdev->{'vdev_type'}; + my $cnt = $zdev->{'vdev_count'} || 0; + if ( $vt eq 'log' ) { $role = $z_log; } + elsif ( $vt eq 'cache' ) { $role = $z_cache; } + elsif ( $vt eq 'spare' ) { $role = $z_spare; } + elsif ( $zdev->{'is_mirrored'} ) { + $role = $z_mirror; + $role .= " ($cnt in group)" if $cnt; + } + elsif ( $zdev->{'is_raidz'} ) { + my $lvl = $zdev->{'raidz_level'} || 1; + $role = 'RAID-Z' . $lvl; + $role .= " ($cnt in group)" if $cnt; + } + elsif ( $zdev->{'is_striped'} ) { + $role = $z_stripe; + $role .= " ($cnt in group)" if $cnt; + } + elsif ( $zdev->{'is_single'} ) { $role = $z_single; } + else { $role = $z_data; } + return ( $format, $usage, $role ); + } + # Not in ZFS: infer by type_desc, rawtype and size heuristic (this # shouldn't be reached for boot, but keep as fallback) - if ( defined $type_desc && $type_desc =~ /(?:freebsd|linux)-swap/i ) { - $format = 'Swap'; - $usage = $text{'disk_swap'}; - $role = $text{'disk_swap_role'}; - } - elsif ( defined $type_desc && $type_desc =~ /linux-lvm/i ) { - $format = 'Linux LVM'; - } - elsif ( defined $type_desc && $type_desc =~ /linux-raid/i ) { - $format = 'Linux RAID'; - } - - # Recognize common FAT/NTFS identifiers on both GPT and MBR - elsif ( defined $type_desc && $type_desc =~ /ntfs/i ) { $format = 'NTFS'; } - elsif ( defined $type_desc && $type_desc =~ /fat32/i ) { - $format = 'FAT32'; - } - elsif ( defined $type_desc && $type_desc =~ /fat|msdos/i ) { - $format = 'FAT'; - } - elsif ( defined $type_desc && $type_desc =~ /ms-basic/i ) { - $format = 'FAT/NTFS'; - } - elsif ( $raw ne '' && $raw =~ /^\d+$/ ) { - - # MBR raw type codes: 7=NTFS, 6/11/12/14 = FAT variants - my $code = int($raw); - if ( $code == 7 ) { $format = 'NTFS'; } - elsif ( $code == 11 || $code == 12 ) { $format = 'FAT32'; } - elsif ( $code == 6 || $code == 14 ) { $format = 'FAT'; } - } - elsif ( defined $type_desc && $type_desc =~ /linux/i ) { - $format = 'Linux'; - } - elsif ( defined $type_desc && $type_desc =~ /apple-ufs/i ) { - $format = 'Apple UFS'; - } - elsif ( defined $type_desc && $type_desc =~ /apple-hfs/i ) { - $format = 'HFS+'; - } - elsif ( defined $type_desc && $type_desc =~ /apple-raid/i ) { - $format = 'Apple RAID'; - } - elsif ( defined $type_desc && $type_desc =~ /freebsd-ufs/i ) { - $format = 'FreeBSD UFS'; - } - elsif ( defined $type_desc && $type_desc =~ /freebsd-zfs/i ) { - $format = 'FreeBSD ZFS'; - } - else { - if ( defined $args{'part_label'} - && $args{'part_label'} =~ /^swap\d*$/i ) - { - $format = 'Swap'; - $usage = $text{'disk_swap'}; - $role = $text{'disk_swap_role'}; - } - } - return ( $format, $usage, $role ); -} - -# list_partition_types() -# Returns a list suitable for ui_select: [ [ value, label ], ... ] -# Adapts to whether the system uses gpart (GPT) or legacy disklabel. -sub list_partition_types { - my ($scheme) = @_; - if ( is_using_gpart() ) { - -# BSD-on-MBR inner label (used when creating sub-partitions inside an MBR slice) - if ( defined $scheme && $scheme =~ /BSD/i ) { - return ( - [ 'freebsd-ufs', get_type_description('freebsd-ufs') ], - [ 'freebsd-zfs', get_type_description('freebsd-zfs') ], - [ 'freebsd-swap', get_type_description('freebsd-swap') ], - [ 'freebsd-vinum', get_type_description('freebsd-vinum') ], - ); - } - + if ( defined $type_desc && $type_desc =~ /(?:freebsd|linux)-swap/i ) { + $format = 'Swap'; + $usage = $text{'disk_swap'}; + $role = $text{'disk_swap_role'}; + } + elsif ( defined $type_desc && $type_desc =~ /linux-lvm/i ) { + $format = 'Linux LVM'; + } + elsif ( defined $type_desc && $type_desc =~ /linux-raid/i ) { + $format = 'Linux RAID'; + } + + # Recognize common FAT/NTFS identifiers on both GPT and MBR + elsif ( defined $type_desc && $type_desc =~ /ntfs/i ) { $format = 'NTFS'; } + elsif ( defined $type_desc && $type_desc =~ /fat32/i ) { + $format = 'FAT32'; + } + elsif ( defined $type_desc && $type_desc =~ /fat|msdos/i ) { + $format = 'FAT'; + } + elsif ( defined $type_desc && $type_desc =~ /ms-basic/i ) { + $format = 'FAT/NTFS'; + } + elsif ( $raw ne '' && $raw =~ /^\d+$/ ) { + + # MBR raw type codes: 7=NTFS, 6/11/12/14 = FAT variants + my $code = int($raw); + if ( $code == 7 ) { $format = 'NTFS'; } + elsif ( $code == 11 || $code == 12 ) { $format = 'FAT32'; } + elsif ( $code == 6 || $code == 14 ) { $format = 'FAT'; } + } + elsif ( defined $type_desc && $type_desc =~ /linux/i ) { + $format = 'Linux'; + } + elsif ( defined $type_desc && $type_desc =~ /apple-ufs/i ) { + $format = 'Apple UFS'; + } + elsif ( defined $type_desc && $type_desc =~ /apple-hfs/i ) { + $format = 'HFS+'; + } + elsif ( defined $type_desc && $type_desc =~ /apple-raid/i ) { + $format = 'Apple RAID'; + } + elsif ( defined $type_desc && $type_desc =~ /freebsd-ufs/i ) { + $format = 'FreeBSD UFS'; + } + elsif ( defined $type_desc && $type_desc =~ /freebsd-zfs/i ) { + $format = 'FreeBSD ZFS'; + } + else { + if ( defined $args{'part_label'} + && $args{'part_label'} =~ /^swap\d*$/i ) + { + $format = 'Swap'; + $usage = $text{'disk_swap'}; + $role = $text{'disk_swap_role'}; + } + } + return ( $format, $usage, $role ); +} + +# list_partition_types() +# Returns a list suitable for ui_select: [ [ value, label ], ... ] +# Adapts to whether the system uses gpart (GPT) or legacy disklabel. +sub list_partition_types { + my ($scheme) = @_; + if ( is_using_gpart() ) { + +# BSD-on-MBR inner label (used when creating sub-partitions inside an MBR slice) + if ( defined $scheme && $scheme =~ /BSD/i ) { + return ( + [ 'freebsd-ufs', get_type_description('freebsd-ufs') ], + [ 'freebsd-zfs', get_type_description('freebsd-zfs') ], + [ 'freebsd-swap', get_type_description('freebsd-swap') ], + [ 'freebsd-vinum', get_type_description('freebsd-vinum') ], + ); + } + # If outer scheme is not GPT (e.g. MBR), present MBR partition types for # top-level slices - if ( defined $scheme && $scheme !~ /GPT/i ) { - my @mbr_types = ( - [ 'freebsd', get_type_description('freebsd') ], - [ 'fat32lba', 'FAT32 (LBA)' ], - [ 'fat32', 'FAT32' ], - [ 'fat16', 'FAT16' ], - [ 'ntfs', 'NTFS' ], - [ 'linux', 'Linux' ], - [ 'linux-swap', get_type_description('linux-swap') ], - [ 'efi', get_type_description('efi') ], - ); - return @mbr_types; - } - - # Default GPT types (common, practical choices) - my @gpt_types = ( - [ 'efi', get_type_description('efi') ], - [ 'bios-boot', get_type_description('bios-boot') ], - [ 'freebsd-boot', get_type_description('freebsd-boot') ], - [ 'freebsd-zfs', get_type_description('freebsd-zfs') ], - [ 'freebsd-ufs', get_type_description('freebsd-ufs') ], - [ 'freebsd-swap', get_type_description('freebsd-swap') ], - [ 'freebsd-vinum', get_type_description('freebsd-vinum') ], - [ 'ms-basic-data', get_type_description('ms-basic-data') ], - [ 'ms-reserved', get_type_description('ms-reserved') ], - [ 'ms-recovery', get_type_description('ms-recovery') ], - [ 'linux-data', get_type_description('linux-data') ], - [ 'linux-swap', get_type_description('linux-swap') ], - [ 'linux-lvm', get_type_description('linux-lvm') ], - [ 'linux-raid', get_type_description('linux-raid') ], - [ 'apple-boot', get_type_description('apple-boot') ], - [ 'apple-hfs', get_type_description('apple-hfs') ], - [ 'apple-ufs', get_type_description('apple-ufs') ], - [ 'apple-raid', get_type_description('apple-raid') ], - [ 'apple-label', get_type_description('apple-label') ], - ); - return @gpt_types; - } - else { - # Legacy BSD disklabel types - my @label_types = ( - [ '4.2BSD', 'FreeBSD UFS' ], - [ 'swap', 'Swap' ], - [ 'unused', 'Unused' ], - [ 'vinum', 'FreeBSD Vinum' ], - ); - return @label_types; - } -} - -sub get_partition_role { - my ($part) = @_; - if ( is_boot_partition($part) ) { return $text{'part_boot'}; } - my $zfs_info = get_all_zfs_info(); - if ( $zfs_info->{ $part->{'device'} } - and $zfs_info->{ $part->{'device'} } =~ /\(log\)$/ ) - { - return $text{'part_zfslog'}; - } - if ( $zfs_info->{ $part->{'device'} } ) { - return $text{'part_zfsdata'}; - } - my @mounts = mount::list_mounted(); - foreach my $m (@mounts) { - if ( $m->[0] eq $part->{'device'} ) { - return text( 'part_mounted', $m->[1] ); - } - } - return $text{'part_unused'}; -} - -sub get_detailed_disk_info { - my ($device) = @_; - my $info = {}; - ( my $dev_name = $device ) =~ s/^\/dev\///; - my $out = backquote_command( - "geom disk list " . quote_path($dev_name) . " 2>/dev/null" ); - return undef if ($?); - foreach my $line ( split( /\n/, $out ) ) { - if ( $line =~ /^\s+Mediasize:\s+(\d+)\s+\(([^)]+)\)/ ) { - $info->{'mediasize_bytes'} = $1; - $info->{'mediasize'} = $2; - } - elsif ( $line =~ /^\s+Sectorsize:\s+(\d+)/ ) { - $info->{'sectorsize'} = $1; - } - elsif ( $line =~ /^\s+Stripesize:\s+(\d+)/ ) { - $info->{'stripesize'} = $1; - } - elsif ( $line =~ /^\s+Stripeoffset:\s+(\d+)/ ) { - $info->{'stripeoffset'} = $1; - } - elsif ( $line =~ /^\s+Mode:\s+(.*)/ ) { - $info->{'mode'} = $1; - } - elsif ( $line =~ /^\s+rotationrate:\s+(\d+)/ ) { - $info->{'rotationrate'} = $1; - } - elsif ( $line =~ /^\s+ident:\s+(.*)/ ) { - $info->{'ident'} = $1; - } - elsif ( $line =~ /^\s+lunid:\s+(.*)/ ) { - $info->{'lunid'} = $1; - } - elsif ( $line =~ /^\s+descr:\s+(.*)/ ) { - $info->{'descr'} = $1; - } - } - return $info; -} - + if ( defined $scheme && $scheme !~ /GPT/i ) { + my @mbr_types = ( + [ 'freebsd', get_type_description('freebsd') ], + [ 'fat32lba', 'FAT32 (LBA)' ], + [ 'fat32', 'FAT32' ], + [ 'fat16', 'FAT16' ], + [ 'ntfs', 'NTFS' ], + [ 'linux', 'Linux' ], + [ 'linux-swap', get_type_description('linux-swap') ], + [ 'efi', get_type_description('efi') ], + ); + return @mbr_types; + } + + # Default GPT types (common, practical choices) + my @gpt_types = ( + [ 'efi', get_type_description('efi') ], + [ 'bios-boot', get_type_description('bios-boot') ], + [ 'freebsd-boot', get_type_description('freebsd-boot') ], + [ 'freebsd-zfs', get_type_description('freebsd-zfs') ], + [ 'freebsd-ufs', get_type_description('freebsd-ufs') ], + [ 'freebsd-swap', get_type_description('freebsd-swap') ], + [ 'freebsd-vinum', get_type_description('freebsd-vinum') ], + [ 'ms-basic-data', get_type_description('ms-basic-data') ], + [ 'ms-reserved', get_type_description('ms-reserved') ], + [ 'ms-recovery', get_type_description('ms-recovery') ], + [ 'linux-data', get_type_description('linux-data') ], + [ 'linux-swap', get_type_description('linux-swap') ], + [ 'linux-lvm', get_type_description('linux-lvm') ], + [ 'linux-raid', get_type_description('linux-raid') ], + [ 'apple-boot', get_type_description('apple-boot') ], + [ 'apple-hfs', get_type_description('apple-hfs') ], + [ 'apple-ufs', get_type_description('apple-ufs') ], + [ 'apple-raid', get_type_description('apple-raid') ], + [ 'apple-label', get_type_description('apple-label') ], + ); + return @gpt_types; + } + else { + # Legacy BSD disklabel types + my @label_types = ( + [ '4.2BSD', 'FreeBSD UFS' ], + [ 'swap', 'Swap' ], + [ 'unused', 'Unused' ], + [ 'vinum', 'FreeBSD Vinum' ], + ); + return @label_types; + } +} + +sub get_partition_role { + my ($part) = @_; + if ( is_boot_partition($part) ) { return $text{'part_boot'}; } + my $zfs_info = get_all_zfs_info(); + if ( $zfs_info->{ $part->{'device'} } + and $zfs_info->{ $part->{'device'} } =~ /\(log\)$/ ) + { + return $text{'part_zfslog'}; + } + if ( $zfs_info->{ $part->{'device'} } ) { + return $text{'part_zfsdata'}; + } + my @mounts = mount::list_mounted(); + foreach my $m (@mounts) { + if ( $m->[0] eq $part->{'device'} ) { + return text( 'part_mounted', $m->[1] ); + } + } + return $text{'part_unused'}; +} + +sub get_detailed_disk_info { + my ($device) = @_; + my $info = {}; + ( my $dev_name = $device ) =~ s/^\/dev\///; + my $out = backquote_command( + "geom disk list " . quote_path($dev_name) . " 2>/dev/null" ); + return undef if ($?); + foreach my $line ( split( /\n/, $out ) ) { + if ( $line =~ /^\s+Mediasize:\s+(\d+)\s+\(([^)]+)\)/ ) { + $info->{'mediasize_bytes'} = $1; + $info->{'mediasize'} = $2; + } + elsif ( $line =~ /^\s+Sectorsize:\s+(\d+)/ ) { + $info->{'sectorsize'} = $1; + } + elsif ( $line =~ /^\s+Stripesize:\s+(\d+)/ ) { + $info->{'stripesize'} = $1; + } + elsif ( $line =~ /^\s+Stripeoffset:\s+(\d+)/ ) { + $info->{'stripeoffset'} = $1; + } + elsif ( $line =~ /^\s+Mode:\s+(.*)/ ) { + $info->{'mode'} = $1; + } + elsif ( $line =~ /^\s+rotationrate:\s+(\d+)/ ) { + $info->{'rotationrate'} = $1; + } + elsif ( $line =~ /^\s+ident:\s+(.*)/ ) { + $info->{'ident'} = $1; + } + elsif ( $line =~ /^\s+lunid:\s+(.*)/ ) { + $info->{'lunid'} = $1; + } + elsif ( $line =~ /^\s+descr:\s+(.*)/ ) { + $info->{'descr'} = $1; + } + } + return $info; +} + sub initialize_slice { my ( $disk, $slice ) = @_; # If the outer disk is GPT, we are not creating inner BSD labels if ( is_using_gpart() ) { - my $base = $disk->{'device'}; - $base =~ s{^/dev/}{}; - my $ds = get_disk_structure($base); - if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) { - return undef; + my $base = $disk->{'device'}; + $base =~ s{^/dev/}{}; + my $ds = get_disk_structure($base); + if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) { + return undef; } } @@ -2247,69 +2251,69 @@ sub initialize_slice { return "Failed to initialize slice: $werr" if $werr; return undef; } - -# --------------------------------------------------------------------- -# partition_select(name, value) -# Provide a selector for partitions/slices for external modules (e.g., mount) -# Returns an HTML element populated with available devices. +sub partition_select { + my ( $name, $value ) = @_; + my @opts; + my @disks = list_disks_partitions(); + foreach my $d (@disks) { + foreach my $s ( @{ $d->{'slices'} || [] } ) { + my $stype = get_type_description( $s->{'type'} ) || $s->{'type'}; + my $ssize_b = bytes_from_blocks( $s->{'device'}, $s->{'blocks'} ); + my $ssize = defined $ssize_b ? nice_size($ssize_b) : undef; + my $slabel = $s->{'device'} + . ( defined $ssize ? " ($stype, $ssize)" : " ($stype)" ); + push @opts, [ $s->{'device'}, $slabel ]; + foreach my $p ( @{ $s->{'parts'} || [] } ) { + my $ptype = + get_type_description( $p->{'type'} ) || $p->{'type'}; + my $psz_b = bytes_from_blocks( $p->{'device'}, $p->{'blocks'} ); + my $psz = defined $psz_b ? nice_size($psz_b) : undef; + my $plabel = $p->{'device'} + . ( defined $psz ? " ($ptype, $psz)" : " ($ptype)" ); + push @opts, [ $p->{'device'}, $plabel ]; + } + } + } + + # Sort options by device name for consistency + @opts = sort { $a->[0] cmp $b->[0] } @opts; + $value ||= ( $opts[0] ? $opts[0]->[0] : undef ); + return ui_select( $name, $value, \@opts, 1, 0, 0, 0 ); +} + +# Return a short human-readable description for a given device +# Example: "FreeBSD ZFS, 39G" +sub partition_description { + my ($device) = @_; + return undef unless $device; + my ( $ptype, $blocks ); + my @disks = list_disks_partitions(); + foreach my $d (@disks) { + foreach my $s ( @{ $d->{'slices'} || [] } ) { + if ( $s->{'device'} && $s->{'device'} eq $device ) { + $ptype = get_type_description( $s->{'type'} ) || $s->{'type'}; + $blocks = $s->{'blocks'}; + last; + } + foreach my $p ( @{ $s->{'parts'} || [] } ) { + if ( $p->{'device'} && $p->{'device'} eq $device ) { + $ptype = + get_type_description( $p->{'type'} ) || $p->{'type'}; + $blocks = $p->{'blocks'}; + last; + } + } + } + } + return undef unless defined $ptype; + my $bytes = bytes_from_blocks( $device, $blocks ); + my $sz = defined $bytes ? nice_size($bytes) : '-'; + return "$ptype, $sz"; +} + +1; diff --git a/bsdfdisk/delete_part.cgi b/bsdfdisk/delete_part.cgi index f486ed615..3a3498eee 100755 --- a/bsdfdisk/delete_part.cgi +++ b/bsdfdisk/delete_part.cgi @@ -11,14 +11,15 @@ our ( %in, %text, $module_name ); # Get the disk and slice my @disks = &list_disks_partitions(); -$in{'device'} =~ /^[a-zA-Z0-9_\/.-]+$/ +# Validate inputs +($in{'device'} =~ /^[a-zA-Z0-9_\/.-]+$/ && $in{'device'} !~ /\.\./) or &error( $text{'disk_edevice'} || 'Invalid device' ); -$in{'device'} !~ /\.\./ or &error( $text{'disk_edevice'} || 'Invalid device' ); $in{'slice'} =~ /^\d+$/ or &error( $text{'slice_egone'} ); $in{'part'} =~ /^[a-z]$/ or &error( $text{'part_egone'} ); my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; $disk || &error( $text{'disk_egone'} ); -my ($slice) = grep { $_->{'number'} eq $in{'slice'} } @{ $disk->{'slices'} }; +my $in_slice_num = int($in{'slice'}); +my ($slice) = grep { int($_->{'number'}) == $in_slice_num } @{ $disk->{'slices'} }; $slice || &error( $text{'slice_egone'} ); my ($part) = grep { $_->{'letter'} eq $in{'part'} } @{ $slice->{'parts'} }; $part || &error( $text{'part_egone'} ); @@ -41,7 +42,7 @@ if ( $in{'confirm'} ) { else { # Ask first my @st = &fdisk::device_status( $part->{'device'} ); - my $use = &fdisk::device_status_link(@st); + my $use = &fdisk::device_status_link(@st); # returns safe HTML link(s); ensure upstream sanitization print &ui_confirmation_form( "delete_part.cgi", &text( @@ -53,9 +54,10 @@ else { [ "slice", $in{'slice'} ], [ "part", $in{'part'} ] ], - [ [ "confirm", $text{'dslice_confirm'} ] ], + # Use partition-specific confirmation text key if available + [ [ "confirm", $text{'dpart_confirm'} || $text{'dslice_confirm'} ] ], undef, - $use ? &text( 'dpart_warn', &html_escape($use) ) : undef + $use ? &text( 'dpart_warn', $use ) : undef ); } diff --git a/bsdfdisk/delete_slice.cgi b/bsdfdisk/delete_slice.cgi index 4ec71d36d..227486c3c 100755 --- a/bsdfdisk/delete_slice.cgi +++ b/bsdfdisk/delete_slice.cgi @@ -58,7 +58,7 @@ else { [ [ "confirm", $text{'dslice_confirm'} ] ], undef, @warn - ? &text( 'dslice_warn', &html_escape( join( " ", @warn ) ) ) + ? &text( 'dslice_warn', join( " ", @warn ) ) : undef ); } diff --git a/bsdfdisk/edit_part.cgi b/bsdfdisk/edit_part.cgi index f5ba93bdf..b215ec035 100755 --- a/bsdfdisk/edit_part.cgi +++ b/bsdfdisk/edit_part.cgi @@ -101,8 +101,8 @@ my $use_text = ( ( !@st && !$zfs_info->{ $part->{'device'} } ) ? $text{'part_nouse'} : ( ( $st[2] || $zfs_info->{ $part->{'device'} } ) - ? text( 'part_inuse', &html_escape($use) ) - : text( 'part_foruse', &html_escape($use) ) + ? text( 'part_inuse', $use ) + : text( 'part_foruse', $use ) ) ); print ui_table_row( $text{'part_use'}, $use_text ); @@ -144,7 +144,7 @@ if ( @{ $slice->{'parts'} || [] } ) { $p->{'startblock'}, $p->{'startblock'} + $p->{'blocks'} - 1, ( $pb2 ? safe_nice_size($pb2) : '-' ), - &html_escape($usep), + $usep, &html_escape($rolep) ] ); diff --git a/bsdfdisk/edit_slice.cgi b/bsdfdisk/edit_slice.cgi index 09d50776d..6c1359f42 100755 --- a/bsdfdisk/edit_slice.cgi +++ b/bsdfdisk/edit_slice.cgi @@ -116,8 +116,8 @@ else { print ui_table_row( $text{'slice_suse'}, ( !$slice_use || $slice_use eq $text{'part_nouse'} ) ? $text{'part_nouse'} - : ( $slice_status[2] ? text( 'part_inuse', html_escape($slice_use) ) - : text( 'part_foruse', html_escape($slice_use) ) ) + : ( $slice_status[2] ? text( 'part_inuse', $slice_use ) + : text( 'part_foruse', $slice_use ) ) ); # Add a row for the slice role @@ -223,7 +223,7 @@ if ( @{ $slice->{'parts'} } ) { $p->{'startblock'}, $p->{'startblock'} + $p->{'blocks'} - 1, $stripesize, - html_escape($use_txt), + $use_txt, html_escape($role_txt), ] );