From 3444a3be8c8a567f944fd7b81838e615852d787a Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Sat, 30 Oct 2021 19:37:44 +0200 Subject: [PATCH] Factor out parsing git-style size numbers to StringUtils Move the code to parse numbers with an optional 'k', 'm', or 'g' suffix from the config file handling to StringUtils. This enables me to re-use it in EGit, which has duplicate code in StorageSizeFieldEditor. As this is generally useful functionality, providing it in the library makes sense. Change-Id: I86e4f5f62e14f99b35726b198ba3bbf1669418d9 Signed-off-by: Thomas Wolf --- .../eclipse/jgit/util/StringUtilsTest.java | 83 +++++++++++ .../eclipse/jgit/internal/JGitText.properties | 1 + .../org/eclipse/jgit/internal/JGitText.java | 1 + .../src/org/eclipse/jgit/lib/Config.java | 18 +-- .../jgit/lib/DefaultTypedConfigGetter.java | 27 +--- .../org/eclipse/jgit/util/StringUtils.java | 134 ++++++++++++++++++ 6 files changed, 226 insertions(+), 38 deletions(-) diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java index 82c0afec5..aa7247e10 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java @@ -12,6 +12,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import org.junit.Test; @@ -70,4 +71,86 @@ public void testReplaceLineBreaks() { assertEquals("a b c d", StringUtils.replaceLineBreaksWithSpace("a\r\nb\nc d")); } + + @Test + public void testFormatWithSuffix() { + assertEquals("1023", StringUtils.formatWithSuffix(1023)); + assertEquals("1k", StringUtils.formatWithSuffix(1024)); + assertEquals("1025", StringUtils.formatWithSuffix(1025)); + assertEquals("1048575", StringUtils.formatWithSuffix(1024 * 1024 - 1)); + assertEquals("1m", StringUtils.formatWithSuffix(1024 * 1024)); + assertEquals("1048577", StringUtils.formatWithSuffix(1024 * 1024 + 1)); + assertEquals("1073741823", + StringUtils.formatWithSuffix(1024 * 1024 * 1024 - 1)); + assertEquals("1g", StringUtils.formatWithSuffix(1024 * 1024 * 1024)); + assertEquals("1073741825", + StringUtils.formatWithSuffix(1024 * 1024 * 1024 + 1)); + assertEquals("3k", StringUtils.formatWithSuffix(3 * 1024)); + assertEquals("3m", StringUtils.formatWithSuffix(3 * 1024 * 1024)); + assertEquals("2050k", + StringUtils.formatWithSuffix(2 * 1024 * 1024 + 2048)); + assertEquals("3g", + StringUtils.formatWithSuffix(3L * 1024 * 1024 * 1024)); + assertEquals("3000", StringUtils.formatWithSuffix(3000)); + assertEquals("3000000", StringUtils.formatWithSuffix(3_000_000)); + assertEquals("1953125k", StringUtils.formatWithSuffix(2_000_000_000)); + assertEquals("2000000010", StringUtils.formatWithSuffix(2_000_000_010)); + assertEquals("3000000000", + StringUtils.formatWithSuffix(3_000_000_000L)); + } + + @Test + public void testParseWithSuffix() { + assertEquals(1024, StringUtils.parseIntWithSuffix("1k", true)); + assertEquals(1024, StringUtils.parseIntWithSuffix("1 k", true)); + assertEquals(1024, StringUtils.parseIntWithSuffix("1 k", true)); + assertEquals(1024, StringUtils.parseIntWithSuffix(" \t1 k \n", true)); + assertEquals(1024, StringUtils.parseIntWithSuffix("1k", false)); + assertEquals(1024, StringUtils.parseIntWithSuffix("1K", false)); + assertEquals(1024 * 1024, StringUtils.parseIntWithSuffix("1m", false)); + assertEquals(1024 * 1024, StringUtils.parseIntWithSuffix("1M", false)); + assertEquals(-1024 * 1024, + StringUtils.parseIntWithSuffix("-1M", false)); + assertEquals(1_000_000, + StringUtils.parseIntWithSuffix(" 1000000\r\n", false)); + assertEquals(1024 * 1024 * 1024, + StringUtils.parseIntWithSuffix("1g", false)); + assertEquals(1024 * 1024 * 1024, + StringUtils.parseIntWithSuffix("1G", false)); + assertEquals(3L * 1024 * 1024 * 1024, + StringUtils.parseLongWithSuffix("3g", false)); + assertEquals(3L * 1024 * 1024 * 1024, + StringUtils.parseLongWithSuffix("3G", false)); + assertThrows(NumberFormatException.class, + () -> StringUtils.parseIntWithSuffix("2G", false)); + assertEquals(2L * 1024 * 1024 * 1024, + StringUtils.parseLongWithSuffix("2G", false)); + assertThrows(NumberFormatException.class, + () -> StringUtils.parseLongWithSuffix("-1m", true)); + assertThrows(NumberFormatException.class, + () -> StringUtils.parseLongWithSuffix("-1000", true)); + assertThrows(StringIndexOutOfBoundsException.class, + () -> StringUtils.parseLongWithSuffix("", false)); + assertThrows(StringIndexOutOfBoundsException.class, + () -> StringUtils.parseLongWithSuffix(" \t \n", false)); + assertThrows(StringIndexOutOfBoundsException.class, + () -> StringUtils.parseLongWithSuffix("k", false)); + assertThrows(StringIndexOutOfBoundsException.class, + () -> StringUtils.parseLongWithSuffix("m", false)); + assertThrows(StringIndexOutOfBoundsException.class, + () -> StringUtils.parseLongWithSuffix("g", false)); + assertThrows(NumberFormatException.class, + () -> StringUtils.parseLongWithSuffix("1T", false)); + assertThrows(NumberFormatException.class, + () -> StringUtils.parseLongWithSuffix("1t", false)); + assertThrows(NumberFormatException.class, + () -> StringUtils.parseLongWithSuffix("Nonumber", false)); + assertThrows(NumberFormatException.class, + () -> StringUtils.parseLongWithSuffix("0x001f", false)); + assertThrows(NumberFormatException.class, + () -> StringUtils.parseLongWithSuffix("beef", false)); + assertThrows(NumberFormatException.class, + () -> StringUtils.parseLongWithSuffix("8000000000000000000G", + false)); + } } diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 74762a902..ee97c265e 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -793,6 +793,7 @@ uriNotFoundWithMessage={0} not found: {1} URINotSupported=URI not supported: {0} userConfigInvalid=Git config in the user's home directory {0} is invalid {1} validatingGitModules=Validating .gitmodules files +valueExceedsRange=Value ''{0}'' exceeds the range of {1} verifySignatureBad=BAD signature from "{0}" verifySignatureExpired=Expired signature from "{0}" verifySignatureGood=Good signature from "{0}" diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 3d5d0607e..f7ebe4f40 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -821,6 +821,7 @@ public static JGitText get() { /***/ public String URINotSupported; /***/ public String userConfigInvalid; /***/ public String validatingGitModules; + /***/ public String valueExceedsRange; /***/ public String verifySignatureBad; /***/ public String verifySignatureExpired; /***/ public String verifySignatureGood; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java index a369026c9..1ce3e312e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java @@ -42,6 +42,7 @@ import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.StringUtils; /** * Git style {@code .config}, {@code .gitconfig}, {@code .gitmodules} file. @@ -50,9 +51,6 @@ public class Config { private static final String[] EMPTY_STRING_ARRAY = {}; - static final long KiB = 1024; - static final long MiB = 1024 * KiB; - static final long GiB = 1024 * MiB; private static final int MAX_DEPTH = 10; private static final TypedConfigGetter DEFAULT_GETTER = new DefaultTypedConfigGetter(); @@ -765,18 +763,8 @@ public void setInt(final String section, final String subsection, */ public void setLong(final String section, final String subsection, final String name, final long value) { - final String s; - - if (value >= GiB && (value % GiB) == 0) - s = String.valueOf(value / GiB) + "g"; //$NON-NLS-1$ - else if (value >= MiB && (value % MiB) == 0) - s = String.valueOf(value / MiB) + "m"; //$NON-NLS-1$ - else if (value >= KiB && (value % KiB) == 0) - s = String.valueOf(value / KiB) + "k"; //$NON-NLS-1$ - else - s = String.valueOf(value); - - setString(section, subsection, name, s); + setString(section, subsection, name, + StringUtils.formatWithSuffix(value)); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java index cc0b995f1..9f96bce25 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java @@ -126,30 +126,11 @@ public long getLong(Config config, String section, String subsection, if (str == null) { return defaultValue; } - String n = str.trim(); - if (n.length() == 0) { - return defaultValue; - } - long mul = 1; - switch (StringUtils.toLowerCase(n.charAt(n.length() - 1))) { - case 'g': - mul = Config.GiB; - break; - case 'm': - mul = Config.MiB; - break; - case 'k': - mul = Config.KiB; - break; - } - if (mul > 1) { - n = n.substring(0, n.length() - 1).trim(); - } - if (n.length() == 0) { - return defaultValue; - } try { - return mul * Long.parseLong(n); + return StringUtils.parseLongWithSuffix(str, false); + } catch (StringIndexOutOfBoundsException e) { + // Empty + return defaultValue; } catch (NumberFormatException nfe) { throw new IllegalArgumentException(MessageFormat.format( JGitText.get().invalidIntegerValue, section, name, str), diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java index 61de65cac..b77fb920e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java @@ -13,12 +13,20 @@ import java.text.MessageFormat; import java.util.Collection; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.internal.JGitText; /** * Miscellaneous string comparison utility methods. */ public final class StringUtils { + + private static final long KiB = 1024; + + private static final long MiB = 1024 * KiB; + + private static final long GiB = 1024 * MiB; + private static final char[] LC; static { @@ -307,4 +315,130 @@ public static String replaceLineBreaksWithSpace(String in) { } return new String(buf, 0, o); } + + /** + * Parses a number with optional case-insensitive suffix 'k', 'm', or 'g' + * indicating KiB, MiB, and GiB, respectively. The suffix may follow the + * number with optional separation by one or more blanks. + * + * @param value + * {@link String} to parse; with leading and trailing whitespace + * ignored + * @param positiveOnly + * {@code true} to only accept positive numbers, {@code false} to + * allow negative numbers, too + * @return the value parsed + * @throws NumberFormatException + * if the {@value} is not parseable, or beyond the range of + * {@link Long} + * @throws StringIndexOutOfBoundsException + * if the string is empty or contains only whitespace, or + * contains only the letter 'k', 'm', or 'g' + * @since 6.0 + */ + public static long parseLongWithSuffix(@NonNull String value, + boolean positiveOnly) + throws NumberFormatException, StringIndexOutOfBoundsException { + String n = value.strip(); + if (n.isEmpty()) { + throw new StringIndexOutOfBoundsException(); + } + long mul = 1; + switch (n.charAt(n.length() - 1)) { + case 'g': + case 'G': + mul = GiB; + break; + case 'm': + case 'M': + mul = MiB; + break; + case 'k': + case 'K': + mul = KiB; + break; + default: + break; + } + if (mul > 1) { + n = n.substring(0, n.length() - 1).trim(); + } + if (n.isEmpty()) { + throw new StringIndexOutOfBoundsException(); + } + long number; + if (positiveOnly) { + number = Long.parseUnsignedLong(n); + if (number < 0) { + throw new NumberFormatException( + MessageFormat.format(JGitText.get().valueExceedsRange, + value, Long.class.getSimpleName())); + } + } else { + number = Long.parseLong(n); + } + if (mul == 1) { + return number; + } + try { + return Math.multiplyExact(mul, number); + } catch (ArithmeticException e) { + throw new NumberFormatException(e.getLocalizedMessage()); + } + } + + /** + * Parses a number with optional case-insensitive suffix 'k', 'm', or 'g' + * indicating KiB, MiB, and GiB, respectively. The suffix may follow the + * number with optional separation by blanks. + * + * @param value + * {@link String} to parse; with leading and trailing whitespace + * ignored + * @param positiveOnly + * {@code true} to only accept positive numbers, {@code false} to + * allow negative numbers, too + * @return the value parsed + * @throws NumberFormatException + * if the {@value} is not parseable or beyond the range of + * {@link Integer} + * @throws StringIndexOutOfBoundsException + * if the string is empty or contains only whitespace, or + * contains only the letter 'k', 'm', or 'g' + * @since 6.0 + */ + public static int parseIntWithSuffix(@NonNull String value, + boolean positiveOnly) + throws NumberFormatException, StringIndexOutOfBoundsException { + try { + return Math.toIntExact(parseLongWithSuffix(value, positiveOnly)); + } catch (ArithmeticException e) { + throw new NumberFormatException( + MessageFormat.format(JGitText.get().valueExceedsRange, + value, Integer.class.getSimpleName())); + } + } + + /** + * Formats an integral value as a decimal number with 'k', 'm', or 'g' + * suffix if it is an exact multiple of 1024, otherwise returns the value + * representation as a decimal number without suffix. + * + * @param value + * Value to format + * @return the value's String representation + * @since 6.0 + */ + public static String formatWithSuffix(long value) { + if (value >= GiB && (value % GiB) == 0) { + return String.valueOf(value / GiB) + 'g'; + } + if (value >= MiB && (value % MiB) == 0) { + return String.valueOf(value / MiB) + 'm'; + } + if (value >= KiB && (value % KiB) == 0) { + return String.valueOf(value / KiB) + 'k'; + } + return String.valueOf(value); + } }