From 902b761637eb2850f5bf223df92718a824282275 Mon Sep 17 00:00:00 2001 From: George Peponakis Date: Sat, 2 Jun 2018 17:46:13 +0300 Subject: [PATCH 1/2] Add methods allowing masking of Strings maskStart and maskEnd allow masking the original str by replacing it's characters with the specified character-mask. Common usecase is to hide sensitive information from logs, by using it in toString of classes or in inputs to log calls. --- .../org/apache/commons/lang3/StringUtils.java | 132 ++++++++++++++++++ .../apache/commons/lang3/StringUtilsTest.java | 34 +++++ 2 files changed, 166 insertions(+) diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java b/src/main/java/org/apache/commons/lang3/StringUtils.java index 66a2960c32b..a3b59161d21 100644 --- a/src/main/java/org/apache/commons/lang3/StringUtils.java +++ b/src/main/java/org/apache/commons/lang3/StringUtils.java @@ -9341,4 +9341,136 @@ public static int[] toCodePoints(final CharSequence str) { } return result; } + + /** + *

Masks the given {@code str} by replacing, starting from the first character of the {@link String}, at list + * {@code minMasked} characters.

+ * + *

The result will have up to the last {@code maxUnmasked} characters with the same value as the original + * {@code str} with the rest being replaced by {@code mask}.

+ * + *

