diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java index 10b86d8093..75d47cc57e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java @@ -27,6 +27,8 @@ import java.nio.charset.StandardCharsets; */ final class AsciiBytes { + private static final int[] EXCESS = { 0x0, 0x1080, 0x96, 0x1c82080 }; + private final byte[] bytes; private final int offset; @@ -118,36 +120,56 @@ final class AsciiBytes { return new AsciiBytes(this.bytes, this.offset + beginIndex, length); } - public AsciiBytes append(String string) { - if (string == null || string.isEmpty()) { - return this; + @Override + public String toString() { + if (this.string == null) { + this.string = new String(this.bytes, this.offset, this.length, + StandardCharsets.UTF_8); } - return append(string.getBytes(StandardCharsets.UTF_8)); + return this.string; } - public AsciiBytes append(AsciiBytes asciiBytes) { - if (asciiBytes == null || asciiBytes.length() == 0) { - return this; + public boolean matches(CharSequence name, char suffix) { + int charIndex = 0; + int nameLen = name.length(); + int totalLen = (nameLen + (suffix == 0 ? 0 : 1)); + for (int i = this.offset; i < this.offset + this.length; i++) { + int b = this.bytes[i]; + if (b < 0) { + b = b & 0x7F; + int limit = getRemainingUtfBytes(b); + for (int j = 0; j < limit; j++) { + b = (b << 6) + (this.bytes[++i] & 0xFF); + } + b -= EXCESS[limit]; + } + char c = getChar(name, suffix, charIndex++); + if (b <= 0xFFFF) { + if (c != b) { + return false; + } + } + else { + if (c != ((b >> 0xA) + 0xD7C0)) { + return false; + } + c = getChar(name, suffix, charIndex++); + if (c != ((b & 0x3FF) + 0xDC00)) { + return false; + } + } } - return append(asciiBytes.bytes); + return charIndex == totalLen; } - public AsciiBytes append(byte[] bytes) { - if (bytes == null || bytes.length == 0) { - return this; + private char getChar(CharSequence name, char suffix, int index) { + if (index < name.length()) { + return name.charAt(index); } - byte[] combined = new byte[this.length + bytes.length]; - System.arraycopy(this.bytes, this.offset, combined, 0, this.length); - System.arraycopy(bytes, 0, combined, this.length, bytes.length); - return new AsciiBytes(combined); - } - - @Override - public String toString() { - if (this.string == null) { - this.string = new String(this.bytes, this.offset, this.length, StandardCharsets.UTF_8); + if (index == name.length()) { + return suffix; } - return this.string; + return 0; } @Override @@ -158,24 +180,11 @@ final class AsciiBytes { int b = this.bytes[i]; if (b < 0) { b = b & 0x7F; - int limit; - int excess = 0x80; - if (b < 96) { - limit = 1; - excess += 0x40 << 6; - } - else if (b < 112) { - limit = 2; - excess += (0x60 << 12) + (0x80 << 6); - } - else { - limit = 3; - excess += (0x70 << 18) + (0x80 << 12) + (0x80 << 6); - } + int limit = getRemainingUtfBytes(b); for (int j = 0; j < limit; j++) { b = (b << 6) + (this.bytes[++i] & 0xFF); } - b -= excess; + b -= EXCESS[limit]; } if (b <= 0xFFFF) { hash = 31 * hash + b; @@ -190,6 +199,10 @@ final class AsciiBytes { return hash; } + private int getRemainingUtfBytes(int b) { + return (b < 96 ? 1 : (b < 112 ? 2 : 3)); + } + @Override public boolean equals(Object obj) { if (obj == null) { @@ -216,16 +229,17 @@ final class AsciiBytes { return new String(bytes, StandardCharsets.UTF_8); } - public static int hashCode(String string) { - // We're compatible with String's hashCode(). - return string.hashCode(); + public static int hashCode(CharSequence charSequence) { + // We're compatible with String's hashCode() + if (charSequence instanceof StringSequence) { + // ... but save making an unnecessary String for StringSequence + return charSequence.hashCode(); + } + return charSequence.toString().hashCode(); } - public static int hashCode(int hash, String string) { - for (int i = 0; i < string.length(); i++) { - hash = 31 * hash + string.charAt(i); - } - return hash; + public static int hashCode(int hash, char suffix) { + return (suffix == 0 ? hash : (31 * hash + suffix)); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java index 5177f41d58..8bb61a66b0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java @@ -101,8 +101,8 @@ final class CentralDirectoryFileHeader implements FileHeader { } @Override - public boolean hasName(String name, String suffix) { - return this.name.equals(new AsciiBytes(suffix == null ? name : name + suffix)); + public boolean hasName(CharSequence name, char suffix) { + return this.name.matches(name, suffix); } public boolean isDirectory() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java index 5cff0a3583..7ecac051d4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java @@ -30,10 +30,10 @@ interface FileHeader { /** * Returns {@code true} if the header has the given name. * @param name the name to test - * @param suffix an additional suffix (or {@code null}) + * @param suffix an additional suffix (or {@code 0}) * @return {@code true} if the header has the given name */ - boolean hasName(String name, String suffix); + boolean hasName(CharSequence name, char suffix); /** * Return the offset of the load file header within the archive data. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java index 68a5634a35..945bac8fd0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java @@ -31,6 +31,8 @@ import java.util.jar.Manifest; */ class JarEntry extends java.util.jar.JarEntry implements FileHeader { + private final AsciiBytes name; + private Certificate[] certificates; private CodeSigner[] codeSigners; @@ -41,6 +43,7 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader { JarEntry(JarFile jarFile, CentralDirectoryFileHeader header) { super(header.getName().toString()); + this.name = header.getName(); this.jarFile = jarFile; this.localHeaderOffset = header.getLocalHeaderOffset(); setCompressedSize(header.getCompressedSize()); @@ -53,10 +56,13 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader { setTime(header.getTime()); } + AsciiBytes getAsciiBytesName() { + return this.name; + } + @Override - public boolean hasName(String name, String suffix) { - return getName().length() == name.length() + suffix.length() - && getName().startsWith(name) && getName().endsWith(suffix); + public boolean hasName(CharSequence name, char suffix) { + return this.name.matches(name, suffix); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java index 7fca918fc7..e01c965b8c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java @@ -191,6 +191,10 @@ public class JarFile extends java.util.jar.JarFile { }; } + public JarEntry getJarEntry(CharSequence name) { + return this.entries.getEntry(name); + } + @Override public JarEntry getJarEntry(String name) { return (JarEntry) getEntry(name); @@ -228,8 +232,7 @@ public class JarFile extends java.util.jar.JarFile { * @return a {@link JarFile} for the entry * @throws IOException if the nested jar file cannot be read */ - public synchronized JarFile getNestedJarFile(ZipEntry entry) - throws IOException { + public synchronized JarFile getNestedJarFile(ZipEntry entry) throws IOException { return getNestedJarFile((JarEntry) entry); } @@ -257,16 +260,16 @@ public class JarFile extends java.util.jar.JarFile { } private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException { - final AsciiBytes sourceName = new AsciiBytes(entry.getName()); - JarEntryFilter filter = (name) -> { - if (name.startsWith(sourceName) && !name.equals(sourceName)) { - return name.substring(sourceName.length()); + AsciiBytes name = entry.getAsciiBytesName(); + JarEntryFilter filter = (candidate) -> { + if (candidate.startsWith(name) && !candidate.equals(name)) { + return candidate.substring(name.length()); } return null; }; return new JarFile(this.rootFile, this.pathFromRoot + "!/" - + entry.getName().substring(0, sourceName.length() - 1), + + entry.getName().substring(0, name.length() - 1), this.data, filter, JarFileType.NESTED_DIRECTORY); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java index a8a93200c3..f583172a47 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java @@ -46,9 +46,9 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { private static final long LOCAL_FILE_HEADER_SIZE = 30; - private static final String SLASH = "/"; + private static final char SLASH = '/'; - private static final String NO_SUFFIX = ""; + private static final char NO_SUFFIX = 0; protected static final int ENTRY_CACHE_SIZE = 25; @@ -166,11 +166,11 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { return new EntryIterator(); } - public boolean containsEntry(String name) { + public boolean containsEntry(CharSequence name) { return getEntry(name, FileHeader.class, true) != null; } - public JarEntry getEntry(String name) { + public JarEntry getEntry(CharSequence name) { return getEntry(name, JarEntry.class, true); } @@ -213,7 +213,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { + nameLength + extraLength, entry.getCompressedSize()); } - private T getEntry(String name, Class type, + private T getEntry(CharSequence name, Class type, boolean cacheEntry) { int hashCode = AsciiBytes.hashCode(name); T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry); @@ -224,8 +224,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { return entry; } - private T getEntry(int hashCode, String name, String suffix, - Class type, boolean cacheEntry) { + private T getEntry(int hashCode, CharSequence name, + char suffix, Class type, boolean cacheEntry) { int index = getFirstIndex(hashCode); while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { T entry = getEntry(index, type, cacheEntry); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java index dbd7a4fdb3..c1ef506b5e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java @@ -68,7 +68,8 @@ final class JarURLConnection extends java.net.JarURLConnection { } } - private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName(""); + private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName( + new StringSequence("")); private static final String READ_ACTION = "read"; @@ -254,17 +255,17 @@ final class JarURLConnection extends java.net.JarURLConnection { } static JarURLConnection get(URL url, JarFile jarFile) throws IOException { - String spec = extractFullSpec(url, jarFile.getPathFromRoot()); + StringSequence spec = new StringSequence(url.getFile()); + int index = indexOfRootSpec(spec, jarFile.getPathFromRoot()); int separator; - int index = 0; while ((separator = spec.indexOf(SEPARATOR, index)) > 0) { - String entryName = spec.substring(index, separator); + StringSequence entryName = spec.subSequence(index, separator); JarEntry jarEntry = jarFile.getJarEntry(entryName); if (jarEntry == null) { return JarURLConnection.notFound(jarFile, JarEntryName.get(entryName)); } jarFile = jarFile.getNestedJarFile(jarEntry); - index += separator + SEPARATOR.length(); + index = separator + SEPARATOR.length(); } JarEntryName jarEntryName = JarEntryName.get(spec, index); if (Boolean.TRUE.equals(useFastExceptions.get())) { @@ -276,14 +277,12 @@ final class JarURLConnection extends java.net.JarURLConnection { return new JarURLConnection(url, jarFile, jarEntryName); } - private static String extractFullSpec(URL url, String pathFromRoot) { - String file = url.getFile(); + private static int indexOfRootSpec(StringSequence file, String pathFromRoot) { int separatorIndex = file.indexOf(SEPARATOR); if (separatorIndex < 0) { - return ""; + return -1; } - int specIndex = separatorIndex + SEPARATOR.length() + pathFromRoot.length(); - return file.substring(specIndex); + return separatorIndex + SEPARATOR.length() + pathFromRoot.length(); } private static JarURLConnection notFound() { @@ -308,22 +307,22 @@ final class JarURLConnection extends java.net.JarURLConnection { */ static class JarEntryName { - private final String name; + private final StringSequence name; private String contentType; - JarEntryName(String spec) { + JarEntryName(StringSequence spec) { this.name = decode(spec); } - private String decode(String source) { + private StringSequence decode(StringSequence source) { if (source.isEmpty() || (source.indexOf('%') < 0)) { return source; } ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length()); - write(source, bos); + write(source.toString(), bos); // AsciiBytes is what is used to store the JarEntries so make it symmetric - return AsciiBytes.toString(bos.toByteArray()); + return new StringSequence(AsciiBytes.toString(bos.toByteArray())); } private void write(String source, ByteArrayOutputStream outputStream) { @@ -367,7 +366,7 @@ final class JarURLConnection extends java.net.JarURLConnection { @Override public String toString() { - return this.name; + return this.name.toString(); } public boolean isEmpty() { @@ -389,15 +388,15 @@ final class JarURLConnection extends java.net.JarURLConnection { return type; } - public static JarEntryName get(String spec) { + public static JarEntryName get(StringSequence spec) { return get(spec, 0); } - public static JarEntryName get(String spec, int beginIndex) { + public static JarEntryName get(StringSequence spec, int beginIndex) { if (spec.length() <= beginIndex) { return EMPTY_JAR_ENTRY_NAME; } - return new JarEntryName(spec.substring(beginIndex)); + return new JarEntryName(spec.subSequence(beginIndex)); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java new file mode 100644 index 0000000000..8a6ab4620d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java @@ -0,0 +1,138 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.util.Objects; + +/** + * A {@link CharSequence} backed by a single shared {@link String}. Unlike a regular + * {@link String}, {@link #subSequence(int, int)} operations will not copy the underlying + * character array. + * + * @author Phillip Webb + */ +final class StringSequence implements CharSequence { + + private final String source; + + private final int start; + + private final int end; + + private int hash; + + StringSequence(String source) { + this(source, 0, (source == null ? -1 : source.length())); + } + + StringSequence(String source, int start, int end) { + Objects.requireNonNull(source, "Source must not be null"); + if (start < 0) { + throw new StringIndexOutOfBoundsException(start); + } + if (end > source.length()) { + throw new StringIndexOutOfBoundsException(end); + } + this.source = source; + this.start = start; + this.end = end; + } + + public StringSequence subSequence(int start) { + return subSequence(start, length()); + } + + @Override + public StringSequence subSequence(int start, int end) { + int subSequenceStart = this.start + start; + int subSequenceEnd = this.start + end; + if (subSequenceStart > this.end) { + throw new StringIndexOutOfBoundsException(start); + } + if (subSequenceEnd > this.end) { + throw new StringIndexOutOfBoundsException(end); + } + return new StringSequence(this.source, subSequenceStart, subSequenceEnd); + } + + public boolean isEmpty() { + return length() == 0; + } + + @Override + public int length() { + return this.end - this.start; + } + + @Override + public char charAt(int index) { + return this.source.charAt(this.start + index); + } + + public int indexOf(char ch) { + return this.source.indexOf(ch, this.start) - this.start; + } + + public int indexOf(String str) { + return this.source.indexOf(str, this.start) - this.start; + } + + public int indexOf(String str, int fromIndex) { + return this.source.indexOf(str, this.start + fromIndex) - this.start; + } + + @Override + public String toString() { + return this.source.substring(this.start, this.end); + } + + @Override + public int hashCode() { + int hash = this.hash; + if (hash == 0 && length() > 0) { + for (int i = this.start; i < this.end; i++) { + hash = 31 * hash + this.source.charAt(i); + } + this.hash = hash; + } + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + StringSequence other = (StringSequence) obj; + int n = length(); + if (n == other.length()) { + int i = 0; + while (n-- != 0) { + if (charAt(i) != other.charAt(i)) { + return false; + } + i++; + } + return true; + } + return true; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java index 947f988b8e..e26cb17096 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java @@ -30,6 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class AsciiBytesTests { + private static final char NO_SUFFIX = 0; + @Rule public ExpectedException thrown = ExpectedException.none(); @@ -106,22 +108,6 @@ public class AsciiBytesTests { abcd.substring(3, 5); } - @Test - public void appendString() { - AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2); - AsciiBytes appended = bc.append("D"); - assertThat(bc.toString()).isEqualTo("BC"); - assertThat(appended.toString()).isEqualTo("BCD"); - } - - @Test - public void appendBytes() { - AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2); - AsciiBytes appended = bc.append(new byte[] { 68 }); - assertThat(bc.toString()).isEqualTo("BC"); - assertThat(appended.toString()).isEqualTo("BCD"); - } - @Test public void hashCodeAndEquals() { AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); @@ -163,4 +149,42 @@ public class AsciiBytesTests { assertThat(new AsciiBytes(input).hashCode()).isEqualTo(input.hashCode()); } + @Test + public void matchesSameAsString() { + matchesSameAsString("abcABC123xyz!"); + } + + @Test + public void matchesSameAsStringWithSpecial() { + matchesSameAsString("special/\u00EB.dat"); + } + + @Test + public void matchesSameAsStringWithCyrillicCharacters() { + matchesSameAsString("\u0432\u0435\u0441\u043D\u0430"); + } + + @Test + public void matchesDifferentLengths() { + assertThat(new AsciiBytes("abc").matches("ab", NO_SUFFIX)).isFalse(); + assertThat(new AsciiBytes("abc").matches("abcd", NO_SUFFIX)).isFalse(); + assertThat(new AsciiBytes("abc").matches("abc", NO_SUFFIX)).isTrue(); + assertThat(new AsciiBytes("abc").matches("a", 'b')).isFalse(); + assertThat(new AsciiBytes("abc").matches("abc", 'd')).isFalse(); + assertThat(new AsciiBytes("abc").matches("ab", 'c')).isTrue(); + } + + @Test + public void matchesSuffix() { + assertThat(new AsciiBytes("ab").matches("a", 'b')).isTrue(); + } + + @Test + public void matchesSameAsStringWithEmoji() { + matchesSameAsString("\ud83d\udca9"); + } + + private void matchesSameAsString(String input) { + assertThat(new AsciiBytes(input).matches(input, NO_SUFFIX)).isTrue(); + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarEntryNameTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarEntryNameTests.java deleted file mode 100644 index 97ec03e2e0..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarEntryNameTests.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2012-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.loader.jar; - -import org.junit.Test; - -import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link JarEntryName}. - * - * @author Andy Wilkinson - */ -public class JarEntryNameTests { - - @Test - public void basicName() { - assertThat(new JarEntryName("a/b/C.class").toString()).isEqualTo("a/b/C.class"); - } - - @Test - public void nameWithSingleByteEncodedCharacters() { - assertThat(new JarEntryName("%61/%62/%43.class").toString()) - .isEqualTo("a/b/C.class"); - } - - @Test - public void nameWithDoubleByteEncodedCharacters() { - assertThat(new JarEntryName("%c3%a1/b/C.class").toString()) - .isEqualTo("\u00e1/b/C.class"); - } - - @Test - public void nameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() { - assertThat(new JarEntryName("%c3%a1/b/\u00c7.class").toString()) - .isEqualTo("\u00e1/b/\u00c7.class"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java index aec69d11eb..5c8234a5ec 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java @@ -26,6 +26,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.springframework.boot.loader.TestJarCreator; +import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName; import static org.assertj.core.api.Assertions.assertThat; @@ -155,6 +156,31 @@ public class JarURLConnectionTests { .isEqualTo(connection.getJarEntry().getTime()); } + @Test + public void jarEntryBasicName() { + assertThat(new JarEntryName(new StringSequence("a/b/C.class")).toString()) + .isEqualTo("a/b/C.class"); + } + + @Test + public void jarEntryNameWithSingleByteEncodedCharacters() { + assertThat(new JarEntryName(new StringSequence("%61/%62/%43.class")).toString()) + .isEqualTo("a/b/C.class"); + } + + @Test + public void jarEntryNameWithDoubleByteEncodedCharacters() { + assertThat(new JarEntryName(new StringSequence("%c3%a1/b/C.class")).toString()) + .isEqualTo("\u00e1/b/C.class"); + } + + @Test + public void jarEntryNameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() { + assertThat( + new JarEntryName(new StringSequence("%c3%a1/b/\u00c7.class")).toString()) + .isEqualTo("\u00e1/b/\u00c7.class"); + } + private String getAbsolutePath() { return this.rootJarFile.getAbsolutePath().replace('\\', '/'); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java new file mode 100644 index 0000000000..0ad73a8c14 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StringSequence}. + * + * @author Phillip Webb + */ +public class StringSequenceTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenSourceIsNullShouldThrowException() { + this.thrown.expect(NullPointerException.class); + this.thrown.expectMessage("Source must not be null"); + new StringSequence(null); + } + + @Test + public void createWithIndexWhenSourceIsNullShouldThrowException() { + this.thrown.expect(NullPointerException.class); + this.thrown.expectMessage("Source must not be null"); + new StringSequence(null, 0, 0); + } + + @Test + public void createWhenStartIsLessThanZeroShouldThrowException() { + this.thrown.expect(StringIndexOutOfBoundsException.class); + new StringSequence("x", -1, 0); + } + + @Test + public void createWhenEndIsGreaterThanLengthShouldThrowException() { + this.thrown.expect(StringIndexOutOfBoundsException.class); + new StringSequence("x", 0, 2); + } + + @Test + public void creatFromString() { + assertThat(new StringSequence("test").toString()).isEqualTo("test"); + } + + @Test + public void subSequenceWithJustStartShouldReturnSubSequence() { + assertThat(new StringSequence("smiles").subSequence(1).toString()) + .isEqualTo("miles"); + } + + @Test + public void subSequenceShouldReturnSubSequence() { + assertThat(new StringSequence("hamburger").subSequence(4, 8).toString()) + .isEqualTo("urge"); + assertThat(new StringSequence("smiles").subSequence(1, 5).toString()) + .isEqualTo("mile"); + } + + @Test + public void subSequenceWhenCalledMultipleTimesShouldReturnSubSequence() { + assertThat(new StringSequence("hamburger").subSequence(4, 8).subSequence(1, 3) + .toString()).isEqualTo("rg"); + } + + @Test + public void subSequenceWhenEndPastExistingEndShouldThrowException() { + StringSequence sequence = new StringSequence("abcde").subSequence(1, 4); + assertThat(sequence.toString()).isEqualTo("bcd"); + assertThat(sequence.subSequence(2, 3).toString()).isEqualTo("d"); + this.thrown.expect(IndexOutOfBoundsException.class); + sequence.subSequence(3, 4); + } + + @Test + public void subSequenceWhenStartPastExistingEndShouldThrowException() { + StringSequence sequence = new StringSequence("abcde").subSequence(1, 4); + assertThat(sequence.toString()).isEqualTo("bcd"); + assertThat(sequence.subSequence(2, 3).toString()).isEqualTo("d"); + this.thrown.expect(IndexOutOfBoundsException.class); + sequence.subSequence(4, 3); + } + + @Test + public void isEmptyWhenEmptyShouldReturnTrue() { + assertThat(new StringSequence("").isEmpty()).isTrue(); + } + + @Test + public void isEmptyWhenNotEmptyShouldReturnFalse() { + assertThat(new StringSequence("x").isEmpty()).isFalse(); + } + + @Test + public void lengthShouldReturnLength() { + StringSequence sequence = new StringSequence("hamburger"); + assertThat(sequence.length()).isEqualTo(9); + assertThat(sequence.subSequence(4, 8).length()).isEqualTo(4); + } + + @Test + public void charAtShouldReturnChar() { + StringSequence sequence = new StringSequence("hamburger"); + assertThat(sequence.charAt(0)).isEqualTo('h'); + assertThat(sequence.charAt(1)).isEqualTo('a'); + assertThat(sequence.subSequence(4, 8).charAt(0)).isEqualTo('u'); + assertThat(sequence.subSequence(4, 8).charAt(1)).isEqualTo('r'); + } + + @Test + public void indexOfCharShouldReturnIndexOf() { + StringSequence sequence = new StringSequence("aabbaacc"); + assertThat(sequence.indexOf('a')).isEqualTo(0); + assertThat(sequence.indexOf('b')).isEqualTo(2); + assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2); + } + + @Test + public void indexOfStringShouldReturnIndexOf() { + StringSequence sequence = new StringSequence("aabbaacc"); + assertThat(sequence.indexOf("a")).isEqualTo(0); + assertThat(sequence.indexOf("b")).isEqualTo(2); + assertThat(sequence.subSequence(2).indexOf("a")).isEqualTo(2); + } + + @Test + public void indexOfStringFromIndexShouldReturnIndexOf() { + StringSequence sequence = new StringSequence("aabbaacc"); + assertThat(sequence.indexOf("a", 2)).isEqualTo(4); + assertThat(sequence.indexOf("b", 3)).isEqualTo(3); + assertThat(sequence.subSequence(2).indexOf("a", 3)).isEqualTo(3); + } + + @Test + public void hashCodeShouldBeSameAsString() { + assertThat(new StringSequence("hamburger").hashCode()) + .isEqualTo("hamburger".hashCode()); + assertThat(new StringSequence("hamburger").subSequence(4, 8).hashCode()) + .isEqualTo("urge".hashCode()); + } + + @Test + public void equalsWhenSameContentShouldMatch() { + StringSequence a = new StringSequence("hamburger").subSequence(4, 8); + StringSequence b = new StringSequence("urge"); + StringSequence c = new StringSequence("urgh"); + assertThat(a).isEqualTo(b).isNotEqualTo(c); + } + +}