From f7f17d0d36b670f11cf4c788d47757b431446cc1 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:13:52 +0000 Subject: [PATCH] feat: importexceptions --- .../{ => Message}/DecodingException.php | 2 +- .../PartialDecodingException.php | 2 +- src/DNS/Exception/Zone/ImportException.php | 25 +++++++++++ src/DNS/Message.php | 4 +- src/DNS/Message/Domain.php | 2 +- src/DNS/Message/Header.php | 2 +- src/DNS/Message/Question.php | 2 +- src/DNS/Message/Record.php | 2 +- src/DNS/Server.php | 2 +- src/DNS/Validator/DNS.php | 8 ++-- src/DNS/Zone/File.php | 41 +++++++++++-------- tests/unit/DNS/Message/DomainTest.php | 2 +- tests/unit/DNS/Message/HeaderTest.php | 2 +- tests/unit/DNS/MessageTest.php | 10 +++-- tests/unit/DNS/Zone/FileTest.php | 28 ++++++------- 15 files changed, 83 insertions(+), 51 deletions(-) rename src/DNS/Exception/{ => Message}/DecodingException.php (75%) rename src/DNS/Exception/{ => Message}/PartialDecodingException.php (93%) create mode 100644 src/DNS/Exception/Zone/ImportException.php diff --git a/src/DNS/Exception/DecodingException.php b/src/DNS/Exception/Message/DecodingException.php similarity index 75% rename from src/DNS/Exception/DecodingException.php rename to src/DNS/Exception/Message/DecodingException.php index 9cb887e..b5935d0 100644 --- a/src/DNS/Exception/DecodingException.php +++ b/src/DNS/Exception/Message/DecodingException.php @@ -1,6 +1,6 @@ content; + } +} diff --git a/src/DNS/Message.php b/src/DNS/Message.php index ee595b3..0d74123 100644 --- a/src/DNS/Message.php +++ b/src/DNS/Message.php @@ -2,8 +2,8 @@ namespace Utopia\DNS; -use Utopia\DNS\Exception\DecodingException; -use Utopia\DNS\Exception\PartialDecodingException; +use Utopia\DNS\Exception\Message\DecodingException; +use Utopia\DNS\Exception\Message\PartialDecodingException; use Utopia\DNS\Message\Header; use Utopia\DNS\Message\Question; use Utopia\DNS\Message\Record; diff --git a/src/DNS/Message/Domain.php b/src/DNS/Message/Domain.php index 5b27187..58481bb 100644 --- a/src/DNS/Message/Domain.php +++ b/src/DNS/Message/Domain.php @@ -2,7 +2,7 @@ namespace Utopia\DNS\Message; -use Utopia\DNS\Exception\DecodingException; +use Utopia\DNS\Exception\Message\DecodingException; final readonly class Domain { diff --git a/src/DNS/Message/Header.php b/src/DNS/Message/Header.php index 6ef863e..66a4220 100644 --- a/src/DNS/Message/Header.php +++ b/src/DNS/Message/Header.php @@ -2,7 +2,7 @@ namespace Utopia\DNS\Message; -use Utopia\DNS\Exception\DecodingException; +use Utopia\DNS\Exception\Message\DecodingException; final readonly class Header { diff --git a/src/DNS/Message/Question.php b/src/DNS/Message/Question.php index 4c6c440..e928067 100644 --- a/src/DNS/Message/Question.php +++ b/src/DNS/Message/Question.php @@ -2,7 +2,7 @@ namespace Utopia\DNS\Message; -use Utopia\DNS\Exception\DecodingException; +use Utopia\DNS\Exception\Message\DecodingException; final readonly class Question { diff --git a/src/DNS/Message/Record.php b/src/DNS/Message/Record.php index 3bab79a..650ab4e 100644 --- a/src/DNS/Message/Record.php +++ b/src/DNS/Message/Record.php @@ -2,7 +2,7 @@ namespace Utopia\DNS\Message; -use Utopia\DNS\Exception\DecodingException; +use Utopia\DNS\Exception\Message\DecodingException; /** * A DNS record. diff --git a/src/DNS/Server.php b/src/DNS/Server.php index c6d2d51..cfa9f2d 100644 --- a/src/DNS/Server.php +++ b/src/DNS/Server.php @@ -4,7 +4,7 @@ use Throwable; use Utopia\Console; -use Utopia\DNS\Exception\PartialDecodingException; +use Utopia\DNS\Exception\Message\PartialDecodingException; use Utopia\Telemetry\Adapter as Telemetry; use Utopia\Telemetry\Adapter\None as NoTelemetry; use Utopia\Telemetry\Counter; diff --git a/src/DNS/Validator/DNS.php b/src/DNS/Validator/DNS.php index 1da5b26..ded9829 100644 --- a/src/DNS/Validator/DNS.php +++ b/src/DNS/Validator/DNS.php @@ -11,10 +11,10 @@ class DNS extends Validator { - protected const FAILURE_REASON_QUERY = 'DNS query failed.'; - protected const FAILURE_REASON_INTERNAL = 'Internal error occurred.'; - protected const FAILURE_REASON_UNKNOWN = ''; - protected const DEFAULT_DNS_SERVER = '8.8.8.8'; + protected const string FAILURE_REASON_QUERY = 'DNS query failed.'; + protected const string FAILURE_REASON_INTERNAL = 'Internal error occurred.'; + protected const string FAILURE_REASON_UNKNOWN = ''; + protected const string DEFAULT_DNS_SERVER = '8.8.8.8'; // Memory from isValid to be used in getDescription /** diff --git a/src/DNS/Zone/File.php b/src/DNS/Zone/File.php index f5f7237..ec989bd 100644 --- a/src/DNS/Zone/File.php +++ b/src/DNS/Zone/File.php @@ -3,6 +3,7 @@ namespace Utopia\DNS\Zone; use InvalidArgumentException; +use Utopia\DNS\Exception\Zone\ImportException; use Utopia\DNS\Message\Record; use Utopia\DNS\Zone; @@ -31,7 +32,7 @@ * @param string|null $defaultOrigin Default origin if $ORIGIN is not specified * @param int $defaultTTL Default TTL if not specified (default: 3600) * - * @throws InvalidArgumentException + * @throws ImportException */ public static function import(string $content, ?string $defaultOrigin = null, int $defaultTTL = 3600): Zone { @@ -46,7 +47,7 @@ public static function import(string $content, ?string $defaultOrigin = null, in if ($defaultOrigin !== null) { $origin = self::canonicalizeName($defaultOrigin); if ($origin === null) { - throw new InvalidArgumentException('Default origin must not be empty'); + throw new ImportException($content, 'Default origin must not be empty'); } $zoneName = $origin; $zoneNameFromDefault = true; @@ -62,11 +63,16 @@ public static function import(string $content, ?string $defaultOrigin = null, in } // Directives - $directive = self::handleDirectives($line, $origin, $lastTTL); + try { + $directive = self::handleDirectives($line, $origin, $lastTTL); + } catch (InvalidArgumentException $e) { + throw new ImportException($content, $e->getMessage(), previous: $e); + } + if ($directive !== null) { if ($directive === 'origin') { if ($origin === null) { - throw new InvalidArgumentException('$ORIGIN directive must not be empty'); + throw new ImportException($content, '$ORIGIN directive must not be empty'); } if ($zoneName === null || $zoneNameFromDefault) { $zoneName = $origin; @@ -84,7 +90,11 @@ public static function import(string $content, ?string $defaultOrigin = null, in $ownerOmitted = $line[0] === ' ' || $line[0] === "\t"; $line = ltrim($line); - $rr = self::parseResourceRecord($line, $origin, $lastOwner, $lastTTL, $lastClass, $ownerOmitted, $num); + try { + $rr = self::parseResourceRecord($line, $origin, $lastOwner, $lastTTL, $lastClass, $ownerOmitted, $num); + } catch (InvalidArgumentException $e) { + throw new ImportException($content, $e->getMessage(), previous: $e); + } // Update state from parsed RR $lastOwner = $rr['name']; @@ -104,7 +114,7 @@ class: $rr['class'], if ($rr['type'] === Record::TYPE_SOA) { if ($soa !== null) { - throw new InvalidArgumentException("Multiple SOA records found (line $num)."); + throw new ImportException($content, "Multiple SOA records found (line $num)."); } $soa = $record; continue; @@ -114,11 +124,11 @@ class: $rr['class'], } if ($soa === null) { - throw new InvalidArgumentException('No SOA record found in zone file'); + throw new ImportException($content, 'No SOA record found in zone file'); } if ($zoneName === null) { - throw new InvalidArgumentException('Unable to determine zone name: provide an $ORIGIN directive or defaultOrigin.'); + throw new ImportException($content, 'Unable to determine zone name: provide an $ORIGIN directive or defaultOrigin.'); } return new Zone($zoneName, $records, $soa); @@ -357,7 +367,11 @@ private static function parseResourceRecord( } $typeString = strtoupper($tokens[$i]); - $type = self::parseType($typeString); + $type = Record::typeNameToCode($typeString); + if ($type === null) { + throw new InvalidArgumentException("Invalid record type '$typeString' (line $lineNum)."); + } + $i++; $rdataTokens = array_slice($tokens, $i); @@ -598,15 +612,6 @@ private static function relativizeDomainName(string $name, string $origin): stri return $name; } - private static function parseType(string $typeString): int - { - $type = Record::typeNameToCode($typeString); - if ($type === null) { - throw new InvalidArgumentException("Unknown record type '$typeString'"); - } - return $type; - } - private static function getTypeString(int $type): string { return Record::typeCodeToName($type) ?? 'TYPE' . $type; diff --git a/tests/unit/DNS/Message/DomainTest.php b/tests/unit/DNS/Message/DomainTest.php index d646e8c..7cdca30 100644 --- a/tests/unit/DNS/Message/DomainTest.php +++ b/tests/unit/DNS/Message/DomainTest.php @@ -3,7 +3,7 @@ namespace Tests\Unit\Utopia\DNS\Message; use PHPUnit\Framework\TestCase; -use Utopia\DNS\Exception\DecodingException; +use Utopia\DNS\Exception\Message\DecodingException; use Utopia\DNS\Message\Domain; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/unit/DNS/Message/HeaderTest.php b/tests/unit/DNS/Message/HeaderTest.php index 8eb35d9..643e265 100644 --- a/tests/unit/DNS/Message/HeaderTest.php +++ b/tests/unit/DNS/Message/HeaderTest.php @@ -3,7 +3,7 @@ namespace Tests\Unit\Utopia\DNS\Message; use PHPUnit\Framework\TestCase; -use Utopia\DNS\Exception\DecodingException; +use Utopia\DNS\Exception\Message\DecodingException; use Utopia\DNS\Message\Header; final class HeaderTest extends TestCase diff --git a/tests/unit/DNS/MessageTest.php b/tests/unit/DNS/MessageTest.php index ef886e9..07af773 100644 --- a/tests/unit/DNS/MessageTest.php +++ b/tests/unit/DNS/MessageTest.php @@ -3,6 +3,8 @@ namespace Tests\Unit\Utopia\DNS; use PHPUnit\Framework\TestCase; +use Utopia\DNS\Exception\Message\DecodingException; +use Utopia\DNS\Exception\Message\PartialDecodingException; use Utopia\DNS\Message; use Utopia\DNS\Message\Header; use Utopia\DNS\Message\Record; @@ -189,7 +191,7 @@ public function testDecodeThrowsWhenPacketTooShort(): void { $packet = "\x00\x01\x00"; // shorter than 12-byte DNS header - $this->expectException(\Utopia\DNS\Exception\DecodingException::class); + $this->expectException(DecodingException::class); $this->expectExceptionMessage('Invalid DNS response: header too short'); Message::decode($packet); @@ -206,7 +208,7 @@ public function testDecodeThrowsPartialDecodingOnTruncatedQuestion(): void try { Message::decode($packet); $this->fail('Expected PartialDecodingException'); - } catch (\Utopia\DNS\Exception\PartialDecodingException $e) { + } catch (PartialDecodingException $e) { $header = $e->getHeader(); $this->assertSame(0x1234, $header->id); $this->assertSame(1, $header->questionCount); @@ -229,7 +231,7 @@ public function testDecodeThrowsPartialDecodingOnTruncatedAnswer(): void try { Message::decode($header . $question . $answer); $this->fail('Expected PartialDecodingException'); - } catch (\Utopia\DNS\Exception\PartialDecodingException $e) { + } catch (PartialDecodingException $e) { $this->assertSame(0xABCD, $e->getHeader()->id); $this->assertSame('RDATA exceeds packet bounds', $e->getMessage()); } @@ -249,7 +251,7 @@ public function testDecodeThrowsPartialDecodingOnExtraBytes(): void "\x5D\xB8\xD8\x22" . "\xFF"; // extra byte - $this->expectException(\Utopia\DNS\Exception\PartialDecodingException::class); + $this->expectException(PartialDecodingException::class); $this->expectExceptionMessage('Invalid packet length'); Message::decode($message); diff --git a/tests/unit/DNS/Zone/FileTest.php b/tests/unit/DNS/Zone/FileTest.php index 58ab409..c51783e 100644 --- a/tests/unit/DNS/Zone/FileTest.php +++ b/tests/unit/DNS/Zone/FileTest.php @@ -2,8 +2,8 @@ namespace Tests\Utopia\DNS\Zone; -use InvalidArgumentException; use PHPUnit\Framework\TestCase; +use Utopia\DNS\Exception\Zone\ImportException; use Utopia\DNS\Message\Record; use Utopia\DNS\Zone; use Utopia\DNS\Zone\File; @@ -103,7 +103,7 @@ public function testImportValidZoneWithDirectives(): void public function testImportFailsWithUnsupportedDirective(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(ImportException::class); $this->expectExceptionMessage('$INCLUDE directive is not supported'); File::import(<<expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Unknown record type 'BADTYPE'"); + $this->expectException(ImportException::class); + $this->expectExceptionMessage("Invalid record type 'BADTYPE' (line 3)."); $soa = self::DEFAULT_SOA; $contents = <<expectException(InvalidArgumentException::class); + $this->expectException(ImportException::class); $this->expectExceptionMessage('MX requires numeric priority and exchange'); $soa = self::DEFAULT_SOA; @@ -144,7 +144,7 @@ public function testImportFailsWhenMxPriorityMissing(): void public function testImportFailsWhenSrvFieldsMissing(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(ImportException::class); $this->expectExceptionMessage('SRV requires priority, weight, port, target'); $soa = self::DEFAULT_SOA; @@ -204,7 +204,7 @@ public function testImportUsesDefaultOriginWhenDirectiveMissing(): void public function testImportFailsWhenSoaDataMissing(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(ImportException::class); $this->expectExceptionMessage('SOA requires MNAME, RNAME, SERIAL, REFRESH, RETRY, EXPIRE, MINIMUM'); File::import('@ IN SOA ns1.example.com. admin.example.com.', 'example.com'); @@ -592,7 +592,7 @@ public function testImportExportRoundTripForCaa(): void public function testImportCaaMissingQuotedValueFails(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(ImportException::class); $this->expectExceptionMessageMatches('/CAA value must be quoted/'); $contents = sprintf( @@ -623,7 +623,7 @@ public function testImportPtrWithReverseOrigin(): void public function testImportFailsWithDuplicateSoa(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(ImportException::class); $this->expectExceptionMessage('Multiple SOA records found'); $contents = <<<'ZONE' @@ -637,8 +637,8 @@ public function testImportFailsWithDuplicateSoa(): void public function testImportRejectsTtlWithSuffix(): void { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Unknown record type '1H'"); + $this->expectException(ImportException::class); + $this->expectExceptionMessage("Invalid record type '1H' (line 3)."); $contents = sprintf( <<<'ZONE' @@ -705,7 +705,7 @@ public function testImportTxtThreeDigitEscapeConsumesOnlyThreeDigits(): void public function testImportFailsWhenSoaHasTooFewFields(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(ImportException::class); $this->expectExceptionMessage('SOA requires MNAME, RNAME, SERIAL, REFRESH, RETRY, EXPIRE, MINIMUM'); File::import('@ IN SOA ns1.example.com. admin.example.com. 2025011801 7200 3600', 'example.com'); @@ -713,7 +713,7 @@ public function testImportFailsWhenSoaHasTooFewFields(): void public function testImportFailsWithoutSoa(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(ImportException::class); $this->expectExceptionMessage('No SOA record found in zone file'); File::import("www IN A 192.168.1.10\n", 'example.com'); @@ -721,7 +721,7 @@ public function testImportFailsWithoutSoa(): void public function testImportFailsWhenOwnerOmittedWithoutContext(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(ImportException::class); $this->expectExceptionMessage('Owner omitted but no previous owner available'); File::import(<<