From 369def079a2e25bdf230e12a6075bfb12f461c00 Mon Sep 17 00:00:00 2001
From: W0rma <beck.worma@gmail.com>
Date: Sat, 11 Jan 2025 15:40:54 +0100
Subject: [PATCH] fix: Succeed format validation if the type is not in the set
 of given instance types (#773)

See https://json-schema.org/draft-04/draft-fge-json-schema-validation-00#rfc.section.7.1: If the type of the instance to validate is not in this set, validation for this format attribute and instance SHOULD succeed.
---
 CHANGELOG.md                                  |  1 +
 .../Constraints/FormatConstraint.php          | 29 ++++++++++++++-----
 tests/Constraints/FormatTest.php              | 29 +++++++++++++++++++
 3 files changed, 52 insertions(+), 7 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6025c397..ef7b256d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fix wrong combined paths when traversing upward, fixes #557 ([#652](https://github.com/jsonrainbow/json-schema/pull/652))
 - Correct PHPStan baseline ([#764](https://github.com/jsonrainbow/json-schema/pull/764))
 - Correct spacing issue in `README.md` ([#763](https://github.com/jsonrainbow/json-schema/pull/763))
+- Format attribute: do not validate data instances that aren't the instance type to validate ([#773](https://github.com/jsonrainbow/json-schema/pull/773))
 
 ### Changed
 - Bump to minimum PHP 7.2 ([#746](https://github.com/jsonrainbow/json-schema/pull/746))
diff --git a/src/JsonSchema/Constraints/FormatConstraint.php b/src/JsonSchema/Constraints/FormatConstraint.php
index 482ba668..d17dd396 100644
--- a/src/JsonSchema/Constraints/FormatConstraint.php
+++ b/src/JsonSchema/Constraints/FormatConstraint.php
@@ -35,7 +35,7 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
 
         switch ($schema->format) {
             case 'date':
-                if (!$date = $this->validateDateTime($element, 'Y-m-d')) {
+                if (is_string($element) && !$date = $this->validateDateTime($element, 'Y-m-d')) {
                     $this->addError(ConstraintError::FORMAT_DATE(), $path, [
                             'date' => $element,
                             'format' => $schema->format
@@ -45,7 +45,7 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
                 break;
 
             case 'time':
-                if (!$this->validateDateTime($element, 'H:i:s')) {
+                if (is_string($element) && !$this->validateDateTime($element, 'H:i:s')) {
                     $this->addError(ConstraintError::FORMAT_TIME(), $path, [
                             'time' => json_encode($element),
                             'format' => $schema->format,
@@ -55,7 +55,7 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
                 break;
 
             case 'date-time':
-                if (null === Rfc3339::createFromString($element)) {
+                if (is_string($element) && null === Rfc3339::createFromString($element)) {
                     $this->addError(ConstraintError::FORMAT_DATE_TIME(), $path, [
                             'dateTime' => json_encode($element),
                             'format' => $schema->format
@@ -101,14 +101,14 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
                 break;
 
             case 'uri':
-                if (null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) {
+                if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) {
                     $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]);
                 }
                 break;
 
             case 'uriref':
             case 'uri-reference':
-                if (null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) {
+                if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) {
                     // FILTER_VALIDATE_URL does not conform to RFC-3986, and cannot handle relative URLs, but
                     // the json-schema spec uses RFC-3986, so need a bit of hackery to properly validate them.
                     // See https://tools.ietf.org/html/rfc3986#section-4.2 for additional information.
@@ -133,6 +133,9 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
                 break;
 
             case 'email':
+                if (!is_string($element)) {
+                    break;
+                }
                 $filterFlags = FILTER_NULL_ON_FAILURE;
                 if (defined('FILTER_FLAG_EMAIL_UNICODE')) {
                     // Only available from PHP >= 7.1.0, so ignore it for coverage checks
@@ -145,13 +148,13 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
 
             case 'ip-address':
             case 'ipv4':
-                if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) {
+                if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) {
                     $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]);
                 }
                 break;
 
             case 'ipv6':
-                if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) {
+                if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) {
                     $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]);
                 }
                 break;
@@ -191,11 +194,19 @@ protected function validateDateTime($datetime, $format)
 
     protected function validateRegex($regex)
     {
+        if (!is_string($regex)) {
+            return true;
+        }
+
         return false !== @preg_match(self::jsonPatternToPhpRegex($regex), '');
     }
 
     protected function validateColor($color)
     {
+        if (!is_string($color)) {
+            return true;
+        }
+
         if (in_array(strtolower($color), ['aqua', 'black', 'blue', 'fuchsia',
             'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange', 'purple',
             'red', 'silver', 'teal', 'white', 'yellow'])) {
@@ -220,6 +231,10 @@ protected function validatePhone($phone)
 
     protected function validateHostname($host)
     {
+        if (!is_string($host)) {
+            return true;
+        }
+
         $hostnameRegex = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/i';
 
         return preg_match($hostnameRegex, $host);
diff --git a/tests/Constraints/FormatTest.php b/tests/Constraints/FormatTest.php
index 2d91e5fe..25eab1c0 100644
--- a/tests/Constraints/FormatTest.php
+++ b/tests/Constraints/FormatTest.php
@@ -100,10 +100,14 @@ public function getValidFormats(): array
         return [
             ['2001-01-23', 'date'],
             ['2000-02-29', 'date'],
+            [42, 'date'],
+            [4.2, 'date'],
 
             ['12:22:01', 'time'],
             ['00:00:00', 'time'],
             ['23:59:59', 'time'],
+            [42, 'time'],
+            [4.2, 'time'],
 
             ['2000-05-01T12:12:12Z', 'date-time'],
             ['2000-05-01T12:12:12+0100', 'date-time'],
@@ -114,6 +118,8 @@ public function getValidFormats(): array
             ['2000-05-01T12:12:12.0Z', 'date-time'],
             ['2000-05-01T12:12:12.000Z', 'date-time'],
             ['2000-05-01T12:12:12.000000Z', 'date-time'],
+            [42, 'date-time'],
+            [4.2, 'date-time'],
 
             ['0', 'utc-millisec'],
 
@@ -136,6 +142,8 @@ public function getValidFormats(): array
             ['yellow', 'color'],
             ['#fff', 'color'],
             ['#00cc00', 'color'],
+            [42, 'color'],
+            [4.2, 'color'],
 
             ['background: blue', 'style'],
             ['color: #000;', 'style'],
@@ -149,18 +157,39 @@ public function getValidFormats(): array
             ['./relative:PathReference/', 'uri-reference'],
             ['relativePathReference/', 'uri-reference'],
             ['relative/Path:Reference/', 'uri-reference'],
+            [42, 'uri-reference'],
+            [4.2, 'uri-reference'],
 
             ['info@something.edu', 'email'],
+            [42, 'email'],
+            [4.2, 'email'],
 
             ['10.10.10.10', 'ip-address'],
             ['127.0.0.1', 'ip-address'],
+            [42, 'ip-address'],
+            [4.2, 'ip-address'],
+
+            ['127.0.0.1', 'ipv4'],
+            [42, 'ipv4'],
+            [4.2, 'ipv4'],
 
             ['::ff', 'ipv6'],
+            [42, 'ipv6'],
+            [4.2, 'ipv6'],
 
             ['www.example.com', 'host-name'],
             ['3v4l.org', 'host-name'],
             ['a-valid-host.com', 'host-name'],
             ['localhost', 'host-name'],
+            [42, 'host-name'],
+            [4.2, 'host-name'],
+
+            ['www.example.com', 'hostname'],
+            ['3v4l.org', 'hostname'],
+            ['a-valid-host.com', 'hostname'],
+            ['localhost', 'hostname'],
+            [42, 'hostname'],
+            [4.2, 'hostname'],
 
             ['anything', '*'],
             ['unknown', '*'],