From c9158a648dab4e4f9c448ee9af797c3b1c2689c1 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sat, 30 Aug 2025 09:53:38 +0800 Subject: [PATCH] Fix time format validation --- .../networknt/schema/format/TimeFormat.java | 33 +++++-- .../schema/format/TimeFormatTest.java | 96 +++++++++++++++++++ 2 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 src/test/java/com/networknt/schema/format/TimeFormatTest.java diff --git a/src/main/java/com/networknt/schema/format/TimeFormat.java b/src/main/java/com/networknt/schema/format/TimeFormat.java index 7ded636e7..17327ddc5 100644 --- a/src/main/java/com/networknt/schema/format/TimeFormat.java +++ b/src/main/java/com/networknt/schema/format/TimeFormat.java @@ -57,10 +57,18 @@ public boolean matches(ExecutionContext executionContext, String value) { long offset = accessor.getLong(OFFSET_SECONDS) / 60; if (MAX_OFFSET_MIN < offset || MIN_OFFSET_MIN > offset) return false; - long hr = accessor.getLong(HOUR_OF_DAY) - offset / 60; - long min = accessor.getLong(MINUTE_OF_HOUR) - offset % 60; + long hr = accessor.getLong(HOUR_OF_DAY); + long min = accessor.getLong(MINUTE_OF_HOUR); long sec = accessor.getLong(SECOND_OF_MINUTE); + boolean isStandardTimeRange = (sec <= 59 && min <= 59 && hr <= 23); + if (isStandardTimeRange) { + return true; + } + // Leap second check normalize to UTC to check if 23:59:60Z + hr = hr - offset / 60; + min = min - offset % 60; + if (min < 0) { --hr; min += 60; @@ -68,18 +76,27 @@ public boolean matches(ExecutionContext executionContext, String value) { if (hr < 0) { hr += 24; } - - boolean isStandardTimeRange = (sec <= 59 && min <= 59 && hr <= 23); - boolean isSpecialCaseEndOfDay = (sec == 60 && min == 59 && hr == 23); - - return isStandardTimeRange - || isSpecialCaseEndOfDay; + return isSpecialCaseLeapSecond(sec, min, hr); } catch (DateTimeException e) { return false; } } + /** + * Determines if it is a valid leap second. + * + * See https://datatracker.ietf.org/doc/html/rfc3339#appendix-D + * + * @param sec second + * @param min minute + * @param hr hour + * @return true if it is a valid leap second + */ + private boolean isSpecialCaseLeapSecond(long sec, long min, long hr) { + return (sec == 60 && min == 59 && hr == 23); + } + @Override public String getName() { return "time"; diff --git a/src/test/java/com/networknt/schema/format/TimeFormatTest.java b/src/test/java/com/networknt/schema/format/TimeFormatTest.java new file mode 100644 index 000000000..f368d6e78 --- /dev/null +++ b/src/test/java/com/networknt/schema/format/TimeFormatTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.format; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import com.networknt.schema.InputFormat; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaValidatorsConfig; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.ValidationMessage; + +class TimeFormatTest { + + enum ValidTimeFormatInput { + Z_OFFSET_LEAP_SECOND("23:59:60Z"), + POSITIVE_OFFSET_LEAP_SECOND("07:59:60+08:00"), + NEGATIVE_OFFSET_LEAP_SECOND("15:59:60-08:00"), + Z_OFFSET_MIN_TIME("00:00:00Z"), + Z_OFFSET("23:59:59Z"), + POSITIVE_OFFSET_ZERO("17:00:00+00:00"), + POSITIVE_OFFSET_MAX("17:00:00+23:59"), + NEGATIVE_OFFSET_ZERO("17:00:00-00:00"), + NEGATIVE_OFFSET_ONE_DAY("17:00:00-07:00"), + NEGATIVE_OFFSET_MAX("17:00:00-23:59"); + + String format; + + ValidTimeFormatInput(String format) { + this.format = format; + } + } + + @ParameterizedTest + @EnumSource(ValidTimeFormatInput.class) + void validTimeShouldPass(ValidTimeFormatInput input) { + String schemaData = "{\r\n" + + " \"format\": \"time\"\r\n" + + "}"; + + String inputData = "\""+input.format+"\""; + + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().formatAssertionsEnabled(true).build(); + JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config); + Set messages = schema.validate(inputData, InputFormat.JSON); + assertTrue(messages.isEmpty()); + } + + enum InvalidTimeFormatInput { + NEGATIVE_OFFSET_INVALID_LEAP_SECOND("23:59:60-07:00"), + Z_OFFSET_EXCEED_LEAP_SECOND("23:59:61Z"), + Z_OFFSET_EXCEED_TIME("24:00:00Z"), + POSITIVE_OFFSET_EXCEED_MAX("17:00:00+24:00"), + NEGATIVE_OFFSET_EXCEED_MAX("17:00:00-24:00"); + + String format; + + InvalidTimeFormatInput(String format) { + this.format = format; + } + } + + @ParameterizedTest + @EnumSource(InvalidTimeFormatInput.class) + void invalidTimeShouldFail(InvalidTimeFormatInput input) { + String schemaData = "{\r\n" + + " \"format\": \"time\"\r\n" + + "}"; + + String inputData = "\""+input.format+"\""; + + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().formatAssertionsEnabled(true).build(); + JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config); + Set messages = schema.validate(inputData, InputFormat.JSON); + assertFalse(messages.isEmpty()); + } +}