For {@code null} or {@link String#isEmpty() empty} {@code str}, the same {@link String} is returned.

+ * + *

Negative values for {@code minMasked} and {@code maxUnmasked} are set to zero.

+ * + *
+     * StringUtils.maskStart(null, *, *, *)       =  null
+     * StringUtils.maskStart("", *, *, *)         =  null
+     * StringUtils.maskStart("test", *, -1, 4)    =  "test"
+     * StringUtils.maskStart("test", *, 0, 4)     =  "test"
+     * StringUtils.maskStart("test", 'X', 1, 4)   =  "Xest"
+     * StringUtils.maskStart("test", 'X', 1, 2)   =  "XXst"
+     * StringUtils.maskStart("test", 'X', 1, -1)  =  "XXXX"
+     * StringUtils.maskStart("test", 'X', 1, 0)   =  "XXXX"
+     * 
+ * + * @param str the String to mask it's content + * @param mask the character used as the mask + * @param minMasked the minimum number of characters to mask + * @param maxUnmasked the maximum number of characters that will remain unmasked + * + * @return the masked String + * + * @since 3.8 + */ + public static String maskStart(final String str, final char mask, int minMasked, int maxUnmasked) { + if (isEmpty(str)) { + //Nothing to mask + return str; + } + if (minMasked < 0) { + minMasked = 0; + } + if (maxUnmasked < 0) { + maxUnmasked = 0; + } + + final int strLength = str.length(); + final int maskLength; + if (strLength <= minMasked) { + maskLength = strLength; + } else if (strLength < minMasked + maxUnmasked) { + maskLength = minMasked; + } else { + maskLength = strLength - maxUnmasked; + } + + if (maskLength == 0) { + //Fast path, no need to mask + return str; + } + + final char[] values = str.toCharArray(); + for (int i = 0; i < maskLength; i++) { + values[i] = mask; + } + + return new String(values); + } + + /** + *

Masks the given {@code str} by replacing, starting from the last character of the {@link String}, at list + * {@code minMasked} characters.

+ * + *

The result will have up to the first {@code maxUnmasked} characters with the same value as the original + * {@code str} with the rest being replaced by {@code mask}.

+ * + *

For {@code null} or {@link String#isEmpty() empty} {@code str}, the same {@link String} is returned.

+ * + *

Negative values for {@code minMasked} and {@code maxUnmasked} are set to zero.

+ * + *
+     * StringUtils.maskEnd(null, *, *, *)       =  null
+     * StringUtils.maskEnd("", *, *, *)         =  null
+     * StringUtils.maskEnd("test", *, -1, 4)    =  "test"
+     * StringUtils.maskEnd("test", *, 0, 4)     =  "test"
+     * StringUtils.maskEnd("test", 'X', 1, 4)   =  "tesX"
+     * StringUtils.maskEnd("test", 'X', 1, 2)   =  "teXX"
+     * StringUtils.maskEnd("test", 'X', 1, -1)  =  "XXXX"
+     * StringUtils.maskEnd("test", 'X', 1, 0)   =  "XXXX"
+     * 
+ * + * @param str the String to mask it's content + * @param mask the character used as the mask + * @param minMasked the minimum number of characters to mask + * @param maxUnmasked the maximum number of characters that will remain unmasked + * + * @return the masked String + * + * @since 3.8 + */ + public static String maskEnd(final String str, final char mask, int minMasked, int maxUnmasked) { + if (isEmpty(str)) { + //Nothing to mask + return str; + } + if (minMasked < 0) { + minMasked = 0; + } + if (maxUnmasked < 0) { + maxUnmasked = 0; + } + + final int strLength = str.length(); + final int maskLength; + if (strLength <= minMasked) { + maskLength = strLength; + } else if (strLength < minMasked + maxUnmasked) { + maskLength = minMasked; + } else { + maskLength = strLength - maxUnmasked; + } + + if (maskLength == 0) { + //Fast path, no need to mask + return str; + } + + final char[] values = str.toCharArray(); + for (int i = strLength - 1; i > strLength - maskLength - 1; i--) { + values[i] = mask; + } + + return new String(values); + } } diff --git a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java index ee3beeb656e..156918ca5b5 100644 --- a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java @@ -3277,4 +3277,38 @@ public void testGetDigits() { assertEquals("5417543010", StringUtils.getDigits("(541) 754-3010")); assertEquals("\u0967\u0968\u0969", StringUtils.getDigits("\u0967\u0968\u0969")); } + + @Test + public void testMaskStart() { + assertNull(StringUtils.maskStart(null, 'a', 0, 0)); + assertEquals("", StringUtils.maskStart("", 'a', 0, 0)); + assertEquals("test", StringUtils.maskStart("test", 'X', -1, 4)); + assertEquals("XXXX", StringUtils.maskStart("test", 'X', 0, -1)); + assertEquals("test", StringUtils.maskStart("test", 'X', 0, 4)); + assertEquals("Xest", StringUtils.maskStart("test", 'X', 0, 3)); + assertEquals("XXst", StringUtils.maskStart("test", 'X', 0, 2)); + assertEquals("XXXt", StringUtils.maskStart("test", 'X', 0, 1)); + assertEquals("XXXX", StringUtils.maskStart("test", 'X', 0, 0)); + assertEquals("Xest", StringUtils.maskStart("test", 'X', 1, 4)); + assertEquals("XXst", StringUtils.maskStart("test", 'X', 2, 4)); + assertEquals("XXXt", StringUtils.maskStart("test", 'X', 3, 4)); + assertEquals("XXXX", StringUtils.maskStart("test", 'X', 4, 4)); + } + + @Test + public void testMaskEnd() { + assertNull(StringUtils.maskEnd(null, 'a', 0, 0)); + assertEquals("", StringUtils.maskEnd("", 'a', 0, 0)); + assertEquals("test", StringUtils.maskEnd("test", 'X', -1, 4)); + assertEquals("XXXX", StringUtils.maskEnd("test", 'X', 0, -1)); + assertEquals("test", StringUtils.maskEnd("test", 'X', 0, 4)); + assertEquals("tesX", StringUtils.maskEnd("test", 'X', 0, 3)); + assertEquals("teXX", StringUtils.maskEnd("test", 'X', 0, 2)); + assertEquals("tXXX", StringUtils.maskEnd("test", 'X', 0, 1)); + assertEquals("XXXX", StringUtils.maskEnd("test", 'X', 0, 0)); + assertEquals("tesX", StringUtils.maskEnd("test", 'X', 1, 4)); + assertEquals("teXX", StringUtils.maskEnd("test", 'X', 2, 4)); + assertEquals("tXXX", StringUtils.maskEnd("test", 'X', 3, 4)); + assertEquals("XXXX", StringUtils.maskEnd("test", 'X', 4, 4)); + } } From 4c972e1d5d55f05a1786cce0cdacd2323399f482 Mon Sep 17 00:00:00 2001 From: George Peponakis Date: Fri, 6 Jul 2018 20:31:45 +0300 Subject: [PATCH 2/2] Add method that mask in the middle and allows unmasked from both sides maskStart and maskEnd now call the new method Also Arrays.fill is now used, in place of the for loop, to take advantage of possible intrinsics by the JVM http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2014-February/013294.html --- .../org/apache/commons/lang3/StringUtils.java | 128 ++++++++++++------ .../apache/commons/lang3/StringUtilsTest.java | 39 ++++++ 2 files changed, 126 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java b/src/main/java/org/apache/commons/lang3/StringUtils.java index a3b59161d21..1d7134f91c1 100644 --- a/src/main/java/org/apache/commons/lang3/StringUtils.java +++ b/src/main/java/org/apache/commons/lang3/StringUtils.java @@ -9355,7 +9355,7 @@ public static int[] toCodePoints(final CharSequence str) { * *
      * StringUtils.maskStart(null, *, *, *)       =  null
-     * StringUtils.maskStart("", *, *, *)         =  null
+     * StringUtils.maskStart("", *, *, *)         =  ""
      * StringUtils.maskStart("test", *, -1, 4)    =  "test"
      * StringUtils.maskStart("test", *, 0, 4)     =  "test"
      * StringUtils.maskStart("test", 'X', 1, 4)   =  "Xest"
@@ -9364,6 +9364,8 @@ public static int[] toCodePoints(final CharSequence str) {
      * StringUtils.maskStart("test", 'X', 1, 0)   =  "XXXX"
      * 
* + *

This is equivalent to calling {@code mask(str, mask, minMasked, 0, maxUnmasked)}

+ * * @param str the String to mask it's content * @param mask the character used as the mask * @param minMasked the minimum number of characters to mask @@ -9371,41 +9373,13 @@ public static int[] toCodePoints(final CharSequence str) { * * @return the masked String * + * @see #mask(String, char, int, int, int) + * @see #maskEnd(String, char, int, int) + * * @since 3.8 */ public static String maskStart(final String str, final char mask, int minMasked, int maxUnmasked) { - if (isEmpty(str)) { - //Nothing to mask - return str; - } - if (minMasked < 0) { - minMasked = 0; - } - if (maxUnmasked < 0) { - maxUnmasked = 0; - } - - final int strLength = str.length(); - final int maskLength; - if (strLength <= minMasked) { - maskLength = strLength; - } else if (strLength < minMasked + maxUnmasked) { - maskLength = minMasked; - } else { - maskLength = strLength - maxUnmasked; - } - - if (maskLength == 0) { - //Fast path, no need to mask - return str; - } - - final char[] values = str.toCharArray(); - for (int i = 0; i < maskLength; i++) { - values[i] = mask; - } - - return new String(values); + return mask(str, mask, minMasked, 0, maxUnmasked); } /** @@ -9421,7 +9395,7 @@ public static String maskStart(final String str, final char mask, int minMasked, * *
      * StringUtils.maskEnd(null, *, *, *)       =  null
-     * StringUtils.maskEnd("", *, *, *)         =  null
+     * StringUtils.maskEnd("", *, *, *)         =  ""
      * StringUtils.maskEnd("test", *, -1, 4)    =  "test"
      * StringUtils.maskEnd("test", *, 0, 4)     =  "test"
      * StringUtils.maskEnd("test", 'X', 1, 4)   =  "tesX"
@@ -9430,6 +9404,8 @@ public static String maskStart(final String str, final char mask, int minMasked,
      * StringUtils.maskEnd("test", 'X', 1, 0)   =  "XXXX"
      * 
* + *

This is equivalent to calling {@code mask(str, mask, minMasked, maxUnmasked, 0)}

+ * * @param str the String to mask it's content * @param mask the character used as the mask * @param minMasked the minimum number of characters to mask @@ -9437,9 +9413,53 @@ public static String maskStart(final String str, final char mask, int minMasked, * * @return the masked String * + * @see #mask(String, char, int, int, int) + * @see #maskStart(String, char, int, int) + * * @since 3.8 */ public static String maskEnd(final String str, final char mask, int minMasked, int maxUnmasked) { + return mask(str, mask, minMasked, maxUnmasked, 0); + } + + /** + *

Masks the given {@code str} by replacing at list {@code minMasked} characters.

+ * + *

The result will have up to {@code maxUnmaskedStart} + {@code maxUnmaskedEnd} characters with the same value as + * the original {@code str} with the rest being replaced by {@code mask}.

+ * + *

For {@code null} or {@link String#isEmpty() empty} {@code str}, the same {@link String} is returned.

+ * + *

Negative values for {@code minMasked}, {@code maxUnmaskedStart} and {@code maxUnmaskedEnd} are set to zero.

+ * + *
+     * StringUtils.maskStart(null, *, *, *, *)       =  null
+     * StringUtils.maskStart("", *, *, *, *)         =  ""
+     * StringUtils.maskStart("test", *, -1, 2, 2)    =  "test"
+     * StringUtils.maskStart("test", *, 0, 2, 2)     =  "test"
+     * StringUtils.maskStart("test", *, 4, *, *)     =  "XXXX"
+     * StringUtils.maskStart("test", *, 0, 2, 2)     =  "test"
+     * StringUtils.maskStart("test", *, 1, 2, 2)     =  "tXst"
+     * StringUtils.maskStart("test", *, 2, 2, 2)     =  "tXXt"
+     * StringUtils.maskStart("test", *, 1, 1, 1)     =  "tXXt"
+     * StringUtils.maskStart("test", *, 1, 2, 1)     =  "teXt"
+     * StringUtils.maskStart("test", *, 1, 1, 2)     =  "tXst"
+     * 
+ * + * @param str the String to mask it's content + * @param mask the character used as the mask + * @param minMasked the minimum number of characters to mask + * @param maxUnmaskedStart the maximum number of characters that will remain unmasked from the start + * @param maxUnmaskedEnd the maximum number of characters that will remain unmasked from the end + * + * @return the masked String + * + * @see #maskStart(String, char, int, int) + * @see #maskEnd(String, char, int, int) + * + * @since 3.8 + */ + public static String mask(final String str, final char mask, int minMasked, int maxUnmaskedStart, int maxUnmaskedEnd) { if (isEmpty(str)) { //Nothing to mask return str; @@ -9447,18 +9467,46 @@ public static String maskEnd(final String str, final char mask, int minMasked, i if (minMasked < 0) { minMasked = 0; } - if (maxUnmasked < 0) { - maxUnmasked = 0; + if (maxUnmaskedStart < 0) { + maxUnmaskedStart = 0; + } + if (maxUnmaskedEnd < 0) { + maxUnmaskedEnd = 0; } final int strLength = str.length(); final int maskLength; + final int maskStart; if (strLength <= minMasked) { maskLength = strLength; - } else if (strLength < minMasked + maxUnmasked) { + maskStart = 0; + } else if ((long)strLength < (long)minMasked + maxUnmaskedStart + maxUnmaskedEnd) { + //long to avoid int overflow maskLength = minMasked; + int diff = Math.abs(maxUnmaskedStart - maxUnmaskedEnd); + if (diff == 0) { + maxUnmaskedStart = (strLength - minMasked)/2; + } else { + int remainingChars = strLength - maskLength; + if (diff > remainingChars) { + if (maxUnmaskedStart > maxUnmaskedEnd) { + maxUnmaskedStart = remainingChars; + } else { + maxUnmaskedStart = 0; + } + } else { + if (maxUnmaskedStart > maxUnmaskedEnd) { + maxUnmaskedStart = remainingChars - diff; + } else { + maxUnmaskedStart = diff; + } + } + } + + maskStart = maxUnmaskedStart; } else { - maskLength = strLength - maxUnmasked; + maskLength = strLength - maxUnmaskedStart - maxUnmaskedEnd; + maskStart = maxUnmaskedStart; } if (maskLength == 0) { @@ -9467,9 +9515,7 @@ public static String maskEnd(final String str, final char mask, int minMasked, i } final char[] values = str.toCharArray(); - for (int i = strLength - 1; i > strLength - maskLength - 1; i--) { - values[i] = mask; - } + Arrays.fill(values, maskStart, maskStart+ maskLength, mask); return new String(values); } diff --git a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java index 156918ca5b5..cd11063b9a0 100644 --- a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java @@ -3293,6 +3293,9 @@ public void testMaskStart() { assertEquals("XXst", StringUtils.maskStart("test", 'X', 2, 4)); assertEquals("XXXt", StringUtils.maskStart("test", 'X', 3, 4)); assertEquals("XXXX", StringUtils.maskStart("test", 'X', 4, 4)); + + assertEquals("Xest", StringUtils.maskStart("test", 'X', 1, Integer.MAX_VALUE)); + assertEquals("XXXX", StringUtils.maskStart("test", 'X', 1, Integer.MIN_VALUE)); } @Test @@ -3310,5 +3313,41 @@ public void testMaskEnd() { assertEquals("teXX", StringUtils.maskEnd("test", 'X', 2, 4)); assertEquals("tXXX", StringUtils.maskEnd("test", 'X', 3, 4)); assertEquals("XXXX", StringUtils.maskEnd("test", 'X', 4, 4)); + + assertEquals("tesX", StringUtils.maskEnd("test", 'X', 1, Integer.MAX_VALUE)); + assertEquals("XXXX", StringUtils.maskEnd("test", 'X', 1, Integer.MIN_VALUE)); + } + + @Test + public void testMask() { + assertNull(StringUtils.mask(null, '*', 4, 4, 4 )); + assertEquals("", StringUtils.mask("", '*', 4, 4, 4)); + assertEquals("test", StringUtils.mask("test", 'X', -1, 2, 2)); + assertEquals("XXXX", StringUtils.mask("test", 'X', 0, -1, -1)); + assertEquals("test", StringUtils.mask("test", 'X', 0, 2, 2)); + assertEquals("tesX", StringUtils.mask("test", 'X', 0, 3, 0)); + assertEquals("teXX", StringUtils.mask("test", 'X', 0, 2, 0)); + assertEquals("tXXX", StringUtils.mask("test", 'X', 0, 1, 0)); + assertEquals("XXXX", StringUtils.mask("test", 'X', 0, 0, 0)); + + assertEquals("Xest", StringUtils.mask("test", 'X', 0, 0, 3)); + assertEquals("XXst", StringUtils.mask("test", 'X', 0, 0, 2)); + assertEquals("XXXt", StringUtils.mask("test", 'X', 0, 0, 1)); + + assertEquals("tXst", StringUtils.mask("test", 'X', 1, 4, 4)); + assertEquals("tXXt", StringUtils.mask("test", 'X', 2, 4, 4)); + assertEquals("XXXt", StringUtils.mask("test", 'X', 3, 4, 4)); + assertEquals("XXXX", StringUtils.mask("test", 'X', 4, 4, 4)); + + assertEquals("tXst", StringUtils.mask("test", 'X', 1, Integer.MAX_VALUE, Integer.MAX_VALUE)); + assertEquals("teXst", StringUtils.mask("teest", 'X', 1, Integer.MAX_VALUE, Integer.MAX_VALUE)); + assertEquals("tesX", StringUtils.mask("test", 'X', 1, Integer.MAX_VALUE, 0)); + assertEquals("Xest", StringUtils.mask("test", 'X', 1, 0, Integer.MAX_VALUE)); + assertEquals("Xest", StringUtils.mask("test", 'X', 1, Integer.MAX_VALUE - 5, Integer.MAX_VALUE)); + assertEquals("tesX", StringUtils.mask("test", 'X', 1, Integer.MAX_VALUE, Integer.MAX_VALUE - 5)); + assertEquals("tXst", StringUtils.mask("test", 'X', 1, Integer.MAX_VALUE -1, Integer.MAX_VALUE)); + assertEquals("teXt", StringUtils.mask("test", 'X', 1, Integer.MAX_VALUE, Integer.MAX_VALUE -1)); + assertEquals("tXXst", StringUtils.mask("teest", 'X', 2, Integer.MAX_VALUE -1, Integer.MAX_VALUE)); + assertEquals("teXXt", StringUtils.mask("teest", 'X', 2, Integer.MAX_VALUE, Integer.MAX_VALUE -1)); } }