This is an automated email from the ASF dual-hosted git repository. elecharny pushed a commit to branch 2.2.X in repository https://gitbox.apache.org/repos/asf/mina.git
commit 691a9df5a0aff0dddeedc5181f6e5832ee90dcea Author: emmanuel lecharny <elecha...@apache.org> AuthorDate: Mon Nov 4 12:06:06 2024 +0100 pom.xml --- mina-core/pom.xml | 2 +- .../apache/mina/core/buffer/AbstractIoBuffer.java | 79 +++- .../java/org/apache/mina/core/buffer/IoBuffer.java | 30 ++ .../apache/mina/core/buffer/IoBufferWrapper.java | 29 ++ .../mina/core/buffer/matcher/ClassNameMatcher.java | 32 ++ .../mina/core/buffer/matcher/FileSystem.java | 526 +++++++++++++++++++++ .../mina/core/buffer/matcher/FilenameUtils.java | 174 +++++++ .../core/buffer/matcher/FullClassNameMatcher.java | 48 ++ .../apache/mina/core/buffer/matcher/IOCase.java | 275 +++++++++++ .../buffer/matcher/RegexpClassNameMatcher.java | 56 +++ .../buffer/matcher/WildcardClassNameMatcher.java | 45 ++ .../org/apache/mina/core/buffer/IoBufferTest.java | 6 +- pom.xml | 13 +- 13 files changed, 1308 insertions(+), 7 deletions(-) diff --git a/mina-core/pom.xml b/mina-core/pom.xml index 73c89ef53..c3d5a1b20 100644 --- a/mina-core/pom.xml +++ b/mina-core/pom.xml @@ -32,6 +32,7 @@ <packaging>bundle</packaging> <dependencies> + <!-- Test dependencies --> <dependency> <groupId>org.easymock</groupId> <artifactId>easymock</artifactId> @@ -112,4 +113,3 @@ </plugins> </build> </project> - diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/AbstractIoBuffer.java b/mina-core/src/main/java/org/apache/mina/core/buffer/AbstractIoBuffer.java index 54d068c4f..bd80469e9 100644 --- a/mina-core/src/main/java/org/apache/mina/core/buffer/AbstractIoBuffer.java +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/AbstractIoBuffer.java @@ -43,8 +43,18 @@ import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.EnumSet; +import java.util.List; import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.apache.mina.core.buffer.matcher.ClassNameMatcher; +import org.apache.mina.core.buffer.matcher.FullClassNameMatcher; +import org.apache.mina.core.buffer.matcher.RegexpClassNameMatcher; +import org.apache.mina.core.buffer.matcher.WildcardClassNameMatcher; + /** * A base implementation of {@link IoBuffer}. This implementation assumes that @@ -80,6 +90,9 @@ public abstract class AbstractIoBuffer extends IoBuffer { /** A mask for an int */ private static final long INT_MASK = 0xFFFFFFFFL; + private final List<ClassNameMatcher> acceptMatchers = new ArrayList<>(); + private final List<ClassNameMatcher> rejectMatchers = new ArrayList<>(); + /** * We don't have any access to Buffer.markValue(), so we need to track it down, * which will cause small extra overhead. @@ -2182,6 +2195,8 @@ public abstract class AbstractIoBuffer extends IoBuffer { @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { Class<?> clazz = desc.forClass(); + + String[] classes = new String[] {"java.util.Date", "long", "java.util.ArrayList"}; if (clazz == null) { String name = desc.getName(); @@ -2191,10 +2206,25 @@ public abstract class AbstractIoBuffer extends IoBuffer { return super.resolveClass(desc); } } else { - return clazz; + boolean found = false; + String className = desc.getName(); + + for (ClassNameMatcher matcher : acceptMatchers) { + if (matcher.matches(className)) { + found = true; + break; + } + } + + if (found) { + return clazz; + } + + throw new ClassNotFoundException(); } } }) { + //((ValidatingObjectInputStream)in).accept(Date.class, long.class, ArrayList.class); return in.readObject(); } catch (IOException e) { throw new BufferDataException(e); @@ -2747,4 +2777,51 @@ public abstract class AbstractIoBuffer extends IoBuffer { throw new IllegalArgumentException("fieldSize cannot be negative: " + fieldSize); } } + + /** + * Accept the specified classes for deserialization, unless they + * are otherwise rejected. + * + * @param classes Classes to accept + * @return this object + */ + public IoBuffer accept(Class<?>... classes) { + for (Class<?> clazz:classes) { + acceptMatchers.add(new FullClassNameMatcher(clazz.getName())); + } + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer accept(ClassNameMatcher m) { + acceptMatchers.add(m); + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer accept(Pattern pattern) { + acceptMatchers.add(new RegexpClassNameMatcher(pattern)); + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer accept(String... patterns) { + for (String pattern:patterns) { + acceptMatchers.add(new WildcardClassNameMatcher(pattern)); + } + + return this; + } } diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/IoBuffer.java b/mina-core/src/main/java/org/apache/mina/core/buffer/IoBuffer.java index 1ac600d85..6cda800cb 100644 --- a/mina-core/src/main/java/org/apache/mina/core/buffer/IoBuffer.java +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/IoBuffer.java @@ -36,7 +36,9 @@ import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.util.EnumSet; import java.util.Set; +import java.util.regex.Pattern; +import org.apache.mina.core.buffer.matcher.ClassNameMatcher; import org.apache.mina.core.session.IoSession; /** @@ -2107,4 +2109,32 @@ public abstract class IoBuffer implements Comparable<IoBuffer> { * @return the modified IoBuffer */ public abstract <E extends Enum<E>> IoBuffer putEnumSetLong(int index, Set<E> set); + + /** + * Accept class names where the supplied ClassNameMatcher matches for + * deserialization, unless they are otherwise rejected. + * + * @param m the matcher to use + * @return this object + */ + public abstract IoBuffer accept(ClassNameMatcher m); + + /** + * Accept class names that match the supplied pattern for + * deserialization, unless they are otherwise rejected. + * + * @param pattern standard Java regexp + * @return this object + */ + public abstract IoBuffer accept(Pattern pattern); + + /** + * Accept the wildcard specified classes for deserialization, + * unless they are otherwise rejected. + * + * @param patterns Wildcard file name patterns as defined by + * {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) FilenameUtils.wildcardMatch} + * @return this object + */ + public abstract IoBuffer accept(String... patterns); } diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/IoBufferWrapper.java b/mina-core/src/main/java/org/apache/mina/core/buffer/IoBufferWrapper.java index 437483fb8..e53081103 100644 --- a/mina-core/src/main/java/org/apache/mina/core/buffer/IoBufferWrapper.java +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/IoBufferWrapper.java @@ -34,6 +34,11 @@ import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.util.Set; +import java.util.regex.Pattern; + +import org.apache.mina.core.buffer.matcher.ClassNameMatcher; +import org.apache.mina.core.buffer.matcher.RegexpClassNameMatcher; +import org.apache.mina.core.buffer.matcher.WildcardClassNameMatcher; /** * A {@link IoBuffer} that wraps a buffer and proxies any operations to it. @@ -1535,4 +1540,28 @@ public class IoBufferWrapper extends IoBuffer { buf.putUnsigned(index, value); return this; } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer accept(ClassNameMatcher m) { + return buf.accept(m); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer accept(Pattern pattern) { + return buf.accept(pattern); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer accept(String... patterns) { + return buf.accept(patterns); + } } diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/ClassNameMatcher.java b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/ClassNameMatcher.java new file mode 100644 index 000000000..44da8ff77 --- /dev/null +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/ClassNameMatcher.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.mina.core.buffer.matcher; + +/** + * An object that matches a Class name to a condition. + */ +public interface ClassNameMatcher { + /** + * Returns {@code true} if the supplied class name matches this object's condition. + * + * @param className fully qualified class name + * @return {@code true} if the class name matches this object's condition + */ + boolean matches(String className); +} \ No newline at end of file diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/FileSystem.java b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/FileSystem.java new file mode 100644 index 000000000..38212c791 --- /dev/null +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/FileSystem.java @@ -0,0 +1,526 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.mina.core.buffer.matcher; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Objects; + +/** + * Abstracts an OS' file system details, currently supporting the single use case of converting a file name String to a + * legal file name with {@link #toLegalFileName(String, char)}. + * <p> + * The starting point of any operation is {@link #getCurrent()} which gets you the enum for the file system that matches + * the OS hosting the running JVM. + * </p> + * + * @since 2.7 + */ +public enum FileSystem { + + /** + * Generic file system. + */ + GENERIC(4096, false, false, Integer.MAX_VALUE, Integer.MAX_VALUE, new int[] { 0 }, new String[] {}, false, false, '/'), + + /** + * Linux file system. + */ + LINUX(8192, true, true, 255, 4096, new int[] { + // KEEP THIS ARRAY SORTED! + // @formatter:off + // ASCII NUL + 0, + '/' + // @formatter:on + }, new String[] {}, false, false, '/'), + + /** + * MacOS file system. + */ + MAC_OSX(4096, true, true, 255, 1024, new int[] { + // KEEP THIS ARRAY SORTED! + // @formatter:off + // ASCII NUL + 0, + '/', + ':' + // @formatter:on + }, new String[] {}, false, false, '/'), + + /** + * Windows file system. + * <p> + * The reserved characters are defined in the + * <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions + * (microsoft.com)</a>. + * </p> + * + * @see <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions + * (microsoft.com)</a> + * @see <a href="https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles"> + * CreateFileA function - Consoles (microsoft.com)</a> + */ + WINDOWS(4096, false, true, + 255, 32000, // KEEP THIS ARRAY SORTED! + new int[] { + // KEEP THIS ARRAY SORTED! + // @formatter:off + // ASCII NUL + 0, + // 1-31 may be allowed in file streams + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 30, 31, + '"', '*', '/', ':', '<', '>', '?', '\\', '|' + // @formatter:on + }, new String[] { "AUX", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "CONIN$", "CONOUT$", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL", "PRN" }, true, true, '\\'); + + /** + * <p> + * Is {@code true} if this is Linux. + * </p> + * <p> + * The field will return {@code false} if {@code OS_NAME} is {@code null}. + * </p> + */ + private static final boolean IS_OS_LINUX = getOsMatchesName("Linux"); + + /** + * <p> + * Is {@code true} if this is Mac. + * </p> + * <p> + * The field will return {@code false} if {@code OS_NAME} is {@code null}. + * </p> + */ + private static final boolean IS_OS_MAC = getOsMatchesName("Mac"); + + /** + * The prefix String for all Windows OS. + */ + private static final String OS_NAME_WINDOWS_PREFIX = "Windows"; + + /** + * <p> + * Is {@code true} if this is Windows. + * </p> + * <p> + * The field will return {@code false} if {@code OS_NAME} is {@code null}. + * </p> + */ + private static final boolean IS_OS_WINDOWS = getOsMatchesName(OS_NAME_WINDOWS_PREFIX); + + /** + * The current FileSystem. + */ + private static final FileSystem CURRENT = current(); + + /** + * Gets the current file system. + * + * @return the current file system + */ + private static FileSystem current() { + if (IS_OS_LINUX) { + return LINUX; + } + if (IS_OS_MAC) { + return MAC_OSX; + } + if (IS_OS_WINDOWS) { + return WINDOWS; + } + return GENERIC; + } + + /** + * Gets the current file system. + * + * @return the current file system + */ + public static FileSystem getCurrent() { + return CURRENT; + } + + /** + * Decides if the operating system matches. + * + * @param osNamePrefix + * the prefix for the os name + * @return true if matches, or false if not or can't determine + */ + private static boolean getOsMatchesName(final String osNamePrefix) { + return isOsNameMatch(getSystemProperty("os.name"), osNamePrefix); + } + + /** + * <p> + * Gets a System property, defaulting to {@code null} if the property cannot be read. + * </p> + * <p> + * If a {@link SecurityException} is caught, the return value is {@code null} and a message is written to + * {@code System.err}. + * </p> + * + * @param property + * the system property name + * @return the system property value or {@code null} if a security problem occurs + */ + private static String getSystemProperty(final String property) { + try { + return System.getProperty(property); + } catch (final SecurityException ex) { + // we are not allowed to look at this property + System.err.println("Caught a SecurityException reading the system property '" + property + + "'; the SystemUtils property value will default to null."); + return null; + } + } + + /** + * Copied from Apache Commons Lang CharSequenceUtils. + * + * Returns the index within {@code cs} of the first occurrence of the + * specified character, starting the search at the specified index. + * <p> + * If a character with value {@code searchChar} occurs in the + * character sequence represented by the {@code cs} + * object at an index no smaller than {@code start}, then + * the index of the first such occurrence is returned. For values + * of {@code searchChar} in the range from 0 to 0xFFFF (inclusive), + * this is the smallest value <i>k</i> such that: + * </p> + * <blockquote><pre> + * (this.charAt(<i>k</i>) == searchChar) && (<i>k</i> >= start) + * </pre></blockquote> + * is true. For other values of {@code searchChar}, it is the + * smallest value <i>k</i> such that: + * <blockquote><pre> + * (this.codePointAt(<i>k</i>) == searchChar) && (<i>k</i> >= start) + * </pre></blockquote> + * <p> + * is true. In either case, if no such character occurs inm {@code cs} + * at or after position {@code start}, then + * {@code -1} is returned. + * </p> + * <p> + * There is no restriction on the value of {@code start}. If it + * is negative, it has the same effect as if it were zero: the entire + * {@link CharSequence} may be searched. If it is greater than + * the length of {@code cs}, it has the same effect as if it were + * equal to the length of {@code cs}: {@code -1} is returned. + * </p> + * <p>All indices are specified in {@code char} values + * (Unicode code units). + * </p> + * + * @param cs the {@link CharSequence} to be processed, not null + * @param searchChar the char to be searched for + * @param start the start index, negative starts at the string start + * @return the index where the search char was found, -1 if not found + * @since 3.6 updated to behave more like {@link String} + */ + private static int indexOf(final CharSequence cs, final int searchChar, int start) { + if (cs instanceof String) { + return ((String) cs).indexOf(searchChar, start); + } + final int sz = cs.length(); + if (start < 0) { + start = 0; + } + if (searchChar < Character.MIN_SUPPLEMENTARY_CODE_POINT) { + for (int i = start; i < sz; i++) { + if (cs.charAt(i) == searchChar) { + return i; + } + } + return -1; + } + //supplementary characters (LANG1300) + if (searchChar <= Character.MAX_CODE_POINT) { + final char[] chars = Character.toChars(searchChar); + for (int i = start; i < sz - 1; i++) { + final char high = cs.charAt(i); + final char low = cs.charAt(i + 1); + if (high == chars[0] && low == chars[1]) { + return i; + } + } + } + return -1; + } + + /** + * Decides if the operating system matches. + * <p> + * This method is package private instead of private to support unit test invocation. + * </p> + * + * @param osName + * the actual OS name + * @param osNamePrefix + * the prefix for the expected OS name + * @return true if matches, or false if not or can't determine + */ + private static boolean isOsNameMatch(final String osName, final String osNamePrefix) { + if (osName == null) { + return false; + } + return osName.toUpperCase(Locale.ROOT).startsWith(osNamePrefix.toUpperCase(Locale.ROOT)); + } + + /** + * Null-safe replace. + * + * @param path the path to be changed, null ignored. + * @param oldChar the old character. + * @param newChar the new character. + * @return the new path. + */ + private static String replace(final String path, final char oldChar, final char newChar) { + return path == null ? null : path.replace(oldChar, newChar); + } + + private final int blockSize; + private final boolean casePreserving; + private final boolean caseSensitive; + private final int[] illegalFileNameChars; + private final int maxFileNameLength; + private final int maxPathLength; + private final String[] reservedFileNames; + private final boolean reservedFileNamesExtensions; + private final boolean supportsDriveLetter; + private final char nameSeparator; + private final char nameSeparatorOther; + + /** + * Constructs a new instance. + * + * @param blockSize file allocation block size in bytes. + * @param caseSensitive Whether this file system is case-sensitive. + * @param casePreserving Whether this file system is case-preserving. + * @param maxFileLength The maximum length for file names. The file name does not include folders. + * @param maxPathLength The maximum length of the path to a file. This can include folders. + * @param illegalFileNameChars Illegal characters for this file system. + * @param reservedFileNames The reserved file names. + * @param reservedFileNamesExtensions TODO + * @param supportsDriveLetter Whether this file system support driver letters. + * @param nameSeparator The name separator, '\\' on Windows, '/' on Linux. + */ + FileSystem(final int blockSize, final boolean caseSensitive, final boolean casePreserving, + final int maxFileLength, final int maxPathLength, final int[] illegalFileNameChars, + final String[] reservedFileNames, final boolean reservedFileNamesExtensions, final boolean supportsDriveLetter, final char nameSeparator) { + this.blockSize = blockSize; + this.maxFileNameLength = maxFileLength; + this.maxPathLength = maxPathLength; + this.illegalFileNameChars = Objects.requireNonNull(illegalFileNameChars, "illegalFileNameChars"); + this.reservedFileNames = Objects.requireNonNull(reservedFileNames, "reservedFileNames"); + this.reservedFileNamesExtensions = reservedFileNamesExtensions; + this.caseSensitive = caseSensitive; + this.casePreserving = casePreserving; + this.supportsDriveLetter = supportsDriveLetter; + this.nameSeparator = nameSeparator; + this.nameSeparatorOther = FilenameUtils.flipSeparator(nameSeparator); + } + + /** + * Gets the file allocation block size in bytes. + * @return the file allocation block size in bytes. + * + * @since 2.12.0 + */ + public int getBlockSize() { + return blockSize; + } + + /** + * Gets a cloned copy of the illegal characters for this file system. + * + * @return the illegal characters for this file system. + */ + public char[] getIllegalFileNameChars() { + final char[] chars = new char[illegalFileNameChars.length]; + for (int i = 0; i < illegalFileNameChars.length; i++) { + chars[i] = (char) illegalFileNameChars[i]; + } + return chars; + } + + /** + * Gets a cloned copy of the illegal code points for this file system. + * + * @return the illegal code points for this file system. + * @since 2.12.0 + */ + public int[] getIllegalFileNameCodePoints() { + return this.illegalFileNameChars.clone(); + } + + /** + * Gets the maximum length for file names. The file name does not include folders. + * + * @return the maximum length for file names. + */ + public int getMaxFileNameLength() { + return maxFileNameLength; + } + + /** + * Gets the maximum length of the path to a file. This can include folders. + * + * @return the maximum length of the path to a file. + */ + public int getMaxPathLength() { + return maxPathLength; + } + + /** + * Gets the name separator, '\\' on Windows, '/' on Linux. + * + * @return '\\' on Windows, '/' on Linux. + * + * @since 2.12.0 + */ + public char getNameSeparator() { + return nameSeparator; + } + + /** + * Gets a cloned copy of the reserved file names. + * + * @return the reserved file names. + */ + public String[] getReservedFileNames() { + return reservedFileNames.clone(); + } + + /** + * Tests whether this file system preserves case. + * + * @return Whether this file system preserves case. + */ + public boolean isCasePreserving() { + return casePreserving; + } + + /** + * Tests whether this file system is case-sensitive. + * + * @return Whether this file system is case-sensitive. + */ + public boolean isCaseSensitive() { + return caseSensitive; + } + + /** + * Tests if the given character is illegal in a file name, {@code false} otherwise. + * + * @param c + * the character to test + * @return {@code true} if the given character is illegal in a file name, {@code false} otherwise. + */ + private boolean isIllegalFileNameChar(final int c) { + return Arrays.binarySearch(illegalFileNameChars, c) >= 0; + } + + /** + * Tests if a candidate file name (without a path) such as {@code "filename.ext"} or {@code "filename"} is a + * potentially legal file name. If the file name length exceeds {@link #getMaxFileNameLength()}, or if it contains + * an illegal character then the check fails. + * + * @param candidate + * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} + * @return {@code true} if the candidate name is legal + */ + public boolean isLegalFileName(final CharSequence candidate) { + if (candidate == null || candidate.length() == 0 || candidate.length() > maxFileNameLength) { + return false; + } + if (isReservedFileName(candidate)) { + return false; + } + return candidate.chars().noneMatch(this::isIllegalFileNameChar); + } + + /** + * Tests whether the given string is a reserved file name. + * + * @param candidate + * the string to test + * @return {@code true} if the given string is a reserved file name. + */ + public boolean isReservedFileName(final CharSequence candidate) { + final CharSequence test = reservedFileNamesExtensions ? trimExtension(candidate) : candidate; + return Arrays.binarySearch(reservedFileNames, test) >= 0; + } + + /** + * Converts all separators to the Windows separator of backslash. + * + * @param path the path to be changed, null ignored + * @return the updated path + * @since 2.12.0 + */ + public String normalizeSeparators(final String path) { + return replace(path, nameSeparatorOther, nameSeparator); + } + + /** + * Tests whether this file system support driver letters. + * <p> + * Windows supports driver letters as do other operating systems. Whether these other OS's still support Java like + * OS/2, is a different matter. + * </p> + * + * @return whether this file system support driver letters. + * @since 2.9.0 + * @see <a href="https://en.wikipedia.org/wiki/Drive_letter_assignment">Operating systems that use drive letter + * assignment</a> + */ + public boolean supportsDriveLetter() { + return supportsDriveLetter; + } + + /** + * Converts a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} to a legal file + * name. Illegal characters in the candidate name are replaced by the {@code replacement} character. If the file + * name length exceeds {@link #getMaxFileNameLength()}, then the name is truncated to + * {@link #getMaxFileNameLength()}. + * + * @param candidate + * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} + * @param replacement + * Illegal characters in the candidate name are replaced by this character + * @return a String without illegal characters + */ + public String toLegalFileName(final String candidate, final char replacement) { + if (isIllegalFileNameChar(replacement)) { + // %s does not work properly with NUL + throw new IllegalArgumentException(String.format("The replacement character '%s' cannot be one of the %s illegal characters: %s", + replacement == '\0' ? "\\0" : replacement, name(), Arrays.toString(illegalFileNameChars))); + } + final String truncated = candidate.length() > maxFileNameLength ? candidate.substring(0, maxFileNameLength) : candidate; + final int[] array = truncated.chars().map(i -> isIllegalFileNameChar(i) ? replacement : i).toArray(); + return new String(array, 0, array.length); + } + + CharSequence trimExtension(final CharSequence cs) { + final int index = indexOf(cs, '.', 0); + return index < 0 ? cs : cs.subSequence(0, index); + } +} diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/FilenameUtils.java b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/FilenameUtils.java new file mode 100644 index 000000000..9ff67ca05 --- /dev/null +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/FilenameUtils.java @@ -0,0 +1,174 @@ +package org.apache.mina.core.buffer.matcher; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; + +public class FilenameUtils +{ + private static final int NOT_FOUND = -1; + + private static final String[] EMPTY_STRING_ARRAY = {}; + + /** + * The Unix separator character. + */ + private static final char UNIX_NAME_SEPARATOR = '/'; + + /** + * The Windows separator character. + */ + private static final char WINDOWS_NAME_SEPARATOR = '\\'; + + /** + * Checks a fileName to see if it matches the specified wildcard matcher + * allowing control over case-sensitivity. + * <p> + * The wildcard matcher uses the characters '?' and '*' to represent a + * single or multiple (zero or more) wildcard characters. + * N.B. the sequence "*?" does not work properly at present in match strings. + * + * @param fileName the fileName to match on + * @param wildcardMatcher the wildcard string to match against + * @param ioCase what case sensitivity rule to use, null means case-sensitive + * @return true if the fileName matches the wildcard string + * @since 1.3 + */ + public static boolean wildcardMatch(final String fileName, final String wildcardMatcher, IOCase ioCase) { + if (fileName == null && wildcardMatcher == null) { + return true; + } + if (fileName == null || wildcardMatcher == null) { + return false; + } + ioCase = IOCase.value(ioCase, IOCase.SENSITIVE); + final String[] wcs = splitOnTokens(wildcardMatcher); + boolean anyChars = false; + int textIdx = 0; + int wcsIdx = 0; + final Deque<int[]> backtrack = new ArrayDeque<>(wcs.length); + + // loop around a backtrack stack, to handle complex * matching + do { + if (!backtrack.isEmpty()) { + final int[] array = backtrack.pop(); + wcsIdx = array[0]; + textIdx = array[1]; + anyChars = true; + } + + // loop whilst tokens and text left to process + while (wcsIdx < wcs.length) { + + if (wcs[wcsIdx].equals("?")) { + // ? so move to next text char + textIdx++; + if (textIdx > fileName.length()) { + break; + } + anyChars = false; + + } else if (wcs[wcsIdx].equals("*")) { + // set any chars status + anyChars = true; + if (wcsIdx == wcs.length - 1) { + textIdx = fileName.length(); + } + + } else { + // matching text token + if (anyChars) { + // any chars then try to locate text token + textIdx = ioCase.checkIndexOf(fileName, textIdx, wcs[wcsIdx]); + if (textIdx == NOT_FOUND) { + // token not found + break; + } + final int repeat = ioCase.checkIndexOf(fileName, textIdx + 1, wcs[wcsIdx]); + if (repeat >= 0) { + backtrack.push(new int[] {wcsIdx, repeat}); + } + } else if (!ioCase.checkRegionMatches(fileName, textIdx, wcs[wcsIdx])) { + // matching from current position + // couldn't match token + break; + } + + // matched text token, move text index to end of matched token + textIdx += wcs[wcsIdx].length(); + anyChars = false; + } + + wcsIdx++; + } + + // full match + if (wcsIdx == wcs.length && textIdx == fileName.length()) { + return true; + } + + } while (!backtrack.isEmpty()); + + return false; + } + + + /** + * Splits a string into a number of tokens. + * The text is split by '?' and '*'. + * Where multiple '*' occur consecutively they are collapsed into a single '*'. + * + * @param text the text to split + * @return the array of tokens, never null + */ + static String[] splitOnTokens(final String text) { + // used by wildcardMatch + // package level so a unit test may run on this + + if (text.indexOf('?') == NOT_FOUND && text.indexOf('*') == NOT_FOUND) { + return new String[] { text }; + } + + final char[] array = text.toCharArray(); + final ArrayList<String> list = new ArrayList<>(); + final StringBuilder buffer = new StringBuilder(); + char prevChar = 0; + for (final char ch : array) { + if (ch == '?' || ch == '*') { + if (buffer.length() != 0) { + list.add(buffer.toString()); + buffer.setLength(0); + } + if (ch == '?') { + list.add("?"); + } else if (prevChar != '*') {// ch == '*' here; check if previous char was '*' + list.add("*"); + } + } else { + buffer.append(ch); + } + prevChar = ch; + } + if (buffer.length() != 0) { + list.add(buffer.toString()); + } + + return list.toArray(EMPTY_STRING_ARRAY); + } + + /** + * Flips the Windows name separator to Linux and vice-versa. + * + * @param ch The Windows or Linux name separator. + * @return The Windows or Linux name separator. + */ + static char flipSeparator(final char ch) { + if (ch == UNIX_NAME_SEPARATOR) { + return WINDOWS_NAME_SEPARATOR; + } + if (ch == WINDOWS_NAME_SEPARATOR) { + return UNIX_NAME_SEPARATOR; + } + throw new IllegalArgumentException(String.valueOf(ch)); + } +} diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/FullClassNameMatcher.java b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/FullClassNameMatcher.java new file mode 100644 index 000000000..1f4d07775 --- /dev/null +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/FullClassNameMatcher.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.mina.core.buffer.matcher; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * A {@link ClassNameMatcher} that matches on full class names. + * <p> + * This object is immutable and thread-safe. + * </p> + */ +public final class FullClassNameMatcher implements ClassNameMatcher { + private final Set<String> classesSet; + + /** + * Constructs an object based on the specified class names. + * + * @param classes a list of class names + */ + public FullClassNameMatcher(String... classes) { + classesSet = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(classes))); + } + + @Override + public boolean matches(String className) { + return classesSet.contains(className); + } +} \ No newline at end of file diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/IOCase.java b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/IOCase.java new file mode 100644 index 000000000..b2a1c89cd --- /dev/null +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/IOCase.java @@ -0,0 +1,275 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.mina.core.buffer.matcher; + +import java.util.Objects; +import java.util.stream.Stream; + +/** + * Enumeration of IO case sensitivity. + * <p> + * Different filing systems have different rules for case-sensitivity. + * Windows is case-insensitive, Unix is case-sensitive. + * </p> + * <p> + * This class captures that difference, providing an enumeration to + * control how file name comparisons should be performed. It also provides + * methods that use the enumeration to perform comparisons. + * </p> + * <p> + * Wherever possible, you should use the {@code check} methods in this + * class to compare file names. + * </p> + * + * @since 1.3 + */ +public enum IOCase { + + /** + * The constant for case-sensitive regardless of operating system. + */ + SENSITIVE("Sensitive", true), + + /** + * The constant for case-insensitive regardless of operating system. + */ + INSENSITIVE("Insensitive", false), + + /** + * The constant for case sensitivity determined by the current operating system. + * Windows is case-insensitive when comparing file names, Unix is case-sensitive. + * <p> + * <strong>Note:</strong> This only caters for Windows and Unix. Other operating + * systems (e.g. OSX and OpenVMS) are treated as case-sensitive if they use the + * Unix file separator and case-insensitive if they use the Windows file separator + * (see {@link java.io.File#separatorChar}). + * </p> + * <p> + * If you serialize this constant on Windows, and deserialize on Unix, or vice + * versa, then the value of the case-sensitivity flag will change. + * </p> + */ + SYSTEM("System", FileSystem.getCurrent().isCaseSensitive()); + + /** Serialization version. */ + private static final long serialVersionUID = -6343169151696340687L; + + /** + * Factory method to create an IOCase from a name. + * + * @param name the name to find + * @return the IOCase object + * @throws IllegalArgumentException if the name is invalid + */ + public static IOCase forName(final String name) { + return Stream.of(IOCase.values()).filter(ioCase -> ioCase.getName().equals(name)).findFirst() + .orElseThrow(() -> new IllegalArgumentException("Illegal IOCase name: " + name)); + } + + /** + * Tests for cases sensitivity in a null-safe manner. + * + * @param ioCase an IOCase. + * @return true if the input is non-null and {@link #isCaseSensitive()}. + * @since 2.10.0 + */ + public static boolean isCaseSensitive(final IOCase ioCase) { + return ioCase != null && ioCase.isCaseSensitive(); + } + + /** + * Returns the given value if not-null, the defaultValue if null. + * + * @param value the value to test. + * @param defaultValue the default value. + * @return the given value if not-null, the defaultValue if null. + * @since 2.12.0 + */ + public static IOCase value(final IOCase value, final IOCase defaultValue) { + return value != null ? value : defaultValue; + } + + /** The enumeration name. */ + private final String name; + + /** The sensitivity flag. */ + private final transient boolean sensitive; + + /** + * Constructs a new instance. + * + * @param name the name + * @param sensitive the sensitivity + */ + IOCase(final String name, final boolean sensitive) { + this.name = name; + this.sensitive = sensitive; + } + + /** + * Compares two strings using the case-sensitivity rule. + * <p> + * This method mimics {@link String#compareTo} but takes case-sensitivity + * into account. + * </p> + * + * @param str1 the first string to compare, not null + * @param str2 the second string to compare, not null + * @return true if equal using the case rules + * @throws NullPointerException if either string is null + */ + public int checkCompareTo(final String str1, final String str2) { + Objects.requireNonNull(str1, "str1"); + Objects.requireNonNull(str2, "str2"); + return sensitive ? str1.compareTo(str2) : str1.compareToIgnoreCase(str2); + } + + /** + * Checks if one string ends with another using the case-sensitivity rule. + * <p> + * This method mimics {@link String#endsWith} but takes case-sensitivity + * into account. + * </p> + * + * @param str the string to check + * @param end the end to compare against + * @return true if equal using the case rules, false if either input is null + */ + public boolean checkEndsWith(final String str, final String end) { + if (str == null || end == null) { + return false; + } + final int endLen = end.length(); + return str.regionMatches(!sensitive, str.length() - endLen, end, 0, endLen); + } + + /** + * Compares two strings using the case-sensitivity rule. + * <p> + * This method mimics {@link String#equals} but takes case-sensitivity + * into account. + * </p> + * + * @param str1 the first string to compare, not null + * @param str2 the second string to compare, not null + * @return true if equal using the case rules + * @throws NullPointerException if either string is null + */ + public boolean checkEquals(final String str1, final String str2) { + Objects.requireNonNull(str1, "str1"); + Objects.requireNonNull(str2, "str2"); + return sensitive ? str1.equals(str2) : str1.equalsIgnoreCase(str2); + } + + /** + * Checks if one string contains another starting at a specific index using the + * case-sensitivity rule. + * <p> + * This method mimics parts of {@link String#indexOf(String, int)} + * but takes case-sensitivity into account. + * </p> + * + * @param str the string to check, not null + * @param strStartIndex the index to start at in str + * @param search the start to search for, not null + * @return the first index of the search String, + * -1 if no match or {@code null} string input + * @throws NullPointerException if either string is null + * @since 2.0 + */ + public int checkIndexOf(final String str, final int strStartIndex, final String search) { + final int endIndex = str.length() - search.length(); + if (endIndex >= strStartIndex) { + for (int i = strStartIndex; i <= endIndex; i++) { + if (checkRegionMatches(str, i, search)) { + return i; + } + } + } + return -1; + } + + /** + * Checks if one string contains another at a specific index using the case-sensitivity rule. + * <p> + * This method mimics parts of {@link String#regionMatches(boolean, int, String, int, int)} + * but takes case-sensitivity into account. + * </p> + * + * @param str the string to check, not null + * @param strStartIndex the index to start at in str + * @param search the start to search for, not null + * @return true if equal using the case rules + * @throws NullPointerException if either string is null + */ + public boolean checkRegionMatches(final String str, final int strStartIndex, final String search) { + return str.regionMatches(!sensitive, strStartIndex, search, 0, search.length()); + } + + /** + * Checks if one string starts with another using the case-sensitivity rule. + * <p> + * This method mimics {@link String#startsWith(String)} but takes case-sensitivity + * into account. + * </p> + * + * @param str the string to check + * @param start the start to compare against + * @return true if equal using the case rules, false if either input is null + */ + public boolean checkStartsWith(final String str, final String start) { + return str != null && start != null && str.regionMatches(!sensitive, 0, start, 0, start.length()); + } + + /** + * Gets the name of the constant. + * + * @return the name of the constant + */ + public String getName() { + return name; + } + + /** + * Does the object represent case-sensitive comparison. + * + * @return true if case-sensitive + */ + public boolean isCaseSensitive() { + return sensitive; + } + + /** + * Replaces the enumeration from the stream with a real one. + * This ensures that the correct flag is set for SYSTEM. + * + * @return the resolved object + */ + private Object readResolve() { + return forName(name); + } + + /** + * Gets a string describing the sensitivity. + * + * @return a string describing the sensitivity + */ + @Override + public String toString() { + return name; + } +} diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/RegexpClassNameMatcher.java b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/RegexpClassNameMatcher.java new file mode 100644 index 000000000..bb854245d --- /dev/null +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/RegexpClassNameMatcher.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.mina.core.buffer.matcher; + +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * A {@link ClassNameMatcher} that uses regular expressions. + * <p> + * This object is immutable and thread-safe. + * </p> + */ +public final class RegexpClassNameMatcher implements ClassNameMatcher { + private final Pattern pattern; // Class is thread-safe + + /** + * Constructs an object based on the specified pattern. + * + * @param pattern a pattern for evaluating acceptable class names + * @throws NullPointerException if {@code pattern} is null + */ + public RegexpClassNameMatcher(Pattern pattern) { + this.pattern = Objects.requireNonNull(pattern, "pattern"); + } + + /** + * Constructs an object based on the specified regular expression. + * + * @param regex a regular expression for evaluating acceptable class names + */ + public RegexpClassNameMatcher(String regex) { + this(Pattern.compile(regex)); + } + + @Override + public boolean matches(String className) { + return pattern.matcher(className).matches(); + } +} \ No newline at end of file diff --git a/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/WildcardClassNameMatcher.java b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/WildcardClassNameMatcher.java new file mode 100644 index 000000000..36e607138 --- /dev/null +++ b/mina-core/src/main/java/org/apache/mina/core/buffer/matcher/WildcardClassNameMatcher.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.mina.core.buffer.matcher; + +/** + * A {@link ClassNameMatcher} that uses simplified regular expressions + * provided by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) FilenameUtils.wildcardMatch} + * <p> + * This object is immutable and thread-safe. + * </p> + */ +public final class WildcardClassNameMatcher implements ClassNameMatcher { + + private final String pattern; + + /** + * Constructs an object based on the specified simplified regular expression. + * + * @param pattern a {@link FilenameUtils#wildcardMatch} pattern. + */ + public WildcardClassNameMatcher(String pattern) { + this.pattern = pattern; + } + + @Override + public boolean matches(String className) { + return FilenameUtils.wildcardMatch(className, pattern, IOCase.SENSITIVE); + } +} \ No newline at end of file diff --git a/mina-core/src/test/java/org/apache/mina/core/buffer/IoBufferTest.java b/mina-core/src/test/java/org/apache/mina/core/buffer/IoBufferTest.java index cfc2d7104..bf8c46743 100644 --- a/mina-core/src/test/java/org/apache/mina/core/buffer/IoBufferTest.java +++ b/mina-core/src/test/java/org/apache/mina/core/buffer/IoBufferTest.java @@ -372,6 +372,7 @@ public class IoBufferTest { List<Object> o = new ArrayList<>(); o.add(new Date()); o.add(long.class); + buf.accept(ArrayList.class.getName(), Date.class.getName(), long.class.getName()); // Test writing an object. buf.putObject(o); @@ -387,11 +388,12 @@ public class IoBufferTest { @Test public void testNonserializableClass() throws Exception { - Class<?> c = NonserializableClass.class; + Class<?> c = String.class; IoBuffer buffer = IoBuffer.allocate(16); buffer.setAutoExpand(true); buffer.putObject(c); + buffer.accept(String.class.getName()); buffer.flip(); Object o = buffer.getObject(); @@ -407,6 +409,7 @@ public class IoBufferTest { IoBuffer buffer = IoBuffer.allocate(16); buffer.setAutoExpand(true); buffer.putObject(c); + buffer.accept(NonserializableInterface.class.getName()); buffer.flip(); Object o = buffer.getObject(); @@ -947,6 +950,7 @@ public class IoBufferTest { // Test writing an object. buf.putObject(expected); + buf.accept(Bar.class.getName()); // Test reading an object. buf.clear(); diff --git a/pom.xml b/pom.xml index bc7296306..413033d3f 100644 --- a/pom.xml +++ b/pom.xml @@ -90,9 +90,6 @@ <!-- Make Java 8 javadoc lint to shut the f*** up... --> <!-- additionalparam>-Xdoclint:none</additionalparam --> - <!-- SBOM generation versions --> - <version.cyclonedx>2.9.0</version.cyclonedx> - <!-- Maven Plugins --> <version.apache.rat.plugin>0.16.1</version.apache.rat.plugin> <version.api.plugin>3.6.3</version.api.plugin> @@ -105,6 +102,7 @@ <version.clirr.plugin>2.8</version.clirr.plugin> <version.cobertura.plugin>2.7</version.cobertura.plugin> <version.compiler.plugin>3.13.0</version.compiler.plugin> + <version.cyclonedx.plugin>2.9.0</version.cyclonedx.plugin> <version.dashboard.plugin>1.0.0-beta-1</version.dashboard.plugin> <version.dependency.plugin>3.8.0</version.dependency.plugin> <version.deploy.plugin>3.1.3</version.deploy.plugin> @@ -186,6 +184,13 @@ <!-- =========================================== --> <dependencyManagement> <dependencies> + <!-- Commons dependencies --> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>${commons.io.version}</version> + </dependency> + <!-- Submodules --> <dependency> <groupId>${project.groupId}</groupId> @@ -794,7 +799,7 @@ <plugin> <groupId>org.cyclonedx</groupId> <artifactId>cyclonedx-maven-plugin</artifactId> - <version>${version.cyclonedx}</version> + <version>${version.cyclonedx.plugin}</version> <executions> <execution> <id>make-bom</id>