From 590de8ce375782b99dfde990da92f78b24c63d93 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:31:00 +0200 Subject: [PATCH] feat(docker): enhance Docker image handling with new validation and parsing logic - Refactored DockerImage component to use separate properties for image name, tag, and SHA256 digest. - Introduced DockerImageFormat validation rule to enforce correct image format. - Updated DockerImageParser to handle new parsing logic for image tags and SHA256 hashes. - Enhanced UI to separate input fields for image name, tag, and SHA256 digest, improving user experience. - Added comprehensive tests for DockerImageParser to ensure accurate parsing and validation of image formats. --- app/Livewire/Project/New/DockerImage.php | 29 ++- app/Rules/DockerImageFormat.php | 41 ++++ app/Services/DockerImageParser.php | 37 ++-- .../project/new/docker-image.blade.php | 25 ++- templates/service-templates-latest.json | 2 +- templates/service-templates.json | 2 +- tests/Unit/DockerImageParserTest.php | 206 +++++++++--------- 7 files changed, 209 insertions(+), 133 deletions(-) create mode 100644 app/Rules/DockerImageFormat.php diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index c1403a819..dd928b5e1 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -12,7 +12,11 @@ class DockerImage extends Component { - public string $dockerImage = ''; + public string $imageName = ''; + + public string $imageTag = ''; + + public string $imageSha256 = ''; public array $parameters; @@ -27,11 +31,30 @@ public function mount() public function submit() { $this->validate([ - 'dockerImage' => 'required', + 'imageName' => ['required', 'string'], + 'imageTag' => ['nullable', 'string', 'regex:/^[a-z0-9][a-z0-9._-]*$/i'], + 'imageSha256' => ['nullable', 'string', 'regex:/^[a-f0-9]{64}$/i'], ]); + // Validate that either tag or sha256 is provided, but not both + if ($this->imageTag && $this->imageSha256) { + $this->addError('imageTag', 'Provide either a tag or SHA256 digest, not both.'); + $this->addError('imageSha256', 'Provide either a tag or SHA256 digest, not both.'); + + return; + } + + // Build the full Docker image string + if ($this->imageSha256) { + $dockerImage = $this->imageName.'@sha256:'.$this->imageSha256; + } elseif ($this->imageTag) { + $dockerImage = $this->imageName.':'.$this->imageTag; + } else { + $dockerImage = $this->imageName.':latest'; + } + $parser = new DockerImageParser; - $parser->parse($this->dockerImage); + $parser->parse($dockerImage); $destination_uuid = $this->query['destination']; $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); diff --git a/app/Rules/DockerImageFormat.php b/app/Rules/DockerImageFormat.php new file mode 100644 index 000000000..a6a78a76c --- /dev/null +++ b/app/Rules/DockerImageFormat.php @@ -0,0 +1,41 @@ + strrpos($imageString, '/'))) { - $mainPart = substr($imageString, 0, $lastColon); - $this->tag = substr($imageString, $lastColon + 1); - - // Check if the tag is a SHA256 hash - $this->isImageHash = $this->isSha256Hash($this->tag); + // Check for @sha256: format first (e.g., nginx@sha256:abc123...) + if (preg_match('/^(.+)@sha256:([a-f0-9]{64})$/i', $imageString, $matches)) { + $mainPart = $matches[1]; + $this->tag = $matches[2]; + $this->isImageHash = true; } else { - $mainPart = $imageString; - $this->tag = 'latest'; - $this->isImageHash = false; + // Split by : to handle the tag, but be careful with registry ports + $lastColon = strrpos($imageString, ':'); + $hasSlash = str_contains($imageString, '/'); + + // If the last colon appears after the last slash, it's a tag + // Otherwise it might be a port in the registry URL + if ($lastColon !== false && (! $hasSlash || $lastColon > strrpos($imageString, '/'))) { + $mainPart = substr($imageString, 0, $lastColon); + $this->tag = substr($imageString, $lastColon + 1); + + // Check if the tag is a SHA256 hash + $this->isImageHash = $this->isSha256Hash($this->tag); + } else { + $mainPart = $imageString; + $this->tag = 'latest'; + $this->isImageHash = false; + } } // Split the main part by / to handle registry and image name diff --git a/resources/views/livewire/project/new/docker-image.blade.php b/resources/views/livewire/project/new/docker-image.blade.php index af1005a88..54c175b82 100644 --- a/resources/views/livewire/project/new/docker-image.blade.php +++ b/resources/views/livewire/project/new/docker-image.blade.php @@ -1,4 +1,4 @@ -
+

Create a new Application

You can deploy an existing Docker Image from any Registry.
@@ -7,12 +7,23 @@ Save
- + +
+ + + +
diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 7d8cc6af1..6d61b12ef 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -951,7 +951,7 @@ "ente-photos": { "documentation": "https://help.ente.io/self-hosting/installation/compose?utm_source=coolify.io", "slogan": "Ente Photos is a fully open source, End to End Encrypted alternative to Google Photos and Apple Photos.", - "compose": "IyBkb2N1bWVudGF0aW9uOiBodHRwczovL2hlbHAuZW50ZS5pby9zZWxmLWhvc3RpbmcvaW5zdGFsbGF0aW9uL2NvbXBvc2UKIyBzbG9nYW46IEVudGUgUGhvdG9zIGlzIGEgZnVsbHkgb3BlbiBzb3VyY2UsIEVuZCB0byBFbmQgRW5jcnlwdGVkIGFsdGVybmF0aXZlIHRvIEdvb2dsZSBQaG90b3MgYW5kIEFwcGxlIFBob3Rvcy4KIyBjYXRlZ29yeTogbWVkaWEKIyB0YWdzOiBwaG90b3MsZ2FsbGVyeSxiYWNrdXAsZW5jcnlwdGlvbixwcml2YWN5LHNlbGYtaG9zdGVkLGdvb2dsZS1waG90b3MsYWx0ZXJuYXRpdmUKIyBsb2dvOiBzdmdzL2VudGUtcGhvdG9zLnN2ZwojIHBvcnQ6IDgwODAKCnNlcnZpY2VzOgogIG11c2V1bToKICAgIGltYWdlOiBnaGNyLmlvL2VudGUtaW8vc2VydmVyOmxhdGVzdAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTVVTRVVNXzgwODAKICAgICAgIyBEYXRhYmFzZSBjb25maWd1cmF0aW9uCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3JlcwogICAgICAtIFBPU1RHUkVTX1BPUlQ9NTQzMgogICAgICAtIFBPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWVudGVfZGJ9CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30KICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9CiAgICAgICMgUzMvTWluSU8gY29uZmlndXJhdGlvbgogICAgICAtIFMzX0FSRV9MT0NBTF9CVUNLRVRTPXRydWUKICAgICAgLSBTM19VU0VfUEFUSF9TVFlMRV9VUkxTPXRydWUKICAgICAgLSBTM19CMl9FVV9DRU5fS0VZPSR7U0VSVklDRV9VU0VSX01JTklPfQogICAgICAtIFMzX0IyX0VVX0NFTl9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfQogICAgICAtIFMzX0IyX0VVX0NFTl9FTkRQT0lOVD0ke1NFUlZJQ0VfVVJMX01JTklPXzMyMDB9CiAgICAgIC0gUzNfQjJfRVVfQ0VOX1JFR0lPTj1ldS1jZW50cmFsLTIKICAgICAgLSBTM19CMl9FVV9DRU5fQlVDS0VUPWIyLWV1LWNlbgogICAgICAjIFNlY3VyaXR5IGtleXMKICAgICAgLSBFTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfRU5DUllQVElPTn0KICAgICAgLSBIQVNIX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfSEFTSH0KICAgICAgLSBKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF82NF9KV1R9CiAgICAgICMgQWRtaW4gcGVybWlzc2lvbnMgKGdyYW50cyBmaXJzdCBhY2NvdW50IGFkbWluIGFjY2VzcykKICAgICAgLSBFTlRFX0lOVEVSTkFMX0FETUlOPTE1ODA1NTk5NjIzODY0MzgKICAgICAgIyBBcHAgVVJMcyAob3B0aW9uYWwgLSBmb3Igd2ViIGludGVyZmFjZSkKICAgICAgLSBBUFBTX1BVQkxJQ19BTEJVTVM9JHtBUFBTX1BVQkxJQ19BTEJVTVM6LX0KICAgICAgLSBBUFBTX0NBU1Q9JHtBUFBTX0NBU1Q6LX0KICAgICAgLSBBUFBTX0FDQ09VTlRTPSR7QVBQU19BQ0NPVU5UUzotfQogICAgdm9sdW1lczoKICAgICAgLSBtdXNldW0tZGF0YTovZGF0YQogICAgICAtIG11c2V1bS1jb25maWc6L2NvbmZpZwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgbWluaW86CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiBbIkNNRCIsICJjdXJsIiwgIi1mIiwgImh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9waW5nIl0KICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwoKICBwb3N0Z3JlczoKICAgIGltYWdlOiBwb3N0Z3JlczoxNS1hbHBpbmUKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9CiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfQogICAgICAtIFBPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWVudGVfZGJ9CiAgICB2b2x1bWVzOgogICAgICAtIHBvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogWyJDTUQtU0hFTEwiLCAicGdfaXNyZWFkeSAtVSAke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30gLWQgJHtQT1NUR1JFU19EQjotZW50ZV9kYn0iXQogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUKCiAgbWluaW86CiAgICBpbWFnZTogcXVheS5pby9taW5pby9taW5pbzpsYXRlc3QKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01JTklPXzMyMDAKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUlOSU99CiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99CiAgICBjb21tYW5kOiBzZXJ2ZXIgL2RhdGEgLS1hZGRyZXNzICI6MzIwMCIgLS1jb25zb2xlLWFkZHJlc3MgIjozMjAxIgogICAgdm9sdW1lczoKICAgICAgLSBtaW5pby1kYXRhOi9kYXRhCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogWyJDTUQiLCAibWMiLCAicmVhZHkiLCAibG9jYWwiXQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCgogIG1pbmlvLWluaXQ6CiAgICBpbWFnZTogbWluaW8vbWM6bGF0ZXN0CiAgICBkZXBlbmRzX29uOgogICAgICBtaW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfQogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfQogICAgZW50cnlwb2ludDogPgogICAgICAvYmluL3NoIC1jICIKICAgICAgbWMgYWxpYXMgc2V0IG1pbmlvIGh0dHA6Ly9taW5pbzozMjAwICQke01JTklPX1JPT1RfVVNFUn0gJCR7TUlOSU9fUk9PVF9QQVNTV09SRH07CiAgICAgIG1jIG1iIG1pbmlvL2IyLWV1LWNlbiAtLWlnbm9yZS1leGlzdGluZzsKICAgICAgbWMgbWIgbWluaW8vd2FzYWJpLWV1LWNlbnRyYWwtMi12MyAtLWlnbm9yZS1leGlzdGluZzsKICAgICAgbWMgbWIgbWluaW8vc2N3LWV1LWZyLXYzIC0taWdub3JlLWV4aXN0aW5nOwogICAgICBlY2hvICdNaW5JTyBidWNrZXRzIGNyZWF0ZWQgc3VjY2Vzc2Z1bGx5JzsKICAgICAgIgo=", + "compose": "c2VydmljZXM6CiAgbXVzZXVtOgogICAgaW1hZ2U6ICdnaGNyLmlvL2VudGUtaW8vc2VydmVyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01VU0VVTV84MDgwCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3JlcwogICAgICAtIFBPU1RHUkVTX1BPUlQ9NTQzMgogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1lbnRlX2RifScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gUzNfQVJFX0xPQ0FMX0JVQ0tFVFM9dHJ1ZQogICAgICAtIFMzX1VTRV9QQVRIX1NUWUxFX1VSTFM9dHJ1ZQogICAgICAtICdTM19CMl9FVV9DRU5fS0VZPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnUzNfQjJfRVVfQ0VOX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgICAtICdTM19CMl9FVV9DRU5fRU5EUE9JTlQ9JHtTRVJWSUNFX1VSTF9NSU5JT18zMjAwfScKICAgICAgLSBTM19CMl9FVV9DRU5fUkVHSU9OPWV1LWNlbnRyYWwtMgogICAgICAtIFMzX0IyX0VVX0NFTl9CVUNLRVQ9YjItZXUtY2VuCiAgICAgIC0gJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9FTkNSWVBUSU9OfScKICAgICAgLSAnSEFTSF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0hBU0h9JwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF82NF9KV1R9JwogICAgICAtIEVOVEVfSU5URVJOQUxfQURNSU49MTU4MDU1OTk2MjM4NjQzOAogICAgICAtICdBUFBTX1BVQkxJQ19BTEJVTVM9JHtBUFBTX1BVQkxJQ19BTEJVTVM6LX0nCiAgICAgIC0gJ0FQUFNfQ0FTVD0ke0FQUFNfQ0FTVDotfScKICAgICAgLSAnQVBQU19BQ0NPVU5UUz0ke0FQUFNfQUNDT1VOVFM6LX0nCiAgICB2b2x1bWVzOgogICAgICAtICdtdXNldW0tZGF0YTovZGF0YScKICAgICAgLSAnbXVzZXVtLWNvbmZpZzovY29uZmlnJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgbWluaW86CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwL3BpbmcnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUtYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1lbnRlX2RifScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfSAtZCAke1BPU1RHUkVTX0RCOi1lbnRlX2RifScKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTUlOSU9fMzIwMAogICAgICAtICdNSU5JT19ST09UX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdNSU5JT19ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICBjb21tYW5kOiAnc2VydmVyIC9kYXRhIC0tYWRkcmVzcyAiOjMyMDAiIC0tY29uc29sZS1hZGRyZXNzICI6MzIwMSInCiAgICB2b2x1bWVzOgogICAgICAtICdtaW5pby1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG1jCiAgICAgICAgLSByZWFkeQogICAgICAgIC0gbG9jYWwKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvLWluaXQ6CiAgICBpbWFnZTogJ21pbmlvL21jOmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIG1pbmlvOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgZW50cnlwb2ludDogIi9iaW4vc2ggLWMgXCIgbWMgYWxpYXMgc2V0IG1pbmlvIGh0dHA6Ly9taW5pbzozMjAwICQke01JTklPX1JPT1RfVVNFUn0gJCR7TUlOSU9fUk9PVF9QQVNTV09SRH07IG1jIG1iIG1pbmlvL2IyLWV1LWNlbiAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vd2FzYWJpLWV1LWNlbnRyYWwtMi12MyAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vc2N3LWV1LWZyLXYzIC0taWdub3JlLWV4aXN0aW5nOyBlY2hvICdNaW5JTyBidWNrZXRzIGNyZWF0ZWQgc3VjY2Vzc2Z1bGx5JzsgXCJcbiIK", "tags": [ "photos", "gallery", diff --git a/templates/service-templates.json b/templates/service-templates.json index 7d05eb1ef..4ae838a89 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -951,7 +951,7 @@ "ente-photos": { "documentation": "https://help.ente.io/self-hosting/installation/compose?utm_source=coolify.io", "slogan": "Ente Photos is a fully open source, End to End Encrypted alternative to Google Photos and Apple Photos.", - "compose": "IyBkb2N1bWVudGF0aW9uOiBodHRwczovL2hlbHAuZW50ZS5pby9zZWxmLWhvc3RpbmcvaW5zdGFsbGF0aW9uL2NvbXBvc2UKIyBzbG9nYW46IEVudGUgUGhvdG9zIGlzIGEgZnVsbHkgb3BlbiBzb3VyY2UsIEVuZCB0byBFbmQgRW5jcnlwdGVkIGFsdGVybmF0aXZlIHRvIEdvb2dsZSBQaG90b3MgYW5kIEFwcGxlIFBob3Rvcy4KIyBjYXRlZ29yeTogbWVkaWEKIyB0YWdzOiBwaG90b3MsZ2FsbGVyeSxiYWNrdXAsZW5jcnlwdGlvbixwcml2YWN5LHNlbGYtaG9zdGVkLGdvb2dsZS1waG90b3MsYWx0ZXJuYXRpdmUKIyBsb2dvOiBzdmdzL2VudGUtcGhvdG9zLnN2ZwojIHBvcnQ6IDgwODAKCnNlcnZpY2VzOgogIG11c2V1bToKICAgIGltYWdlOiBnaGNyLmlvL2VudGUtaW8vc2VydmVyOmxhdGVzdAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTVVTRVVNXzgwODAKICAgICAgIyBEYXRhYmFzZSBjb25maWd1cmF0aW9uCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3JlcwogICAgICAtIFBPU1RHUkVTX1BPUlQ9NTQzMgogICAgICAtIFBPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWVudGVfZGJ9CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30KICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9CiAgICAgICMgUzMvTWluSU8gY29uZmlndXJhdGlvbgogICAgICAtIFMzX0FSRV9MT0NBTF9CVUNLRVRTPXRydWUKICAgICAgLSBTM19VU0VfUEFUSF9TVFlMRV9VUkxTPXRydWUKICAgICAgLSBTM19CMl9FVV9DRU5fS0VZPSR7U0VSVklDRV9VU0VSX01JTklPfQogICAgICAtIFMzX0IyX0VVX0NFTl9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfQogICAgICAtIFMzX0IyX0VVX0NFTl9FTkRQT0lOVD0ke1NFUlZJQ0VfVVJMX01JTklPXzMyMDB9CiAgICAgIC0gUzNfQjJfRVVfQ0VOX1JFR0lPTj1ldS1jZW50cmFsLTIKICAgICAgLSBTM19CMl9FVV9DRU5fQlVDS0VUPWIyLWV1LWNlbgogICAgICAjIFNlY3VyaXR5IGtleXMKICAgICAgLSBFTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfRU5DUllQVElPTn0KICAgICAgLSBIQVNIX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfSEFTSH0KICAgICAgLSBKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF82NF9KV1R9CiAgICAgICMgQWRtaW4gcGVybWlzc2lvbnMgKGdyYW50cyBmaXJzdCBhY2NvdW50IGFkbWluIGFjY2VzcykKICAgICAgLSBFTlRFX0lOVEVSTkFMX0FETUlOPTE1ODA1NTk5NjIzODY0MzgKICAgICAgIyBBcHAgVVJMcyAob3B0aW9uYWwgLSBmb3Igd2ViIGludGVyZmFjZSkKICAgICAgLSBBUFBTX1BVQkxJQ19BTEJVTVM9JHtBUFBTX1BVQkxJQ19BTEJVTVM6LX0KICAgICAgLSBBUFBTX0NBU1Q9JHtBUFBTX0NBU1Q6LX0KICAgICAgLSBBUFBTX0FDQ09VTlRTPSR7QVBQU19BQ0NPVU5UUzotfQogICAgdm9sdW1lczoKICAgICAgLSBtdXNldW0tZGF0YTovZGF0YQogICAgICAtIG11c2V1bS1jb25maWc6L2NvbmZpZwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgbWluaW86CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiBbIkNNRCIsICJjdXJsIiwgIi1mIiwgImh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9waW5nIl0KICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwoKICBwb3N0Z3JlczoKICAgIGltYWdlOiBwb3N0Z3JlczoxNS1hbHBpbmUKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9CiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfQogICAgICAtIFBPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWVudGVfZGJ9CiAgICB2b2x1bWVzOgogICAgICAtIHBvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogWyJDTUQtU0hFTEwiLCAicGdfaXNyZWFkeSAtVSAke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30gLWQgJHtQT1NUR1JFU19EQjotZW50ZV9kYn0iXQogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUKCiAgbWluaW86CiAgICBpbWFnZTogcXVheS5pby9taW5pby9taW5pbzpsYXRlc3QKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01JTklPXzMyMDAKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUlOSU99CiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99CiAgICBjb21tYW5kOiBzZXJ2ZXIgL2RhdGEgLS1hZGRyZXNzICI6MzIwMCIgLS1jb25zb2xlLWFkZHJlc3MgIjozMjAxIgogICAgdm9sdW1lczoKICAgICAgLSBtaW5pby1kYXRhOi9kYXRhCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogWyJDTUQiLCAibWMiLCAicmVhZHkiLCAibG9jYWwiXQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCgogIG1pbmlvLWluaXQ6CiAgICBpbWFnZTogbWluaW8vbWM6bGF0ZXN0CiAgICBkZXBlbmRzX29uOgogICAgICBtaW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfQogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfQogICAgZW50cnlwb2ludDogPgogICAgICAvYmluL3NoIC1jICIKICAgICAgbWMgYWxpYXMgc2V0IG1pbmlvIGh0dHA6Ly9taW5pbzozMjAwICQke01JTklPX1JPT1RfVVNFUn0gJCR7TUlOSU9fUk9PVF9QQVNTV09SRH07CiAgICAgIG1jIG1iIG1pbmlvL2IyLWV1LWNlbiAtLWlnbm9yZS1leGlzdGluZzsKICAgICAgbWMgbWIgbWluaW8vd2FzYWJpLWV1LWNlbnRyYWwtMi12MyAtLWlnbm9yZS1leGlzdGluZzsKICAgICAgbWMgbWIgbWluaW8vc2N3LWV1LWZyLXYzIC0taWdub3JlLWV4aXN0aW5nOwogICAgICBlY2hvICdNaW5JTyBidWNrZXRzIGNyZWF0ZWQgc3VjY2Vzc2Z1bGx5JzsKICAgICAgIgo=", + "compose": "c2VydmljZXM6CiAgbXVzZXVtOgogICAgaW1hZ2U6ICdnaGNyLmlvL2VudGUtaW8vc2VydmVyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NVVNFVU1fODA4MAogICAgICAtIFBPU1RHUkVTX0hPU1Q9cG9zdGdyZXMKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotZW50ZV9kYn0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtIFMzX0FSRV9MT0NBTF9CVUNLRVRTPXRydWUKICAgICAgLSBTM19VU0VfUEFUSF9TVFlMRV9VUkxTPXRydWUKICAgICAgLSAnUzNfQjJfRVVfQ0VOX0tFWT0ke1NFUlZJQ0VfVVNFUl9NSU5JT30nCiAgICAgIC0gJ1MzX0IyX0VVX0NFTl9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgICAgLSAnUzNfQjJfRVVfQ0VOX0VORFBPSU5UPSR7U0VSVklDRV9GUUROX01JTklPXzMyMDB9JwogICAgICAtIFMzX0IyX0VVX0NFTl9SRUdJT049ZXUtY2VudHJhbC0yCiAgICAgIC0gUzNfQjJfRVVfQ0VOX0JVQ0tFVD1iMi1ldS1jZW4KICAgICAgLSAnRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT059JwogICAgICAtICdIQVNIX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfSEFTSH0nCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0pXVH0nCiAgICAgIC0gRU5URV9JTlRFUk5BTF9BRE1JTj0xNTgwNTU5OTYyMzg2NDM4CiAgICAgIC0gJ0FQUFNfUFVCTElDX0FMQlVNUz0ke0FQUFNfUFVCTElDX0FMQlVNUzotfScKICAgICAgLSAnQVBQU19DQVNUPSR7QVBQU19DQVNUOi19JwogICAgICAtICdBUFBTX0FDQ09VTlRTPSR7QVBQU19BQ0NPVU5UUzotfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ211c2V1bS1kYXRhOi9kYXRhJwogICAgICAtICdtdXNldW0tY29uZmlnOi9jb25maWcnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBtaW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNS1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWVudGVfZGJ9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9IC1kICR7UE9TVEdSRVNfREI6LWVudGVfZGJ9JwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUKICBtaW5pbzoKICAgIGltYWdlOiAncXVheS5pby9taW5pby9taW5pbzpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTUlOSU9fMzIwMAogICAgICAtICdNSU5JT19ST09UX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdNSU5JT19ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICBjb21tYW5kOiAnc2VydmVyIC9kYXRhIC0tYWRkcmVzcyAiOjMyMDAiIC0tY29uc29sZS1hZGRyZXNzICI6MzIwMSInCiAgICB2b2x1bWVzOgogICAgICAtICdtaW5pby1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG1jCiAgICAgICAgLSByZWFkeQogICAgICAgIC0gbG9jYWwKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvLWluaXQ6CiAgICBpbWFnZTogJ21pbmlvL21jOmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIG1pbmlvOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgZW50cnlwb2ludDogIi9iaW4vc2ggLWMgXCIgbWMgYWxpYXMgc2V0IG1pbmlvIGh0dHA6Ly9taW5pbzozMjAwICQke01JTklPX1JPT1RfVVNFUn0gJCR7TUlOSU9fUk9PVF9QQVNTV09SRH07IG1jIG1iIG1pbmlvL2IyLWV1LWNlbiAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vd2FzYWJpLWV1LWNlbnRyYWwtMi12MyAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vc2N3LWV1LWZyLXYzIC0taWdub3JlLWV4aXN0aW5nOyBlY2hvICdNaW5JTyBidWNrZXRzIGNyZWF0ZWQgc3VjY2Vzc2Z1bGx5JzsgXCJcbiIK", "tags": [ "photos", "gallery", diff --git a/tests/Unit/DockerImageParserTest.php b/tests/Unit/DockerImageParserTest.php index f41a9b170..6102a90b2 100644 --- a/tests/Unit/DockerImageParserTest.php +++ b/tests/Unit/DockerImageParserTest.php @@ -1,115 +1,109 @@ parse('nginx:latest'); + + expect($parser->getImageName())->toBe('nginx') + ->and($parser->getTag())->toBe('latest') + ->and($parser->isImageHash())->toBeFalse() + ->and($parser->toString())->toBe('nginx:latest'); +}); + +it('parses image with sha256 hash using colon format', function () { + $parser = new DockerImageParser; + $hash = '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0'; + $parser->parse("ghcr.io/benjaminehowe/rail-disruptions:{$hash}"); + + expect($parser->getFullImageNameWithoutTag())->toBe('ghcr.io/benjaminehowe/rail-disruptions') + ->and($parser->getTag())->toBe($hash) + ->and($parser->isImageHash())->toBeTrue() + ->and($parser->toString())->toBe("ghcr.io/benjaminehowe/rail-disruptions@sha256:{$hash}") + ->and($parser->getFullImageNameWithHash())->toBe("ghcr.io/benjaminehowe/rail-disruptions@sha256:{$hash}"); +}); + +it('parses image with sha256 hash using at sign format', function () { + $parser = new DockerImageParser; + $hash = '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0'; + $parser->parse("nginx@sha256:{$hash}"); + + expect($parser->getImageName())->toBe('nginx') + ->and($parser->getTag())->toBe($hash) + ->and($parser->isImageHash())->toBeTrue() + ->and($parser->toString())->toBe("nginx@sha256:{$hash}") + ->and($parser->getFullImageNameWithHash())->toBe("nginx@sha256:{$hash}"); +}); + +it('parses registry image with hash', function () { + $parser = new DockerImageParser; + $hash = 'abc123def456789abcdef123456789abcdef123456789abcdef123456789abc1'; + $parser->parse("docker.io/library/nginx:{$hash}"); + + expect($parser->getFullImageNameWithoutTag())->toBe('docker.io/library/nginx') + ->and($parser->getTag())->toBe($hash) + ->and($parser->isImageHash())->toBeTrue() + ->and($parser->toString())->toBe("docker.io/library/nginx@sha256:{$hash}"); +}); + +it('parses image without tag defaults to latest', function () { + $parser = new DockerImageParser; + $parser->parse('nginx'); + + expect($parser->getImageName())->toBe('nginx') + ->and($parser->getTag())->toBe('latest') + ->and($parser->isImageHash())->toBeFalse() + ->and($parser->toString())->toBe('nginx:latest'); +}); + +it('parses registry with port', function () { + $parser = new DockerImageParser; + $parser->parse('registry.example.com:5000/myapp:latest'); + + expect($parser->getFullImageNameWithoutTag())->toBe('registry.example.com:5000/myapp') + ->and($parser->getTag())->toBe('latest') + ->and($parser->isImageHash())->toBeFalse(); +}); + +it('parses registry with port and hash', function () { + $parser = new DockerImageParser; + $hash = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + $parser->parse("registry.example.com:5000/myapp:{$hash}"); + + expect($parser->getFullImageNameWithoutTag())->toBe('registry.example.com:5000/myapp') + ->and($parser->getTag())->toBe($hash) + ->and($parser->isImageHash())->toBeTrue() + ->and($parser->toString())->toBe("registry.example.com:5000/myapp@sha256:{$hash}"); +}); + +it('identifies valid sha256 hashes', function () { + $validHashes = [ + '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0', + '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + ]; + + foreach ($validHashes as $hash) { $parser = new DockerImageParser; - $parser->parse('nginx:latest'); - - $this->assertEquals('nginx', $parser->getImageName()); - $this->assertEquals('latest', $parser->getTag()); - $this->assertFalse($parser->isImageHash()); - $this->assertEquals('nginx:latest', $parser->toString()); + $parser->parse("image:{$hash}"); + expect($parser->isImageHash())->toBeTrue("Hash {$hash} should be recognized as valid SHA256"); } +}); - public function test_parses_image_with_sha256_hash() - { +it('identifies invalid sha256 hashes', function () { + $invalidHashes = [ + 'latest', + 'v1.2.3', + 'abc123', // too short + '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf', // too short + '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf00', // too long + '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cfg0', // invalid char + ]; + + foreach ($invalidHashes as $hash) { $parser = new DockerImageParser; - $hash = '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0'; - $parser->parse("ghcr.io/benjaminehowe/rail-disruptions:{$hash}"); - - $this->assertEquals('ghcr.io/benjaminehowe/rail-disruptions', $parser->getFullImageNameWithoutTag()); - $this->assertEquals($hash, $parser->getTag()); - $this->assertTrue($parser->isImageHash()); - $this->assertEquals("ghcr.io/benjaminehowe/rail-disruptions@sha256:{$hash}", $parser->toString()); - $this->assertEquals("ghcr.io/benjaminehowe/rail-disruptions@sha256:{$hash}", $parser->getFullImageNameWithHash()); + $parser->parse("image:{$hash}"); + expect($parser->isImageHash())->toBeFalse("Hash {$hash} should not be recognized as valid SHA256"); } - - public function test_parses_registry_image_with_hash() - { - $parser = new DockerImageParser; - $hash = 'abc123def456789abcdef123456789abcdef123456789abcdef123456789abc1'; - $parser->parse("docker.io/library/nginx:{$hash}"); - - $this->assertEquals('docker.io/library/nginx', $parser->getFullImageNameWithoutTag()); - $this->assertEquals($hash, $parser->getTag()); - $this->assertTrue($parser->isImageHash()); - $this->assertEquals("docker.io/library/nginx@sha256:{$hash}", $parser->toString()); - } - - public function test_parses_image_without_tag_defaults_to_latest() - { - $parser = new DockerImageParser; - $parser->parse('nginx'); - - $this->assertEquals('nginx', $parser->getImageName()); - $this->assertEquals('latest', $parser->getTag()); - $this->assertFalse($parser->isImageHash()); - $this->assertEquals('nginx:latest', $parser->toString()); - } - - public function test_parses_registry_with_port() - { - $parser = new DockerImageParser; - $parser->parse('registry.example.com:5000/myapp:latest'); - - $this->assertEquals('registry.example.com:5000/myapp', $parser->getFullImageNameWithoutTag()); - $this->assertEquals('latest', $parser->getTag()); - $this->assertFalse($parser->isImageHash()); - } - - public function test_parses_registry_with_port_and_hash() - { - $parser = new DockerImageParser; - $hash = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; - $parser->parse("registry.example.com:5000/myapp:{$hash}"); - - $this->assertEquals('registry.example.com:5000/myapp', $parser->getFullImageNameWithoutTag()); - $this->assertEquals($hash, $parser->getTag()); - $this->assertTrue($parser->isImageHash()); - $this->assertEquals("registry.example.com:5000/myapp@sha256:{$hash}", $parser->toString()); - } - - public function test_identifies_valid_sha256_hashes() - { - $parser = new DockerImageParser; - - // Valid SHA256 hashes - $validHashes = [ - '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0', - '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', - 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', - ]; - - foreach ($validHashes as $hash) { - $parser->parse("image:{$hash}"); - $this->assertTrue($parser->isImageHash(), "Hash {$hash} should be recognized as valid SHA256"); - } - } - - public function test_identifies_invalid_sha256_hashes() - { - $parser = new DockerImageParser; - - // Invalid SHA256 hashes - $invalidHashes = [ - 'latest', - 'v1.2.3', - 'abc123', // too short - '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf', // too short - '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf00', // too long - '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cfg0', // invalid char - ]; - - foreach ($invalidHashes as $hash) { - $parser->parse("image:{$hash}"); - $this->assertFalse($parser->isImageHash(), "Hash {$hash} should not be recognized as valid SHA256"); - } - } -} +});