diff --git a/lib/OperatingSystems/FreeBSD.php b/lib/OperatingSystems/FreeBSD.php index 624e3758..3c0f18fc 100644 --- a/lib/OperatingSystems/FreeBSD.php +++ b/lib/OperatingSystems/FreeBSD.php @@ -189,8 +189,8 @@ public function getDiskInfo(): array { $matches = []; // `df -P` prints one record per line with the mount point last, so the mount // point is everything after the capacity column. Used and Capacity are loose - // because nothing reads them and `df` prints "-" for filesystems without - // usage numbers. + // because nothing reads them and `df` prints "-" in those two columns when + // it has no usage numbers. $pattern = '/^(?\S+)[ \t]+(?\S+)[ \t]+(?\d+)[ \t]+(?\S+)[ \t]+(?\d+)[ \t]+(?\S+)[ \t]+(?\S.*?)[ \t\r]*$/m'; $result = preg_match_all($pattern, $disks, $matches); @@ -204,13 +204,17 @@ public function getDiskInfo(): array { continue; } + $blocks = (int)$matches['Blocks'][$i]; + $available = (int)$matches['Available'][$i]; + // Clamped: more available than total would give a negative size + $used = max(0, $blocks - $available); + $disk = new Disk(); $disk->setDevice($filesystem); $disk->setFs($matches['Type'][$i]); - $used = (int)((int)$matches['Blocks'][$i] - (int)$matches['Available'][$i]); $disk->setUsed((int)ceil($used / 1024)); - $disk->setAvailable((int)floor((int)$matches['Available'][$i] / 1024)); - $disk->setPercent(round(($used * 100 / (int)$matches['Blocks'][$i]), 2) . '%'); + $disk->setAvailable((int)floor($available / 1024)); + $disk->setPercent($blocks > 0 ? (string)round($used * 100 / $blocks, 2) . '%' : '0%'); $disk->setMount($matches['Mounted'][$i]); $data[] = $disk; diff --git a/lib/OperatingSystems/Linux.php b/lib/OperatingSystems/Linux.php index 16c4293a..d1a3c35b 100644 --- a/lib/OperatingSystems/Linux.php +++ b/lib/OperatingSystems/Linux.php @@ -208,8 +208,8 @@ public function getDiskInfo(): array { $matches = []; // `df -P` prints one record per line with the mount point last, so the mount // point is everything after the capacity column. Used and Capacity are loose - // because nothing reads them and `df` prints "-" for filesystems without - // usage numbers. + // because nothing reads them and `df` prints "-" in those two columns when + // it has no usage numbers. $pattern = '/^(?\S+)[ \t]+(?\S+)[ \t]+(?\d+)[ \t]+(?\S+)[ \t]+(?\d+)[ \t]+(?\S+)[ \t]+(?\S.*?)[ \t\r]*$/m'; $result = preg_match_all($pattern, $disks, $matches); @@ -224,13 +224,18 @@ public function getDiskInfo(): array { continue; } + $blocks = (int)$matches['Blocks'][$i]; + $available = (int)$matches['Available'][$i]; + // Clamped: more available than total would give a negative size + $used = max(0, $blocks - $available); + $disk = new Disk(); $disk->setDevice($filesystem); $disk->setFs($matches['Type'][$i]); - $used = (int)((int)$matches['Blocks'][$i] - (int)$matches['Available'][$i]); $disk->setUsed((int)ceil($used / 1024)); - $disk->setAvailable((int)floor((int)$matches['Available'][$i] / 1024)); - $disk->setPercent(round(($used * 100 / (int)$matches['Blocks'][$i]), 2) . '%'); + $disk->setAvailable((int)floor($available / 1024)); + // busybox df lists zero-block filesystems, coreutils does not + $disk->setPercent($blocks > 0 ? (string)round($used * 100 / $blocks, 2) . '%' : '0%'); $disk->setMount($matches['Mounted'][$i]); $data[] = $disk; diff --git a/tests/data/freebsd_df_tp_edge_cases b/tests/data/freebsd_df_tp_edge_cases new file mode 100644 index 00000000..d17364ee --- /dev/null +++ b/tests/data/freebsd_df_tp_edge_cases @@ -0,0 +1,4 @@ +Filesystem Type 1024-blocks Used Available Capacity Mounted on +/dev/gpt/rootfs ufs 56422560 6205468 47321220 12% / +empty nullfs 0 0 0 0% /mnt/empty +inverted zfs 1000 0 2000 0% /tank diff --git a/tests/data/linux_df_tp_edge_cases b/tests/data/linux_df_tp_edge_cases new file mode 100644 index 00000000..7708e1d6 --- /dev/null +++ b/tests/data/linux_df_tp_edge_cases @@ -0,0 +1,4 @@ +Filesystem Type 1024-blocks Used Available Capacity Mounted on +/dev/vda1 ext4 56422560 6205468 47321220 12% / +nsfs nsfs 0 0 0 0% /run/snapd/ns +inverted zfs 1000 0 2000 0% /tank diff --git a/tests/lib/FreeBSDTest.php b/tests/lib/FreeBSDTest.php index b89bdfe7..4ea26ab1 100644 --- a/tests/lib/FreeBSDTest.php +++ b/tests/lib/FreeBSDTest.php @@ -200,6 +200,40 @@ public function testGetNetworkInterfacesError(): void { $this->assertEquals($expected, $actual); } + public function testGetDiskInfoZeroCapacityAndInvertedUsage(): void { + $this->os->method('executeCommand') + ->with('df -TPk') + ->willReturn(file_get_contents(__DIR__ . '/../data/freebsd_df_tp_edge_cases')); + + $disk1 = new Disk(); + $disk1->setDevice('/dev/gpt/rootfs'); + $disk1->setFs('ufs'); + $disk1->setUsed(8889); + $disk1->setAvailable(46212); + $disk1->setPercent('16.13%'); + $disk1->setMount('/'); + + // Zero blocks: used to be a DivisionByZeroError + $disk2 = new Disk(); + $disk2->setDevice('empty'); + $disk2->setFs('nullfs'); + $disk2->setUsed(0); + $disk2->setAvailable(0); + $disk2->setPercent('0%'); + $disk2->setMount('/mnt/empty'); + + // More available than total: used is clamped + $disk3 = new Disk(); + $disk3->setDevice('inverted'); + $disk3->setFs('zfs'); + $disk3->setUsed(0); + $disk3->setAvailable(1); + $disk3->setPercent('0%'); + $disk3->setMount('/tank'); + + $this->assertEquals([$disk1, $disk2, $disk3], $this->os->getDiskInfo()); + } + public function testSupported(): void { $this->assertFalse($this->os->supported()); } diff --git a/tests/lib/LinuxTest.php b/tests/lib/LinuxTest.php index ae5f0b35..acfe1e5a 100644 --- a/tests/lib/LinuxTest.php +++ b/tests/lib/LinuxTest.php @@ -235,17 +235,51 @@ public function testGetDiskInfo(): void { $this->assertEquals([$disk1, $disk2, $disk3, $disk4, $disk5, $disk6, $disk7], $this->os->getDiskInfo()); } + public function testGetDiskInfoZeroCapacityAndInvertedUsage(): void { + $this->os->method('executeCommand') + ->with('df -TPk') + ->willReturn(file_get_contents(__DIR__ . '/../data/linux_df_tp_edge_cases')); + + $disk1 = new Disk(); + $disk1->setDevice('/dev/vda1'); + $disk1->setFs('ext4'); + $disk1->setUsed(8889); + $disk1->setAvailable(46212); + $disk1->setPercent('16.13%'); + $disk1->setMount('/'); + + // Zero blocks: used to be a DivisionByZeroError + $disk2 = new Disk(); + $disk2->setDevice('nsfs'); + $disk2->setFs('nsfs'); + $disk2->setUsed(0); + $disk2->setAvailable(0); + $disk2->setPercent('0%'); + $disk2->setMount('/run/snapd/ns'); + + // More available than total: used is clamped + $disk3 = new Disk(); + $disk3->setDevice('inverted'); + $disk3->setFs('zfs'); + $disk3->setUsed(0); + $disk3->setAvailable(1); + $disk3->setPercent('0%'); + $disk3->setMount('/tank'); + + $this->assertEquals([$disk1, $disk2, $disk3], $this->os->getDiskInfo()); + } + public function testGetDiskInfoNoCommandOutput(): void { $this->os->method('executeCommand') - ->with('df -TP') - ->willThrowException(new RuntimeException('No output for command "df -TP"')); + ->with('df -TPk') + ->willThrowException(new RuntimeException('No output for command "df -TPk"')); $this->assertEquals([], $this->os->getDiskInfo()); } public function testGetDiskInfoInvalidCommandOutput(): void { $this->os->method('executeCommand') - ->with('df -TP') + ->with('df -TPk') ->willReturn('invalid_data'); $this->assertEquals([], $this->os->getDiskInfo()); diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 726ebdb3..442eafb1 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -52,7 +52,6 @@ - @@ -62,7 +61,6 @@ -