using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; namespace SabreTools.IO.Extensions { /// /// Methods around device access and identification /// public static class DeviceExtensions { #region Unix /// /// sysfs block-device name prefixes that the fixed-drive enumerator does not surface: /// loopback images, ramdisks, device-mapper and software RAID virtual devices, network /// block devices, ZFS volumes, and the floppy and optical drives (each surfaced /// separately by its own enumerator rather than as a fixed or removable disk). /// private static readonly string[] _excludedUnixBlockPrefixes = [ "dm-", // device-mapper (LVM/LUKS, etc.) virtual devices "fd", // floppy drive (surfaced separately by the floppy enumerator) "loop", // loopback-mounted image "md", // software RAID (mdadm) "nbd", // network block device "ram", // ramdisk "sr", // optical drive (surfaced separately by the optical enumerator) "zd", // ZFS volume (zvol) "zram", // compressed ramdisk ]; /// /// Floppy media sizes in bytes for the standard 5.25" and 3.5" PC formats /// (360 KB, 720 KB, 1.2 MB, 1.44 MB, 2.88 MB). A removable SCSI disk reporting one of /// these exact capacities is a USB floppy with media inserted; no other removable /// storage uses these sizes, so this doubles as the floppy identity check. /// private static readonly List _unixFloppyMediaSizes = [ 368640, // 360 KB (5.25" DD) 737280, // 720 KB (3.5" DD) 1228800, // 1.2 MB (5.25" HD) 1474560, // 1.44 MB (3.5" HD) 2949120, // 2.88 MB (3.5" ED) ]; /// /// Enumerate Linux floppy block devices under a directory by matching the kernel /// convention "fd" followed by one or more digits (fd0, fd1, ...). /// /// Device paths, or an empty list when the directory is unreadable /// /// This deliberately rejects the "/dev/fd" symlink (no trailing digits) and /// format-specific nodes such as "fd0u1440" (embedded non-digit characters), /// swhich are not whole-device targets. /// public static List EnumerateUnixFloppyBlockPaths() { // If the device root is invalid string devRoot = "/dev"; if (!Directory.Exists(devRoot)) return []; // Attempt to retrieve all matching devices string[] entries = IOExtensions.SafeGetFiles(devRoot, "fd*"); // Return only valid devices var result = new List(); foreach (var path in entries) { if (HasDeviceIndexSuffix(Path.GetFileName(path), "fd")) result.Add(path); } return result; } /// /// Enumerate Linux optical block devices under a directory by matching /// the kernel convention "sr" followed by one or more digits (sr0, sr1, ...). /// /// Device paths, or an empty list when the directory is unreadable public static List EnumerateUnixOpticalBlockPaths() { // If the device root is invalid string devRoot = "/dev"; if (!Directory.Exists(devRoot)) return []; // Attempt to retrieve all matching devices string[] entries = IOExtensions.SafeGetFiles(devRoot, "sr*"); // Return only valid devices var result = new List(); foreach (var path in entries) { if (HasDeviceIndexSuffix(Path.GetFileName(path), "sr")) result.Add(path); } return result; } /// /// Enumerate Linux optical drives via their generic SCSI nodes (/dev/sg*). /// /// Device paths, or an empty list when the sysfs directory is unreadable /// /// Unlike the block nodes, "sg" is not optical-specific, so each candidate is /// confirmed against sysfs: an optical drive reports SCSI peripheral device type 5. /// public static List EnumerateUnixOpticalGenericPaths() { // If the device root is invalid string devRoot = "/dev"; if (!Directory.Exists(devRoot)) return []; // If the system generic root is invalid string sysfsScsiGenericRoot = "/sys/class/scsi_generic"; if (!Directory.Exists(sysfsScsiGenericRoot)) return []; // Attempt to retrieve all matching devices string[] entries = IOExtensions.SafeGetFileSystemEntries(sysfsScsiGenericRoot, "sg*"); // Return only valid devices var result = new List(); foreach (var entry in entries) { string name = Path.GetFileName(entry); if (!HasDeviceIndexSuffix(name, "sg")) continue; // "sg" is generic SCSI; keep only optical drives (peripheral device type 5) int scsiType = ReadUnixBlockDeviceType(entry); if (scsiType != 5) continue; result.Add(Path.Combine(devRoot, name)); } return result; } /// /// Enumerate USB floppy drives, which the kernel exposes as ordinary SCSI disks /// (/dev/sd*) rather than /dev/fd* nodes. /// /// Device paths, or an empty list when the directory is unreadable /// /// A disk is treated as a floppy when it reports itself removable and its media is one of /// the standard floppy sizes. This mirrors how Windows identifies a floppy from the /// inserted medium (Win32_LogicalDisk.MediaType) and, like that path, only recognizes the /// drive while floppy media is present. /// public static List EnumerateUnixUsbFloppyBlockPaths() { // If the device root is invalid string devRoot = "/dev"; if (!Directory.Exists(devRoot)) return []; // If the system block root is invalid string sysBlockRoot = "/sys/block"; if (!Directory.Exists(sysBlockRoot)) return []; // Attempt to retrieve all matching devices string[] entries = IOExtensions.SafeGetFileSystemEntries(sysBlockRoot, "sd*"); // Return only valid devices var result = new List(); foreach (var entry in entries) { // Ensure the drive has the removable flag set if (!ReadUnixRemovableFlag(entry)) continue; // Filter out non-standard floppy sizes for now if (!_unixFloppyMediaSizes.Contains(ReadUnixBlockDeviceSize(entry))) continue; string devicePath = Path.Combine(devRoot, Path.GetFileName(entry)); result.Add(devicePath); } return result; } /// /// Check whether a sysfs block-device name is a virtual or non-target device that /// should not be surfaced as a dump target. /// /// Block-device name (e.g. "sda", "loop0") /// True when the device should be skipped public static bool IsExcludedUnixBlockDevice(string name) { if (string.IsNullOrEmpty(name)) return true; foreach (var prefix in _excludedUnixBlockPrefixes) { if (name.StartsWith(prefix, StringComparison.Ordinal)) return true; } return false; } #if NET6_0_OR_GREATER /// /// Get the label based on the block device mapping for generic SCSI /// /// Device path to check /// Label for the generic device on success, null otherwise public static string? GetLabelGeneric(string? devicePath) { // If the device path is invalid if (devicePath is null) return null; // Get the mapped device var mappedDevice = GetMappedDeviceForGeneric(devicePath); if (mappedDevice is null) return null; // Return the label for that device return GetLabelLink(mappedDevice); } /// /// Get the label based on the softlinks in /dev/disk/by-label/ /// /// Device path to check /// Label for the device on success, null otherwise public static string? GetLabelLink(string? devicePath) { // If the device path is invalid if (devicePath is null) return null; // Try looping through the by-label directory string byLabelPath = "/dev/disk/by-label/"; try { if (!Directory.Exists(byLabelPath)) return null; var entries = Directory.GetFileSystemEntries("/dev/disk/by-label/"); foreach (var entry in entries) { // Ensure the entry is a file if (!File.Exists(entry)) continue; // Extract the link target from the file var linkTarget = new FileInfo(entry).LinkTarget; if (linkTarget is null) continue; // Reformat the link target based on relative paths linkTarget = linkTarget.Replace("../../", "/dev/"); // If the drive does not match if (!string.Equals(devicePath, linkTarget)) continue; // Return the mount point return Path.GetFileName(entry) .Replace("\\x20", " ") // Replace hex-encoded space characters .Replace("\\020", " "); // Replace octal-encoded space characters } // No matches were found return null; } catch { // Absorb errors for now return null; } } #endif /// /// Get the mount point based on the block device mapping for generic SCSI /// /// Device path to check /// Mount point for the generic device on success, null otherwise public static string? GetMountPointGeneric(string? devicePath) { // If the device path is invalid if (devicePath is null) return null; // Get the mapped device var mappedDevice = GetMappedDeviceForGeneric(devicePath); if (mappedDevice is null) return null; // Return the mount point for that device return GetMountPointMounts(mappedDevice); } /// /// Get the mount point based a mapping to /proc/mounts /// /// Device path to check /// Mount point for the device on success, null otherwise public static string? GetMountPointMounts(string? devicePath) { // If the device path is invalid if (devicePath is null) return null; // Try reading the mounts file for information string mountsPath = "/proc/mounts"; try { if (!File.Exists(mountsPath)) return null; // Read all lines from the mounts file string[] mountsLines = File.ReadAllLines(mountsPath); foreach (string mount in mountsLines) { // Split the whitespace-separated mount entry line #if NETFRAMEWORK || NETSTANDARD string[] entry = mount.Split(' '); #else string[] entry = mount.Split(' ', StringSplitOptions.RemoveEmptyEntries); #endif if (entry.Length < 2) continue; // Extract the two required pieces string entryDevicePath = entry[0]; string entryMountPoint = entry[1]; // If the drive does not match if (!string.Equals(devicePath, entryDevicePath)) continue; // Return the mount point return entryMountPoint; } // No matches were found return null; } catch { // Absorb errors for now return null; } } /// /// Read the device size from sysfs (reported in 512-byte sectors) and convert to bytes /// /// Path to the device directory under the sysfs block root /// The device size in bytes, 0 on error public static long ReadUnixBlockDeviceSize(string sysBlockEntry) { string sizePath = Path.Combine(sysBlockEntry, "size"); try { if (!File.Exists(sizePath)) return 0; string sizeString = File.ReadAllText(sizePath).Trim(); if (long.TryParse(sizeString, out long sectors) && sectors > 0) return sectors * 512; } catch { // Unreadable size; report it as unknown } return 0; } /// /// Read the device size from sysfs (reported in 512-byte sectors) and convert to bytes /// /// Path to the device directory under the sysfs block root /// The device size in bytes, 0 on error public static int ReadUnixBlockDeviceType(string sysGenericEntry) { #if NET20 || NET35 string typePath = Path.Combine(Path.Combine(sysGenericEntry, "device"), "type"); #else string typePath = Path.Combine(sysGenericEntry, "device", "type"); #endif try { if (!File.Exists(typePath)) return 0; string typeString = File.ReadAllText(typePath).Trim(); if (int.TryParse(typeString, out int scsiType)) return scsiType; } catch { // Unreadable type; report it as unknown } return 0; } /// /// Read the sysfs "removable" flag for a block device /// /// Path to the device directory under the sysfs block root /// True when the device reports itself as removable, false otherwise public static bool ReadUnixRemovableFlag(string sysBlockEntry) { string removablePath = Path.Combine(sysBlockEntry, "removable"); try { if (!File.Exists(removablePath)) return false; string removableString = File.ReadAllText(removablePath).Trim(); return removableString == "1"; } catch { return false; } } /// /// Map a generic (/dev/sg*) device to its corresponding block (/dev/sr*) device /// /// Device path to check /// The mapped device path on success, null otherwise internal static string? GetMappedDeviceForGeneric(string? devicePath) { // If the device path is invalid if (devicePath is null) return null; #pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. // If the device does not match the generic pattern if (!Regex.IsMatch(devicePath, @"/dev/sg.+")) return null; #pragma warning restore SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. // Get the SCSI name from the path by trimming `/dev/` #if NETFRAMEWORK || NETSTANDARD string scsiName = devicePath.Substring(5); #else string scsiName = devicePath[5..]; #endif // Build the path to the block directory string blockDirectory = $"/sys/class/scsi_generic/{scsiName}/device/block/"; if (!Directory.Exists(blockDirectory)) return null; // If there are no block mappings var blockDevices = IOExtensions.SafeGetDirectories(blockDirectory); if (blockDevices.Length == 0) return null; // Take the first found block mapping and reformat to `/dev/sr*` string mappedDevice = blockDevices[0]; #if NETFRAMEWORK || NETSTANDARD mappedDevice = mappedDevice.Substring($"/sys/class/scsi_generic/{scsiName}/device/block/".Length); #else mappedDevice = mappedDevice[$"/sys/class/scsi_generic/{scsiName}/device/block/".Length..]; #endif return $"/dev/{mappedDevice}"; } /// /// Check that a device node name is the given prefix followed by one or more digits /// (e.g. "sr0", "sg12"), rejecting names with trailing or embedded non-digit characters. /// /// Device node name to check /// Required leading text (e.g. "sr", "sg") /// True when the name is the prefix followed only by digits internal static bool HasDeviceIndexSuffix(string name, string prefix) { // If the prefix is longer than the name if (name.Length <= prefix.Length) return false; // If the prefix is invalid for the provided name if (!name.StartsWith(prefix, StringComparison.Ordinal)) return false; // Check that all following characters are numeric for (int i = prefix.Length; i < name.Length; i++) { if (!char.IsDigit(name[i])) return false; } return true; } #endregion } }