Skip to content

Commit 9c1e5de

Browse files
eamonnmcmanusGoogle Java Core Libraries
authored and
Google Java Core Libraries
committedAug 24, 2022
Add BaseEncoding.ignoreCase().
Calling `baseEncoding.ignoreCase()` returns a new `BaseEncoding` instance equivalent to `baseEncoding` except that it accepts either case when decoding. When *en*coding it continues to use whatever case the original `baseEncoding` used. RELNOTES=`BaseEncoding` acquires a new `ignoreCase()` method to support case-insensitive decoding. PiperOrigin-RevId: 469812601
1 parent 0f1d935 commit 9c1e5de

File tree

4 files changed

+236
-20
lines changed

4 files changed

+236
-20
lines changed
 

‎android/guava-tests/test/com/google/common/io/BaseEncodingTest.java

+49-2
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ public void testBase64CannotUpperCase() {
125125
base64().upperCase();
126126
fail();
127127
} catch (IllegalStateException expected) {
128-
// success
129128
}
130129
}
131130

@@ -134,7 +133,14 @@ public void testBase64CannotLowerCase() {
134133
base64().lowerCase();
135134
fail();
136135
} catch (IllegalStateException expected) {
137-
// success
136+
}
137+
}
138+
139+
public void testBase64CannotIgnoreCase() {
140+
try {
141+
base64().ignoreCase();
142+
fail();
143+
} catch (IllegalStateException expected) {
138144
}
139145
}
140146

@@ -265,6 +271,18 @@ public void testBase32UpperCaseIsNoOp() {
265271
assertThat(base32().upperCase()).isSameInstanceAs(base32());
266272
}
267273

