Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions src/main/java/org/apache/commons/lang3/StringUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -9341,4 +9341,182 @@ public static int[] toCodePoints(final CharSequence str) {
}
return result;
}

/**
* <p>Masks the given {@code str} by replacing, starting from the first character of the {@link String}, at list
* {@code minMasked} characters.</p>
*
* <p>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}.</p>
*
* <p>For {@code null} or {@link String#isEmpty() empty} {@code str}, the same {@link String} is returned.</p>
*
* <p>Negative values for {@code minMasked} and {@code maxUnmasked} are set to zero.</p>
*
* <pre>
* StringUtils.maskStart(null, *, *, *) = null
* StringUtils.maskStart("", *, *, *) = ""
* 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"
* </pre>
*
* <p>This is equivalent to calling {@code mask(str, mask, minMasked, 0, maxUnmasked)}</p>
*
* @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
*
* @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) {
return mask(str, mask, minMasked, 0, maxUnmasked);
}

/**
* <p>Masks the given {@code str} by replacing, starting from the last character of the {@link String}, at list
* {@code minMasked} characters.</p>
*
* <p>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}.</p>
*
* <p>For {@code null} or {@link String#isEmpty() empty} {@code str}, the same {@link String} is returned.</p>
*
* <p>Negative values for {@code minMasked} and {@code maxUnmasked} are set to zero.</p>
*
* <pre>
* StringUtils.maskEnd(null, *, *, *) = null
* StringUtils.maskEnd("", *, *, *) = ""
* 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"
* </pre>
*
* <p>This is equivalent to calling {@code mask(str, mask, minMasked, maxUnmasked, 0)}</p>
*
* @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
*
* @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);
}

/**
* <p>Masks the given {@code str} by replacing at list {@code minMasked} characters.</p>
*
* <p>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}.</p>
*
* <p>For {@code null} or {@link String#isEmpty() empty} {@code str}, the same {@link String} is returned.</p>
*
* <p>Negative values for {@code minMasked}, {@code maxUnmaskedStart} and {@code maxUnmaskedEnd} are set to zero.</p>
*
* <pre>
* 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"
* </pre>
*
* @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;
}
if (minMasked < 0) {
minMasked = 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;
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 - maxUnmaskedStart - maxUnmaskedEnd;
maskStart = maxUnmaskedStart;
}

if (maskLength == 0) {
//Fast path, no need to mask
return str;
}

final char[] values = str.toCharArray();
Arrays.fill(values, maskStart, maskStart+ maskLength, mask);

return new String(values);
}
}
73 changes: 73 additions & 0 deletions src/test/java/org/apache/commons/lang3/StringUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3277,4 +3277,77 @@ 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));

assertEquals("Xest", StringUtils.maskStart("test", 'X', 1, Integer.MAX_VALUE));
assertEquals("XXXX", StringUtils.maskStart("test", 'X', 1, Integer.MIN_VALUE));
}

@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));

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));
}
}