From acefae289c2393da6956cdc9ef453c8bfcbeffa7 Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Wed, 13 Sep 2023 12:04:34 +0200 Subject: [PATCH] [jOOQ/jOOQ#12547] Internal patchIso8601Timestamp() isn't safe for 5 digit years This addresses also the various edge cases from [jOOQ/jOOQ#13786] --- jOOQ/src/main/java/org/jooq/impl/Convert.java | 79 +++++++++++++----- .../src/main/java/org/jooq/tools/Convert.java | 81 ++++++++++++++----- 2 files changed, 121 insertions(+), 39 deletions(-) diff --git a/jOOQ/src/main/java/org/jooq/impl/Convert.java b/jOOQ/src/main/java/org/jooq/impl/Convert.java index 2723ae5e17..f8c8351aac 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Convert.java +++ b/jOOQ/src/main/java/org/jooq/impl/Convert.java @@ -45,6 +45,7 @@ import static org.jooq.impl.Internal.arrayType; import static org.jooq.impl.Internal.converterContext; import static org.jooq.impl.Tools.configuration; import static org.jooq.impl.Tools.emulateMultiset; +import static org.jooq.tools.StringUtils.leftPad; import static org.jooq.tools.reflect.Reflect.accessible; import static org.jooq.tools.reflect.Reflect.wrapper; import static org.jooq.types.Unsigned.ubyte; @@ -113,6 +114,7 @@ import org.jooq.exception.DataTypeException; import org.jooq.tools.Ints; import org.jooq.tools.JooqLogger; import org.jooq.tools.Longs; +import org.jooq.tools.StringUtils; import org.jooq.tools.jdbc.MockArray; import org.jooq.tools.jdbc.MockResultSet; import org.jooq.tools.json.ContainerFactory; @@ -1613,31 +1615,70 @@ final class Convert { : string; } - static final String patchIso8601Time(String string) { - return string.length() == 5 - ? (string + ":00") - // [#12158] Support Db2's 15.30.45 format - : string.length() == 8 - ? string.replace('.', ':') - : string; + static final String patchIso8601Time(String s) { + int l = s.length(); + int c1 = s.indexOf(':'); + + if (c1 >= 0) { + int c2 = s.indexOf(':', c1 + 1); + + if (c2 == -1) + return leftPad(s.substring(0, c1), 2, '0') + ':' + leftPad(s.substring(c1 + 1), 2, '0') + ":00"; + else if (l < 8 || c2 != l - 3 || c1 != l - 6) + return leftPad(s.substring(0, c1), 2, '0') + ':' + leftPad(s.substring(c1 + 1, c2), 2, '0') + ':' + leftPad(s.substring(c2 + 1), 2, '0'); + } + + // [#12158] Support Db2's 15.30.45 format + else if ((c1 = s.indexOf('.')) >= 0) { + return patchIso8601Time(s.replace('.', ':')); + } + + return s; } - static final String patchIso8601Timestamp(String string, boolean t) { + static final String patchIso8601Timestamp(String s, boolean t) { // [#11485] Trino produces a non-ISO 8601 "UTC" suffix, instead of "Z" - if (string.endsWith(" UTC")) - string = string.replace(" UTC", "Z"); + if (s.endsWith(" UTC")) + s = s.replace(" UTC", "Z"); + int l = s.length(); + int d1 = s.indexOf('-'); + int d2 = s.indexOf('-', d1 + 1); + int ss = s.indexOf(' ', d2 + 1); + int st = s.indexOf('T', d2 + 1); + int sx = Math.max(ss, st); + int c1 = s.indexOf(':', sx + 1); + int c2 = s.indexOf(':', c1 + 1); + + if (d1 == -1 || d2 == -1) + return s; + + // [#12547] Support year numbers with more or less than 4 digits // [#13786] Be lenient with PostgreSQL style abbreviated time stamp literals - if (string.length() == 10) - return string + (t ? "T" : " ") + "00:00:00"; + else if (sx == -1) + return leftPad(s.substring(0, d1), 4, '0') + '-' + + leftPad(s.substring(d1 + 1, d2), 2, '0') + '-' + + leftPad(s.substring(d2 + 1), 2, '0') + + (t ? "T00:00:00" : " 00:00:00"); + else if (c2 == -1) + return leftPad(s.substring(0, d1), 4, '0') + '-' + + leftPad(s.substring(d1 + 1, d2), 2, '0') + '-' + + leftPad(s.substring(d2 + 1, sx), 2, '0') + + (t ? 'T' : ' ') + + leftPad(s.substring(sx + 1, c1), 2, '0') + ':' + + leftPad(s.substring(c1 + 1), 2, '0') + ":00"; - if (string.length() > 11) - if (t && string.charAt(10) == ' ') - return string.substring(0, 10) + "T" + string.substring(11); - else if (!t && string.charAt(10) == 'T') - return string.substring(0, 10) + " " + string.substring(11); - - return string; + // [#13786] TODO: This doesn't pad seconds in the presence of fractional seconds or time zones + else if (t == (st == -1) || l - c2 < 3 || c2 - c1 < 3 || c1 - sx < 3 || sx - d2 < 3 || d2 - d1 < 3) + return leftPad(s.substring(0, d1), 4, '0') + '-' + + leftPad(s.substring(d1 + 1, d2), 2, '0') + '-' + + leftPad(s.substring(d2 + 1, sx), 2, '0') + + (t ? 'T' : ' ') + + leftPad(s.substring(sx + 1, c1), 2, '0') + ':' + + leftPad(s.substring(c1 + 1, c2), 2, '0') + ':' + + leftPad(s.substring(c2 + 1), 2, '0'); + else + return s; } } diff --git a/jOOQ/src/main/java/org/jooq/tools/Convert.java b/jOOQ/src/main/java/org/jooq/tools/Convert.java index a8152eb20e..c138eddb5e 100644 --- a/jOOQ/src/main/java/org/jooq/tools/Convert.java +++ b/jOOQ/src/main/java/org/jooq/tools/Convert.java @@ -43,6 +43,7 @@ import static java.time.temporal.ChronoField.MILLI_OF_SECOND; import static org.jooq.ContextConverter.scoped; import static org.jooq.impl.Internal.arrayType; import static org.jooq.impl.Internal.converterContext; +import static org.jooq.tools.StringUtils.leftPad; import static org.jooq.tools.reflect.Reflect.accessible; import static org.jooq.tools.reflect.Reflect.wrapper; import static org.jooq.types.Unsigned.ubyte; @@ -537,7 +538,7 @@ public final class Convert { /** * The converter to convert them all. */ - private static final class ConvertAll extends AbstractContextConverter { + static final class ConvertAll extends AbstractContextConverter { private final Class toClass; @@ -1334,32 +1335,72 @@ public final class Convert { : string; } - static final String patchIso8601Time(String string) { - return string.length() == 5 - ? (string + ":00") - // [#12158] Support Db2's 15.30.45 format - : string.length() == 8 - ? string.replace('.', ':') - : string; + + static final String patchIso8601Time(String s) { + int l = s.length(); + int c1 = s.indexOf(':'); + + if (c1 >= 0) { + int c2 = s.indexOf(':', c1 + 1); + + if (c2 == -1) + return leftPad(s.substring(0, c1), 2, '0') + ':' + leftPad(s.substring(c1 + 1), 2, '0') + ":00"; + else if (l < 8 || c2 != l - 3 || c1 != l - 6) + return leftPad(s.substring(0, c1), 2, '0') + ':' + leftPad(s.substring(c1 + 1, c2), 2, '0') + ':' + leftPad(s.substring(c2 + 1), 2, '0'); + } + + // [#12158] Support Db2's 15.30.45 format + else if ((c1 = s.indexOf('.')) >= 0) { + return patchIso8601Time(s.replace('.', ':')); + } + + return s; } - private static final String patchIso8601Timestamp(String string, boolean t) { + static final String patchIso8601Timestamp(String s, boolean t) { // [#11485] Trino produces a non-ISO 8601 "UTC" suffix, instead of "Z" - if (string.endsWith(" UTC")) - string = string.replace(" UTC", "Z"); + if (s.endsWith(" UTC")) + s = s.replace(" UTC", "Z"); + int l = s.length(); + int d1 = s.indexOf('-'); + int d2 = s.indexOf('-', d1 + 1); + int ss = s.indexOf(' ', d2 + 1); + int st = s.indexOf('T', d2 + 1); + int sx = Math.max(ss, st); + int c1 = s.indexOf(':', sx + 1); + int c2 = s.indexOf(':', c1 + 1); + + if (d1 == -1 || d2 == -1) + return s; + + // [#12547] Support year numbers with more or less than 4 digits // [#13786] Be lenient with PostgreSQL style abbreviated time stamp literals - if (string.length() == 10) - return string + (t ? "T" : " ") + "00:00:00"; + else if (sx == -1) + return leftPad(s.substring(0, d1), 4, '0') + '-' + + leftPad(s.substring(d1 + 1, d2), 2, '0') + '-' + + leftPad(s.substring(d2 + 1), 2, '0') + + (t ? "T00:00:00" : " 00:00:00"); + else if (c2 == -1) + return leftPad(s.substring(0, d1), 4, '0') + '-' + + leftPad(s.substring(d1 + 1, d2), 2, '0') + '-' + + leftPad(s.substring(d2 + 1, sx), 2, '0') + + (t ? 'T' : ' ') + + leftPad(s.substring(sx + 1, c1), 2, '0') + ':' + + leftPad(s.substring(c1 + 1), 2, '0') + ":00"; - if (string.length() > 11) - if (t && string.charAt(10) == ' ') - return string.substring(0, 10) + "T" + string.substring(11); - else if (!t && string.charAt(10) == 'T') - return string.substring(0, 10) + " " + string.substring(11); - - return string; + // [#13786] TODO: This doesn't pad seconds in the presence of fractional seconds or time zones + else if (t == (st == -1) || l - c2 < 3 || c2 - c1 < 3 || c1 - sx < 3 || sx - d2 < 3 || d2 - d1 < 3) + return leftPad(s.substring(0, d1), 4, '0') + '-' + + leftPad(s.substring(d1 + 1, d2), 2, '0') + '-' + + leftPad(s.substring(d2 + 1, sx), 2, '0') + + (t ? 'T' : ' ') + + leftPad(s.substring(sx + 1, c1), 2, '0') + ':' + + leftPad(s.substring(c1 + 1, c2), 2, '0') + ':' + + leftPad(s.substring(c2 + 1), 2, '0'); + else + return s; } @Override