From 29d38e70d98a17f817e5fcdee7b3c5946a640fc1 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Jan 2026 11:19:10 -0500 Subject: [PATCH 1/6] refactor(memoryinfo): use ini_parse_quantity for memory limit parsing Refactor `MemoryInfo` to leverage PHP's (8.2+) native `ini_parse_quantity()` for parsing and converting `memory_limit` values. - Eliminates custom logic and unnecessary checks. - Fully aligns parsing with PHP's internal parsing behavior. - Fixes a bug where our support for `float` in this particular context was too generous, because on 32-bit systems PHP's own parsing of this value wraps. By switching to this new function we are literally checking from PHP's perspective rather than checking the user's intent. Checking actual effective config value is more appropriate in this context. Signed-off-by: Josh --- lib/private/MemoryInfo.php | 50 ++++++++------------------------------ 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/lib/private/MemoryInfo.php b/lib/private/MemoryInfo.php index 9b5c709b4a94e..a2f5c659dfa0b 100644 --- a/lib/private/MemoryInfo.php +++ b/lib/private/MemoryInfo.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** - * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2018-2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -28,47 +28,17 @@ public function isMemoryLimitSufficient(): bool { } /** - * Returns the php memory limit. + * Returns the interpreted (by PHP) memory limit in bytes. * - * @return int|float The memory limit in bytes. + * @return int The memory limit in bytes, or -1 if unlimited. + * @throws \InvalidArgumentException If the memory_limit value cannot be parsed. */ - public function getMemoryLimit(): int|float { - $iniValue = trim(ini_get('memory_limit')); - if ($iniValue === '-1') { - return -1; - } elseif (is_numeric($iniValue)) { - return Util::numericToNumber($iniValue); - } else { - return $this->memoryLimitToBytes($iniValue); + public function getMemoryLimit(): int { + $iniValue = ini_get('memory_limit'); + $bytes = ini_parse_quantity($iniValue); // can emit E_WARNING + if ($bytes === false) { + throw new \InvalidArgumentException($iniValue . ' is not a valid memory limit value (in memory_limit ini directive)'); } - } - - /** - * Converts the ini memory limit to bytes. - * - * @param string $memoryLimit The "memory_limit" ini value - */ - private function memoryLimitToBytes(string $memoryLimit): int|float { - $last = strtolower(substr($memoryLimit, -1)); - $number = substr($memoryLimit, 0, -1); - if (is_numeric($number)) { - $memoryLimit = Util::numericToNumber($number); - } else { - throw new \InvalidArgumentException($number . ' is not a valid numeric string (in memory_limit ini directive)'); - } - - // intended fall through - switch ($last) { - case 'g': - $memoryLimit *= 1024; - // no break - case 'm': - $memoryLimit *= 1024; - // no break - case 'k': - $memoryLimit *= 1024; - } - - return $memoryLimit; + return $bytes; } } From 0f20dea799ce6835ac9f805641398b6fa4377cda Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 21 Jan 2026 11:26:21 -0500 Subject: [PATCH 2/6] chore: drop unused import from MemoryInfo Signed-off-by: Josh --- lib/private/MemoryInfo.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/private/MemoryInfo.php b/lib/private/MemoryInfo.php index a2f5c659dfa0b..ace19f6e3472f 100644 --- a/lib/private/MemoryInfo.php +++ b/lib/private/MemoryInfo.php @@ -9,8 +9,6 @@ namespace OC; -use OCP\Util; - /** * Helper class that covers memory info. */ From 8a3e3eec85c490f681c27f1aa06b9ed770330ffa Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 22 Jan 2026 11:12:32 -0500 Subject: [PATCH 3/6] test(MemoryInfo): Add real 32-bit test data Signed-off-by: Josh --- tests/lib/MemoryInfoTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/lib/MemoryInfoTest.php b/tests/lib/MemoryInfoTest.php index 707b214912286..b70e9fc6f442b 100644 --- a/tests/lib/MemoryInfoTest.php +++ b/tests/lib/MemoryInfoTest.php @@ -32,12 +32,14 @@ public function restoreMemoryInfoIniSetting() { } public static function getMemoryLimitTestData(): array { + // On 32-bit, '2G' should overflows to 0; on 64-bit, it is 2147483648 + $twoG = PHP_INT_SIZE === 4 ? 0 : 2147483648; return [ 'unlimited' => ['-1', -1,], '524288000 bytes' => ['524288000', 524288000,], '500M' => ['500M', 524288000,], '512000K' => ['512000K', 524288000,], - '2G' => ['2G', 2147483648,], + '2G' => ['2G', $twoG,], ]; } From ab1c561044d9332e61b27388306ac34047b3727c Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 22 Jan 2026 23:22:29 -0500 Subject: [PATCH 4/6] chore: fixup wrapped value Signed-off-by: Josh --- tests/lib/MemoryInfoTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/MemoryInfoTest.php b/tests/lib/MemoryInfoTest.php index b70e9fc6f442b..5b3adc9c969d4 100644 --- a/tests/lib/MemoryInfoTest.php +++ b/tests/lib/MemoryInfoTest.php @@ -32,8 +32,8 @@ public function restoreMemoryInfoIniSetting() { } public static function getMemoryLimitTestData(): array { - // On 32-bit, '2G' should overflows to 0; on 64-bit, it is 2147483648 - $twoG = PHP_INT_SIZE === 4 ? 0 : 2147483648; + // On 32-bit, '2G' should overflow; on 64-bit, it'll be 2147483648 + $twoG = PHP_INT_SIZE === 4 ? -2147483648 : 2147483648; return [ 'unlimited' => ['-1', -1,], '524288000 bytes' => ['524288000', 524288000,], From dfbcdca32d24e99516d8e0dc555afb6e88c87aa5 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 22 Jan 2026 23:47:21 -0500 Subject: [PATCH 5/6] chore: implement E_WARNING/return handling in MemoryInfo Signed-off-by: Josh --- lib/private/MemoryInfo.php | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/private/MemoryInfo.php b/lib/private/MemoryInfo.php index ace19f6e3472f..ba794d6d15970 100644 --- a/lib/private/MemoryInfo.php +++ b/lib/private/MemoryInfo.php @@ -16,9 +16,10 @@ class MemoryInfo { public const RECOMMENDED_MEMORY_LIMIT = 512 * 1024 * 1024; /** - * Tests if the memory limit is greater or equal the recommended value. + * Tests if the memory limit is compliant with the recommendation value. * * @return bool + * @throws \InvalidArgumentException (via $this->getMemoryLimit()) if the memory limit is misconfigured. */ public function isMemoryLimitSufficient(): bool { $memoryLimit = $this->getMemoryLimit(); @@ -29,14 +30,22 @@ public function isMemoryLimitSufficient(): bool { * Returns the interpreted (by PHP) memory limit in bytes. * * @return int The memory limit in bytes, or -1 if unlimited. - * @throws \InvalidArgumentException If the memory_limit value cannot be parsed. + * @throws \InvalidArgumentException if the memory_limit value cannot be parsed. */ public function getMemoryLimit(): int { $iniValue = ini_get('memory_limit'); - $bytes = ini_parse_quantity($iniValue); // can emit E_WARNING - if ($bytes === false) { - throw new \InvalidArgumentException($iniValue . ' is not a valid memory limit value (in memory_limit ini directive)'); + + set_error_handler(function($errno, $errstr) { + throw new \ErrorException($errstr, 0, $errno); + }); + + try { + $bytes = ini_parse_quantity($iniValue); // can emit E_WARNING + return $bytes; + } catch (\ErrorException $e) { + throw new \InvalidArgumentException('Error parsing PHP memory_limit ini directive: ' . $e->getMessage()); + } finally { + restore_error_handler(); } - return $bytes; } } From 6dfcc4177b3775d11618c71a8dca0e47b3dcaf85 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 23 Jan 2026 10:09:10 -0500 Subject: [PATCH 6/6] refactor(util): use native parsing, standardize unlimited handling for upload limits Signed-off-by: Josh --- lib/public/Util.php | 91 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 20 deletions(-) diff --git a/lib/public/Util.php b/lib/public/Util.php index 70a862880f19d..6532c518aba76 100644 --- a/lib/public/Util.php +++ b/lib/public/Util.php @@ -525,24 +525,40 @@ public static function recursiveArraySearch($haystack, $needle, $index = null) { } /** - * calculates the maximum upload size respecting system settings, free space and user quota + * Calculates the maximum allowed upload size for a directory, + * considering available free space/quota and PHP upload size configuration. * - * @param string $dir the current folder where the user currently operates - * @param int|float|null $free the number of bytes free on the storage holding $dir, if not set this will be received from the storage directly - * @return int|float number of bytes representing + * @param string $dir Directory to check free space for. + * @param int|float|null $free Optional: bytes free on storage; retrieved if not provided. + * @return int|float Max upload size in bytes, or -1 if unlimited. * @since 5.0.0 */ public static function maxUploadFilesize(string $dir, int|float|null $free = null): int|float { if (is_null($free) || $free < 0) { $free = self::freeSpace($dir); } - return min($free, self::uploadLimit()); + $limit = self::uploadLimit(); + + if ($free === -1 && $limit === -1) { + return -1; + } + + // If only one is unlimited, return the finite value + if ($free === -1) { + return $limit; + } + if ($limit === -1) { + return $free; + } + // Both finite + return min($free, $limit); } /** - * Calculate free space left within user quota - * @param string $dir the current folder where the user currently operates - * @return int|float number of bytes representing + * Gets the available free space (respecting user quota) for the given directory. + * + * @param string $dir Directory to compute free space for. + * @return int|float Bytes free (0 or more), or INF for unlimited. * @since 7.0.0 */ public static function freeSpace(string $dir): int|float { @@ -556,22 +572,57 @@ public static function freeSpace(string $dir): int|float { } /** - * Calculate PHP upload limit + * Calculates the effective PHP upload limit in bytes, based on ini settings. * - * @return int|float number of bytes representing + * Returns the strictest limit from `upload_max_filesize` and `post_max_size`, + * treating 0 and -1 as unlimited. Returns -1 if both limits are unlimited. + * + * @return int Effective upload limit in bytes, or -1 if unlimited. + * @throws \InvalidArgumentException If unable to parse the ini values. * @since 7.0.0 */ - public static function uploadLimit(): int|float { - $ini = Server::get(IniGetWrapper::class); - $upload_max_filesize = self::computerFileSize($ini->get('upload_max_filesize')) ?: 0; - $post_max_size = self::computerFileSize($ini->get('post_max_size')) ?: 0; - if ($upload_max_filesize === 0 && $post_max_size === 0) { - return INF; - } elseif ($upload_max_filesize === 0 || $post_max_size === 0) { - return max($upload_max_filesize, $post_max_size); //only the non 0 value counts - } else { - return min($upload_max_filesize, $post_max_size); + public static function uploadLimit(): int { + $uploadMaxString = ini_get('upload_max_filesize'); + $postMaxString = ini_get('post_max_size'); + + set_error_handler(function($errno, $errstr) { + throw new \ErrorException($errstr, 0, $errno); + }); + + try { + $uploadMax = ini_parse_quantity($uploadMaxString); + $postMax = ini_parse_quantity($postMaxString); + } catch (\ErrorException $e) { + throw new \InvalidArgumentException( + 'Error parsing PHP upload_max_filesize or post_max_size ini directive: ' . $e->getMessage() + ); + } finally { + restore_error_handler(); + } + + // For these config parameters, both -1 and 0 mean unlimited in modern PHP, so normalize 0 to -1. + $uploadMax = ($uploadMax === 0) ? -1 : $uploadMax; + $postMax = ($postMax === 0) ? -1 : $postMax; + + if ($uploadMax === -1 && $postMax === -1) { + return -1; // unlimited + } + + if ($uploadMax > $postMax && $postMax !== -1) { + // Optional: Log a warning if upload_max_filesize exceeds post_max_size (or a setup check) + // Actual upload limit will be restricted by post_max_size + return $postMax; + } + + if ($uploadMax === -1) { + return $postMax; } + if ($postMax === -1) { + return $uploadMax; + } + + // Normal case: return the most restrictive (lowest) finite limit + return min($uploadMax, $postMax); } /**