Skip to content

Commit 5c85cd5

Browse files
authored
Fix time format validation (#1188)
1 parent e481431 commit 5c85cd5

File tree

2 files changed

+121
-8
lines changed

2 files changed

+121
-8
lines changed

src/main/java/com/networknt/schema/format/TimeFormat.java

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,29 +57,46 @@ public boolean matches(ExecutionContext executionContext, String value) {
5757
long offset = accessor.getLong(OFFSET_SECONDS) / 60;
5858
if (MAX_OFFSET_MIN < offset || MIN_OFFSET_MIN > offset) return false;
5959

60-
long hr = accessor.getLong(HOUR_OF_DAY) - offset / 60;
61-
long min = accessor.getLong(MINUTE_OF_HOUR) - offset % 60;
60+
long hr = accessor.getLong(HOUR_OF_DAY);
61+
long min = accessor.getLong(MINUTE_OF_HOUR);
6262
long sec = accessor.getLong(SECOND_OF_MINUTE);
6363

64+
boolean isStandardTimeRange = (sec <= 59 && min <= 59 && hr <= 23);
65+
if (isStandardTimeRange) {
66+
return true;
67+
}
68+
// Leap second check normalize to UTC to check if 23:59:60Z
69+
hr = hr - offset / 60;
70+
min = min - offset % 60;
71+
6472
if (min < 0) {
6573
--hr;
6674
min += 60;
6775
}
6876
if (hr < 0) {
6977
hr += 24;
7078
}
71-
72-
boolean isStandardTimeRange = (sec <= 59 && min <= 59 && hr <= 23);
73-
boolean isSpecialCaseEndOfDay = (sec == 60 && min == 59 && hr == 23);
74-
75-
return isStandardTimeRange
76-
|| isSpecialCaseEndOfDay;
79+
return isSpecialCaseLeapSecond(sec, min, hr);
7780

7881
} catch (DateTimeException e) {
7982
return false;
8083
}
8184
}
8285

86+
/**
87+
* Determines if it is a valid leap second.
88+
*
89+
* See https://datatracker.ietf.org/doc/html/rfc3339#appendix-D
90+
*
91+
* @param sec second
92+
* @param min minute
93+
* @param hr hour
94+
* @return true if it is a valid leap second
95+
*/
96+
private boolean isSpecialCaseLeapSecond(long sec, long min, long hr) {
97+
return (sec == 60 && min == 59 && hr == 23);
98+
}
99+
83100
@Override
84101
public String getName() {
85102
return "time";
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright (c) 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema.format;
17+
18+
import static org.junit.jupiter.api.Assertions.assertFalse;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
21+
import java.util.Set;
22+
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.EnumSource;
25+
import com.networknt.schema.InputFormat;
26+
import com.networknt.schema.JsonSchema;
27+
import com.networknt.schema.JsonSchemaFactory;
28+
import com.networknt.schema.SchemaValidatorsConfig;
29+
import com.networknt.schema.SpecVersion.VersionFlag;
30+
import com.networknt.schema.ValidationMessage;
31+
32+
class TimeFormatTest {
33+
34+
enum ValidTimeFormatInput {
35+
Z_OFFSET_LEAP_SECOND("23:59:60Z"),
36+
POSITIVE_OFFSET_LEAP_SECOND("07:59:60+08:00"),
37+
NEGATIVE_OFFSET_LEAP_SECOND("15:59:60-08:00"),
38+
Z_OFFSET_MIN_TIME("00:00:00Z"),
39+
Z_OFFSET("23:59:59Z"),
40+
POSITIVE_OFFSET_ZERO("17:00:00+00:00"),
41+
POSITIVE_OFFSET_MAX("17:00:00+23:59"),
42+
NEGATIVE_OFFSET_ZERO("17:00:00-00:00"),
43+
NEGATIVE_OFFSET_ONE_DAY("17:00:00-07:00"),
44+
NEGATIVE_OFFSET_MAX("17:00:00-23:59");
45+
46+
String format;
47+
48+
ValidTimeFormatInput(String format) {
49+
this.format = format;
50+
}
51+
}
52+
53+
@ParameterizedTest
54+
@EnumSource(ValidTimeFormatInput.class)
55+
void validTimeShouldPass(ValidTimeFormatInput input) {
56+
String schemaData = "{\r\n"
57+
+ " \"format\": \"time\"\r\n"
58+
+ "}";
59+
60+
String inputData = "\""+input.format+"\"";
61+
62+
SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().formatAssertionsEnabled(true).build();
63+
JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
64+
Set<ValidationMessage> messages = schema.validate(inputData, InputFormat.JSON);
65+
assertTrue(messages.isEmpty());
66+
}
67+
68+
enum InvalidTimeFormatInput {
69+
NEGATIVE_OFFSET_INVALID_LEAP_SECOND("23:59:60-07:00"),
70+
Z_OFFSET_EXCEED_LEAP_SECOND("23:59:61Z"),
71+
Z_OFFSET_EXCEED_TIME("24:00:00Z"),
72+
POSITIVE_OFFSET_EXCEED_MAX("17:00:00+24:00"),
73+
NEGATIVE_OFFSET_EXCEED_MAX("17:00:00-24:00");
74+
75+
String format;
76+
77+
InvalidTimeFormatInput(String format) {
78+
this.format = format;
79+
}
80+
}
81+
82+
@ParameterizedTest
83+
@EnumSource(InvalidTimeFormatInput.class)
84+
void invalidTimeShouldFail(InvalidTimeFormatInput input) {
85+
String schemaData = "{\r\n"
86+
+ " \"format\": \"time\"\r\n"
87+
+ "}";
88+
89+
String inputData = "\""+input.format+"\"";
90+
91+
SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().formatAssertionsEnabled(true).build();
92+
JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
93+
Set<ValidationMessage> messages = schema.validate(inputData, InputFormat.JSON);
94+
assertFalse(messages.isEmpty());
95+
}
96+
}

0 commit comments

Comments
 (0)