274+
public void testBase32LowerCase() {
275+
testEncodingWithCasing(base32().lowerCase(), "foobar", "mzxw6ytboi======");
276+
}
277+
278+
public void testBase32IgnoreCase() {
279+
BaseEncoding ignoreCase = base32().ignoreCase();
280+
assertThat(ignoreCase).isNotSameInstanceAs(base32());
281+
assertThat(ignoreCase).isSameInstanceAs(base32().ignoreCase());
282+
testDecodes(ignoreCase, "MZXW6YTBOI======", "foobar");
283+
testDecodes(ignoreCase, "mzxw6ytboi======", "foobar");
284+
}
285+
268286
public void testBase32Offset() {
269287
testEncodesWithOffset(base32(), "foobar", 0, 6, "MZXW6YTBOI======");
270288
testEncodesWithOffset(base32(), "foobar", 1, 5, "N5XWEYLS");
@@ -335,6 +353,33 @@ public void testBase16UpperCaseIsNoOp() {
335353
assertThat(base16().upperCase()).isSameInstanceAs(base16());
336354
}
337355

356+
public void testBase16LowerCase() {
357+
BaseEncoding lowerCase = base16().lowerCase();
358+
assertThat(lowerCase).isNotSameInstanceAs(base16());
359+
assertThat(lowerCase).isSameInstanceAs(base16().lowerCase());
360+
testEncodingWithCasing(lowerCase, "foobar", "666f6f626172");
361+
}
362+
363+
public void testBase16IgnoreCase() {
364+
BaseEncoding ignoreCase = base16().ignoreCase();
365+
assertThat(ignoreCase).isNotSameInstanceAs(base16());
366+
assertThat(ignoreCase).isSameInstanceAs(base16().ignoreCase());
367+
testEncodingWithCasing(ignoreCase, "foobar", "666F6F626172");
368+
testDecodes(ignoreCase, "666F6F626172", "foobar");
369+
testDecodes(ignoreCase, "666f6f626172", "foobar");
370+
testDecodes(ignoreCase, "666F6f626172", "foobar");
371+
}
372+
373+
public void testBase16LowerCaseIgnoreCase() {
374+
BaseEncoding ignoreCase = base16().lowerCase().ignoreCase();
375+
assertThat(ignoreCase).isNotSameInstanceAs(base16());
376+
assertThat(ignoreCase).isSameInstanceAs(base16().lowerCase().ignoreCase());
377+
testEncodingWithCasing(ignoreCase, "foobar", "666f6f626172");
378+
testDecodes(ignoreCase, "666F6F626172", "foobar");
379+
testDecodes(ignoreCase, "666f6f626172", "foobar");
380+
testDecodes(ignoreCase, "666F6f626172", "foobar");
381+
}
382+
338383
public void testBase16InvalidDecodings() {
339384
// These contain bytes not in the decodabet.
340385
assertFailsToDecode(base16(), "\n\n", "Unrecognized character: 0xa");
@@ -344,6 +389,8 @@ public void testBase16InvalidDecodings() {
344389
assertFailsToDecode(base16(), "ABC");
345390
// These have a combination of invalid length and unrecognized characters.
346391
assertFailsToDecode(base16(), "?", "Invalid input length 1");
392+
assertFailsToDecode(base16(), "ab");
393+
assertFailsToDecode(base16().lowerCase(), "AB");
347394
}
348395

349396
public void testBase16Offset() {

‎android/guava/src/com/google/common/io/BaseEncoding.java

+69-8
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,15 @@ CharSequence trimTrailingPadding(CharSequence chars) {
318318
*/
319319
public abstract BaseEncoding lowerCase();
320320

321+
/**
322+
* Returns an encoding that behaves equivalently to this encoding, but decodes letters without
323+
* regard to case.
324+
*
325+
* @throws IllegalStateException if the alphabet used by this encoding contains mixed upper- and
326+
* lower-case characters
327+
*/
328+
public abstract BaseEncoding ignoreCase();
329+
321330
private static final BaseEncoding BASE64 =
322331
new Base64Encoding(
323332
"base64()", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", '=');
@@ -428,8 +437,13 @@ private static final class Alphabet {
428437
final int bytesPerChunk;
429438
private final byte[] decodabet;
430439
private final boolean[] validPadding;
440+
private final boolean ignoreCase;
431441

432442
Alphabet(String name, char[] chars) {
443+
this(name, chars, decodabetFor(chars), /* ignoreCase= */ false);
444+
}
445+
446+
private Alphabet(String name, char[] chars, byte[] decodabet, boolean ignoreCase) {
433447
this.name = checkNotNull(name);
434448
this.chars = checkNotNull(chars);
435449
try {
@@ -452,6 +466,17 @@ private static final class Alphabet {
452466

453467
this.mask = chars.length - 1;
454468

469+
this.decodabet = decodabet;
470+
471+
boolean[] validPadding = new boolean[charsPerChunk];
472+
for (int i = 0; i < bytesPerChunk; i++) {
473+
validPadding[divide(i * 8, bitsPerChar, CEILING)] = true;
474+
}
475+
this.validPadding = validPadding;
476+
this.ignoreCase = ignoreCase;
477+
}
478+
479+
private static byte[] decodabetFor(char[] chars) {
455480
byte[] decodabet = new byte[Ascii.MAX + 1];
456481
Arrays.fill(decodabet, (byte) -1);
457482
for (int i = 0; i < chars.length; i++) {
@@ -460,13 +485,33 @@ private static final class Alphabet {
460485
checkArgument(decodabet[c] == -1, "Duplicate character: %s", c);
461486
decodabet[c] = (byte) i;
462487
}
463-
this.decodabet = decodabet;
488+
return decodabet;
489+
}
464490

465-
boolean[] validPadding = new boolean[charsPerChunk];
466-
for (int i = 0; i < bytesPerChunk; i++) {
467-
validPadding[divide(i * 8, bitsPerChar, CEILING)] = true;
491+
/** Returns an equivalent {@code Alphabet} except it ignores case. */
492+
Alphabet ignoreCase() {
493+
if (ignoreCase) {
494+
return this;
468495
}
469-
this.validPadding = validPadding;
496+
497+
// We can't use .clone() because of GWT.
498+
byte[] newDecodabet = Arrays.copyOf(decodabet, decodabet.length);
499+
for (int upper = 'A'; upper <= 'Z'; upper++) {
500+
int lower = upper | 0x20;
501+
byte decodeUpper = decodabet[upper];
502+
byte decodeLower = decodabet[lower];
503+
if (decodeUpper == -1) {
504+
newDecodabet[upper] = decodeLower;
505+
} else {
506+
checkState(
507+
decodeLower == -1,
508+
"Can't ignoreCase() since '%s' and '%s' encode different values",
509+
(char) upper,
510+
(char) lower);
511+
newDecodabet[lower] = decodeUpper;
512+
}
513+
}
514+
return new Alphabet(name + ".ignoreCase()", chars, newDecodabet, /* ignoreCase= */ true);
470515
}
471516

472517
char encode(int bits) {
@@ -551,14 +596,14 @@ public String toString() {
551596
public boolean equals(@CheckForNull Object other) {
552597
if (other instanceof Alphabet) {
553598
Alphabet that = (Alphabet) other;
554-
return Arrays.equals(this.chars, that.chars);
599+
return this.ignoreCase == that.ignoreCase && Arrays.equals(this.chars, that.chars);
555600
}
556601
return false;
557602
}
558603

559604
@Override
560605
public int hashCode() {
561-
return Arrays.hashCode(chars);
606+
return Arrays.hashCode(chars) + (ignoreCase ? 1231 : 1237);
562607
}
563608
}
564609

@@ -832,6 +877,7 @@ public BaseEncoding withSeparator(String separator, int afterEveryChars) {
832877

833878
@LazyInit @CheckForNull private transient BaseEncoding upperCase;
834879
@LazyInit @CheckForNull private transient BaseEncoding lowerCase;
880+
@LazyInit @CheckForNull private transient BaseEncoding ignoreCase;
835881

836882
@Override
837883
public BaseEncoding upperCase() {
@@ -853,14 +899,24 @@ public BaseEncoding lowerCase() {
853899
return result;
854900
}
855901

902+
@Override
903+
public BaseEncoding ignoreCase() {
904+
BaseEncoding result = ignoreCase;
905+
if (result == null) {
906+
Alphabet ignore = alphabet.ignoreCase();
907+
result = ignoreCase = (ignore == alphabet) ? this : newInstance(ignore, paddingChar);
908+
}
909+
return result;
910+
}
911+
856912
BaseEncoding newInstance(Alphabet alphabet, @CheckForNull Character paddingChar) {
857913
return new StandardBaseEncoding(alphabet, paddingChar);
858914
}
859915

860916
@Override
861917
public String toString() {
862918
StringBuilder builder = new StringBuilder("BaseEncoding.");
863-
builder.append(alphabet.toString());
919+
builder.append(alphabet);
864920
if (8 % alphabet.bitsPerChar != 0) {
865921
if (paddingChar == null) {
866922
builder.append(".omitPadding()");
@@ -1170,6 +1226,11 @@ public BaseEncoding lowerCase() {
11701226
return delegate.lowerCase().withSeparator(separator, afterEveryChars);
11711227
}
11721228

1229+
@Override
1230+
public BaseEncoding ignoreCase() {
1231+
return delegate.ignoreCase().withSeparator(separator, afterEveryChars);
1232+
}
1233+
11731234
@Override
11741235
public String toString() {
11751236
return delegate + ".withSeparator(\"" + separator + "\", " + afterEveryChars + ")";

‎guava-tests/test/com/google/common/io/BaseEncodingTest.java

+49-2
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ public void testBase64CannotUpperCase() {
125125
base64().upperCase();
126126
fail();
127127
} catch (IllegalStateException expected) {
128-
// success
129128
}
130129
}
131130

@@ -134,7 +133,14 @@ public void testBase64CannotLowerCase() {
134133
base64().lowerCase();
135134
fail();
136135
} catch (IllegalStateException expected) {
137-
// success
136+
}
137+
}
138+
139+
public void testBase64CannotIgnoreCase() {
140+
try {
141+
base64().ignoreCase();
142+
fail();
143+
} catch (IllegalStateException expected) {
138144
}
139145
}
140146

@@ -265,6 +271,18 @@ public void testBase32UpperCaseIsNoOp() {
265271
assertThat(base32().upperCase()).isSameInstanceAs(base32());
266272
}
267273

274+
public void testBase32LowerCase() {
275+
testEncodingWithCasing(base32().lowerCase(), "foobar", "mzxw6ytboi======");
276+
}
277+
278+
public void testBase32IgnoreCase() {
279+
BaseEncoding ignoreCase = base32().ignoreCase();
280+
assertThat(ignoreCase).isNotSameInstanceAs(base32());
281+
assertThat(ignoreCase).isSameInstanceAs(base32().ignoreCase());
282+
testDecodes(ignoreCase, "MZXW6YTBOI======", "foobar");
283+
testDecodes(ignoreCase, "mzxw6ytboi======", "foobar");
284+
}
285+
268286
public void testBase32Offset() {
269287
testEncodesWithOffset(base32(), "foobar", 0, 6, "MZXW6YTBOI======");
270288
testEncodesWithOffset(base32(), "foobar", 1, 5, "N5XWEYLS");
@@ -335,6 +353,33 @@ public void testBase16UpperCaseIsNoOp() {
335353
assertThat(base16().upperCase()).isSameInstanceAs(base16());
336354
}
337355

356+
public void testBase16LowerCase() {
357+
BaseEncoding lowerCase = base16().lowerCase();
358+
assertThat(lowerCase).isNotSameInstanceAs(base16());
359+
assertThat(lowerCase).isSameInstanceAs(base16().lowerCase());
360+
testEncodingWithCasing(lowerCase, "foobar", "666f6f626172");
361+
}
362+
363+
public void testBase16IgnoreCase() {
364+
BaseEncoding ignoreCase = base16().ignoreCase();
365+
assertThat(ignoreCase).isNotSameInstanceAs(base16());
366+
assertThat(ignoreCase).isSameInstanceAs(base16().ignoreCase());
367+
testEncodingWithCasing(ignoreCase, "foobar", "666F6F626172");
368+
testDecodes(ignoreCase, "666F6F626172", "foobar");
369+
testDecodes(ignoreCase, "666f6f626172", "foobar");
370+
testDecodes(ignoreCase, "666F6f626172", "foobar");
371+
}
372+
373+
public void testBase16LowerCaseIgnoreCase() {
374+
BaseEncoding ignoreCase = base16().lowerCase().ignoreCase();
375+
assertThat(ignoreCase).isNotSameInstanceAs(base16());
376+
assertThat(ignoreCase).isSameInstanceAs(base16().lowerCase().ignoreCase());
377+
testEncodingWithCasing(ignoreCase, "foobar", "666f6f626172");
378+
testDecodes(ignoreCase, "666F6F626172", "foobar");
379+
testDecodes(ignoreCase, "666f6f626172", "foobar");
380+
testDecodes(ignoreCase, "666F6f626172", "foobar");
381+
}
382+
338383
public void testBase16InvalidDecodings() {
339384
// These contain bytes not in the decodabet.
340385
assertFailsToDecode(base16(), "\n\n", "Unrecognized character: 0xa");
@@ -344,6 +389,8 @@ public void testBase16InvalidDecodings() {
344389
assertFailsToDecode(base16(), "ABC");
345390
// These have a combination of invalid length and unrecognized characters.
346391
assertFailsToDecode(base16(), "?", "Invalid input length 1");
392+
assertFailsToDecode(base16(), "ab");
393+
assertFailsToDecode(base16().lowerCase(), "AB");
347394
}
348395

349396
public void testBase16Offset() {

‎guava/src/com/google/common/io/BaseEncoding.java

+69-8
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,15 @@ CharSequence trimTrailingPadding(CharSequence chars) {
318318
*/
319319
public abstract BaseEncoding lowerCase();
320320

321+
/**
322+
* Returns an encoding that behaves equivalently to this encoding, but decodes letters without
323+
* regard to case.
324+
*
325+
* @throws IllegalStateException if the alphabet used by this encoding contains mixed upper- and
326+
* lower-case characters
327+
*/
328+
public abstract BaseEncoding ignoreCase();
329+
321330
private static final BaseEncoding BASE64 =
322331
new Base64Encoding(
323332
"base64()", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", '=');
@@ -428,8 +437,13 @@ private static final class Alphabet {
428437
final int bytesPerChunk;
429438
private final byte[] decodabet;
430439
private final boolean[] validPadding;
440+
private final boolean ignoreCase;
431441

432442
Alphabet(String name, char[] chars) {
443+
this(name, chars, decodabetFor(chars), /* ignoreCase= */ false);
444+
}
445+
446+
private Alphabet(String name, char[] chars, byte[] decodabet, boolean ignoreCase) {
433447
this.name = checkNotNull(name);
434448
this.chars = checkNotNull(chars);
435449
try {
@@ -452,6 +466,17 @@ private static final class Alphabet {
452466

453467
this.mask = chars.length - 1;
454468

469+
this.decodabet = decodabet;
470+
471+
boolean[] validPadding = new boolean[charsPerChunk];
472+
for (int i = 0; i < bytesPerChunk; i++) {
473+
validPadding[divide(i * 8, bitsPerChar, CEILING)] = true;
474+
}
475+
this.validPadding = validPadding;
476+
this.ignoreCase = ignoreCase;
477+
}
478+
479+
private static byte[] decodabetFor(char[] chars) {
455480
byte[] decodabet = new byte[Ascii.MAX + 1];
456481
Arrays.fill(decodabet, (byte) -1);
457482
for (int i = 0; i < chars.length; i++) {
@@ -460,13 +485,33 @@ private static final class Alphabet {
460485
checkArgument(decodabet[c] == -1, "Duplicate character: %s", c);
461486
decodabet[c] = (byte) i;
462487
}
463-
this.decodabet = decodabet;
488+
return decodabet;
489+
}
464490

465-
boolean[] validPadding = new boolean[charsPerChunk];
466-
for (int i = 0; i < bytesPerChunk; i++) {
467-
validPadding[divide(i * 8, bitsPerChar, CEILING)] = true;
491+
/** Returns an equivalent {@code Alphabet} except it ignores case. */
492+
Alphabet ignoreCase() {
493+
if (ignoreCase) {
494+
return this;
468495
}
469-
this.validPadding = validPadding;
496+
497+
// We can't use .clone() because of GWT.
498+
byte[] newDecodabet = Arrays.copyOf(decodabet, decodabet.length);
499+
for (int upper = 'A'; upper <= 'Z'; upper++) {
500+
int lower = upper | 0x20;
501+
byte decodeUpper = decodabet[upper];
502+
byte decodeLower = decodabet[lower];
503+
if (decodeUpper == -1) {
504+
newDecodabet[upper] = decodeLower;
505+
} else {
506+
checkState(
507+
decodeLower == -1,
508+
"Can't ignoreCase() since '%s' and '%s' encode different values",
509+
(char) upper,
510+
(char) lower);
511+
newDecodabet[lower] = decodeUpper;
512+
}
513+
}
514+
return new Alphabet(name + ".ignoreCase()", chars, newDecodabet, /* ignoreCase= */ true);
470515
}
471516

472517
char encode(int bits) {
@@ -551,14 +596,14 @@ public String toString() {
551596
public boolean equals(@CheckForNull Object other) {
552597
if (other instanceof Alphabet) {
553598
Alphabet that = (Alphabet) other;
554-
return Arrays.equals(this.chars, that.chars);
599+
return this.ignoreCase == that.ignoreCase && Arrays.equals(this.chars, that.chars);
555600
}
556601
return false;
557602
}
558603

559604
@Override
560605
public int hashCode() {
561-
return Arrays.hashCode(chars);
606+
return Arrays.hashCode(chars) + (ignoreCase ? 1231 : 1237);
562607
}
563608
}
564609

@@ -832,6 +877,7 @@ public BaseEncoding withSeparator(String separator, int afterEveryChars) {
832877

833878
@LazyInit @CheckForNull private transient BaseEncoding upperCase;
834879
@LazyInit @CheckForNull private transient BaseEncoding lowerCase;
880+
@LazyInit @CheckForNull private transient BaseEncoding ignoreCase;
835881

836882
@Override
837883
public BaseEncoding upperCase() {
@@ -853,14 +899,24 @@ public BaseEncoding lowerCase() {
853899
return result;
854900
}
855901

902+
@Override
903+
public BaseEncoding ignoreCase() {
904+
BaseEncoding result = ignoreCase;
905+
if (result == null) {
906+
Alphabet ignore = alphabet.ignoreCase();
907+
result = ignoreCase = (ignore == alphabet) ? this : newInstance(ignore, paddingChar);
908+
}
909+
return result;
910+
}
911+
856912
BaseEncoding newInstance(Alphabet alphabet, @CheckForNull Character paddingChar) {
857913
return new StandardBaseEncoding(alphabet, paddingChar);
858914
}
859915

860916
@Override
861917
public String toString() {
862918
StringBuilder builder = new StringBuilder("BaseEncoding.");
863-
builder.append(alphabet.toString());
919+
builder.append(alphabet);
864920
if (8 % alphabet.bitsPerChar != 0) {
865921
if (paddingChar == null) {
866922
builder.append(".omitPadding()");
@@ -1170,6 +1226,11 @@ public BaseEncoding lowerCase() {
11701226
return delegate.lowerCase().withSeparator(separator, afterEveryChars);
11711227
}
11721228

1229+
@Override
1230+
public BaseEncoding ignoreCase() {
1231+
return delegate.ignoreCase().withSeparator(separator, afterEveryChars);
1232+
}
1233+
11731234
@Override
11741235
public String toString() {
11751236
return delegate + ".withSeparator(\"" + separator + "\", " + afterEveryChars + ")";

0 commit comments

Comments
 (0)
Please sign in to comment.