http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/ASN1Object.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/ASN1Object.java b/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/ASN1Object.java new file mode 100644 index 0000000..1d32a40 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/ASN1Object.java @@ -0,0 +1,338 @@ +/* + * 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.sshd.common.util.io.der; + +import java.io.EOFException; +import java.io.IOException; +import java.io.Serializable; +import java.io.StreamCorruptedException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.NumberUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class ASN1Object implements Serializable, Cloneable { + // Constructed Flag + public static final byte CONSTRUCTED = 0x20; + + private static final long serialVersionUID = 4687581744706127265L; + + private ASN1Class objClass; + private ASN1Type objType; + private boolean constructed; + private int length; + private byte[] value; + + public ASN1Object() { + super(); + } + + /* + * <P>The first byte in DER encoding is made of following fields</P> + * <pre> + *------------------------------------------------- + *|Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1| + *------------------------------------------------- + *| Class | CF | Type | + *------------------------------------------------- + * </pre> + */ + public ASN1Object(byte tag, int len, byte... data) { + this(ASN1Class.fromDERValue(tag), ASN1Type.fromDERValue(tag), (tag & CONSTRUCTED) == CONSTRUCTED, len, data); + } + + public ASN1Object(ASN1Class c, ASN1Type t, boolean ctored, int len, byte... data) { + objClass = c; + objType = t; + constructed = ctored; + length = len; + value = data; + } + + public ASN1Class getObjClass() { + return objClass; + } + + public void setObjClass(ASN1Class c) { + objClass = c; + } + + public ASN1Type getObjType() { + return objType; + } + + public void setObjType(ASN1Type y) { + objType = y; + } + + public boolean isConstructed() { + return constructed; + } + + public void setConstructed(boolean c) { + constructed = c; + } + + public int getLength() { + return length; + } + + public void setLength(int l) { + length = l; + } + + public byte[] getValue() { + return value; + } + + // if length is less than value.length then returns copy of it + public byte[] getPureValueBytes() { + byte[] bytes = getValue(); + int available = getLength(); + int numBytes = NumberUtils.length(bytes); + if (numBytes == available) { + return bytes; + } + + if (available == 0) { + return GenericUtils.EMPTY_BYTE_ARRAY; + } + + byte[] pure = new byte[available]; + System.arraycopy(bytes, 0, pure, 0, available); + return pure; + } + + public void setValue(byte[] v) { + value = v; + } + + public DERParser createParser() { + return new DERParser(getValue(), 0, getLength()); + } + + public Object asObject() throws IOException { + ASN1Type type = getObjType(); + if (type == null) { + throw new IOException("No type set"); + } + + switch (type) { + case INTEGER: + return asInteger(); + + case NUMERIC_STRING: + case PRINTABLE_STRING: + case VIDEOTEX_STRING: + case IA5_STRING: + case GRAPHIC_STRING: + case ISO646_STRING: + case GENERAL_STRING: + case BMP_STRING: + case UTF8_STRING: + return asString(); + + case OBJECT_IDENTIFIER: + return asOID(); + + case SEQUENCE : + return getValue(); + + default: + throw new IOException("Invalid DER: unsupported type: " + type); + } + } + + /** + * Get the value as {@link BigInteger} + * @return BigInteger + * @throws IOException if type not an {@link ASN1Type#INTEGER} + */ + public BigInteger asInteger() throws IOException { + ASN1Type typeValue = getObjType(); + if (ASN1Type.INTEGER.equals(typeValue)) { + return toInteger(); + } else { + throw new IOException("Invalid DER: object is not integer: " + typeValue); + } + } + + // does not check if this is an integer + public BigInteger toInteger() { + return new BigInteger(getPureValueBytes()); + } + + /** + * Get value as string. Most strings are treated as Latin-1. + * @return Java string + * @throws IOException if + */ + public String asString() throws IOException { + ASN1Type type = getObjType(); + if (type == null) { + throw new IOException("No type set"); + } + + final String encoding; + switch (type) { + // Not all are Latin-1 but it's the closest thing + case NUMERIC_STRING: + case PRINTABLE_STRING: + case VIDEOTEX_STRING: + case IA5_STRING: + case GRAPHIC_STRING: + case ISO646_STRING: + case GENERAL_STRING: + encoding = "ISO-8859-1"; + break; + + case BMP_STRING: + encoding = "UTF-16BE"; + break; + + case UTF8_STRING: + encoding = "UTF-8"; + break; + + case UNIVERSAL_STRING: + throw new IOException("Invalid DER: can't handle UCS-4 string"); + + default: + throw new IOException("Invalid DER: object is not a string: " + type); + } + + return new String(getValue(), 0, getLength(), encoding); + } + + public List<Integer> asOID() throws IOException { + ASN1Type typeValue = getObjType(); + if (ASN1Type.OBJECT_IDENTIFIER.equals(typeValue)) { + return toOID(); + } else { + throw new StreamCorruptedException("Invalid DER: object is not an OID: " + typeValue); + } + } + + // Does not check that type is OID + public List<Integer> toOID() throws IOException { + int vLen = getLength(); + if (vLen <= 0) { + throw new EOFException("Not enough data for an OID"); + } + + List<Integer> oid = new ArrayList<>(vLen + 1); + byte[] bytes = getValue(); + int val1 = bytes[0] & 0xFF; + oid.add(Integer.valueOf(val1 / 40)); + oid.add(Integer.valueOf(val1 % 40)); + + for (int curPos = 1; curPos < vLen; curPos++) { + int v = bytes[curPos] & 0xFF; + if (v <= 0x7F) { // short form + oid.add(Integer.valueOf(v)); + continue; + } + + long curVal = v & 0x7F; + curPos++; + + for (int subLen = 1;; subLen++, curPos++) { + if (curPos >= vLen) { + throw new EOFException("Incomplete OID value"); + } + + if (subLen > 5) { // 32 bit values can span at most 5 octets + throw new StreamCorruptedException("OID component encoding beyond 5 bytes"); + } + + v = bytes[curPos] & 0xFF; + curVal = ((curVal << 7) & 0xFFFFFFFF80L) | (v & 0x7FL); + if (curVal > Integer.MAX_VALUE) { + throw new StreamCorruptedException("OID value exceeds 32 bits: " + curVal); + } + + if (v <= 0x7F) { // found last octet ? + break; + } + } + + oid.add(Integer.valueOf((int) (curVal & 0x7FFFFFFFL))); + } + + return oid; + } + + @Override + public int hashCode() { + return Objects.hash(getObjClass(), getObjType()) + + Boolean.hashCode(isConstructed()) + + getLength() + + NumberUtils.hashCode(getValue(), 0, getLength()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + + ASN1Object other = (ASN1Object) obj; + return Objects.equals(this.getObjClass(), other.getObjClass()) + && Objects.equals(this.getObjType(), other.getObjType()) + && (this.isConstructed() == other.isConstructed()) + && (this.getLength() == other.getLength()) + && (NumberUtils.diffOffset(this.getValue(), 0, other.getValue(), 0, this.getLength()) < 0); + } + + @Override + public ASN1Object clone() { + try { + ASN1Object cpy = getClass().cast(super.clone()); + byte[] data = cpy.getValue(); + if (data != null) { + cpy.setValue(data.clone()); + } + return cpy; + } catch (CloneNotSupportedException e) { + throw new IllegalStateException("Unexpected clone failure: " + e.getMessage(), e); + } + } + + @Override + public String toString() { + return Objects.toString(getObjClass()) + + "/" + getObjType() + + "/" + isConstructed() + + "[" + getLength() + "]" + + ": " + BufferUtils.toHex(getValue(), 0, getLength(), ':'); + } +}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/ASN1Type.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/ASN1Type.java b/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/ASN1Type.java new file mode 100644 index 0000000..3dd13ca --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/ASN1Type.java @@ -0,0 +1,118 @@ +/* + * 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.sshd.common.util.io.der; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +import org.apache.sshd.common.util.GenericUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public enum ASN1Type { + ANY((byte) 0x00), + BOOLEAN((byte) 0x01), + INTEGER((byte) 0x02), + BIT_STRING((byte) 0x03), + OCTET_STRING((byte) 0x04), + NULL((byte) 0x05), + OBJECT_IDENTIFIER((byte) 0x06), + REAL((byte) 0x09), + ENUMERATED((byte) 0x0a), + RELATIVE_OID((byte) 0x0d), + SEQUENCE((byte) 0x10), + SET((byte) 0x11), + NUMERIC_STRING((byte) 0x12), + PRINTABLE_STRING((byte) 0x13), + T61_STRING((byte) 0x14), + VIDEOTEX_STRING((byte) 0x15), + IA5_STRING((byte) 0x16), + GRAPHIC_STRING((byte) 0x19), + ISO646_STRING((byte) 0x1A), + GENERAL_STRING((byte) 0x1B), + UTF8_STRING((byte) 0x0C), + UNIVERSAL_STRING((byte) 0x1C), + BMP_STRING((byte) 0x1E), + UTC_TIME((byte) 0x17), + GENERALIZED_TIME((byte) 0x18); + + public static final Set<ASN1Type> VALUES = + Collections.unmodifiableSet(EnumSet.allOf(ASN1Type.class)); + + private final byte typeValue; + + ASN1Type(byte typeVal) { + typeValue = typeVal; + } + + public byte getTypeValue() { + return typeValue; + } + + public static ASN1Type fromName(String s) { + if (GenericUtils.isEmpty(s)) { + return null; + } + + for (ASN1Type t : VALUES) { + if (s.equalsIgnoreCase(t.name())) { + return t; + } + } + + return null; + } + + /** + * <P>The first byte in DER encoding is made of following fields</P> + * <pre> + *------------------------------------------------- + *|Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1| + *------------------------------------------------- + *| Class | CF | Type | + *------------------------------------------------- + * </pre> + * @param value The original DER encoded byte + * @return The {@link ASN1Type} value - {@code null} if no match found + * @see #fromTypeValue(int) + */ + public static ASN1Type fromDERValue(int value) { + return fromTypeValue(value & 0x1F); + } + + /** + * @param value The "pure" type value - with no extra bits set + * @return The {@link ASN1Type} value - {@code null} if no match found + */ + public static ASN1Type fromTypeValue(int value) { + if ((value < 0) || (value > 0x1F)) { // only 5 bits are used + return null; + } + + for (ASN1Type t : VALUES) { + if (t.getTypeValue() == value) { + return t; + } + } + + return null; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/DERParser.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/DERParser.java b/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/DERParser.java new file mode 100644 index 0000000..5f37bd7 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/DERParser.java @@ -0,0 +1,151 @@ +/* + * 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.sshd.common.util.io.der; + +import java.io.ByteArrayInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.math.BigInteger; +import java.util.Arrays; + +import org.apache.sshd.common.util.NumberUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; + +/** + * A bare minimum DER parser - just enough to be able to decode + * signatures and private keys + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class DERParser extends FilterInputStream { + /** + * Maximum size of data allowed by {@link #readLength()} - it is a bit + * arbitrary since one can encode 32-bit length data, but it is good + * enough for the keys + */ + public static final int MAX_DER_VALUE_LENGTH = 2 * Short.MAX_VALUE; + + private final byte[] lenBytes = new byte[Integer.BYTES]; + + public DERParser(byte... bytes) { + this(bytes, 0, NumberUtils.length(bytes)); + } + + public DERParser(byte[] bytes, int offset, int len) { + this(new ByteArrayInputStream(bytes, offset, len)); + } + + public DERParser(InputStream s) { + super(s); + } + + /** + * Decode the length of the field. Can only support length + * encoding up to 4 octets. In BER/DER encoding, length can + * be encoded in 2 forms: + * <ul> + * <li><p> + * Short form - One octet. Bit 8 has value "0" and bits 7-1 + * give the length. + * </p></li> + * + * <li><p> + * Long form - Two to 127 octets (only 4 is supported here). + * Bit 8 of first octet has value "1" and bits 7-1 give the + * number of additional length octets. Second and following + * octets give the length, base 256, most significant digit + * first. + * </p></li> + * </ul> + * + * @return The length as integer + * @throws IOException If invalid format found + */ + public int readLength() throws IOException { + int i = read(); + if (i == -1) { + throw new StreamCorruptedException("Invalid DER: length missing"); + } + + // A single byte short length + if ((i & ~0x7F) == 0) { + return i; + } + + int num = i & 0x7F; + // TODO We can't handle length longer than 4 bytes + if ((i >= 0xFF) || (num > lenBytes.length)) { + throw new StreamCorruptedException("Invalid DER: length field too big: " + i); + } + + // place the read bytes last so that the 1st ones are zeroes as big endian + Arrays.fill(lenBytes, (byte) 0); + int n = read(lenBytes, 4 - num, num); + if (n < num) { + throw new StreamCorruptedException("Invalid DER: length data too short: expected=" + num + ", actual=" + n); + } + + long len = BufferUtils.getUInt(lenBytes); + if (len < 0x7FL) { // according to standard: "the shortest possible length encoding must be used" + throw new StreamCorruptedException("Invalid DER: length not in shortest form: " + len); + } + + if (len > MAX_DER_VALUE_LENGTH) { + throw new StreamCorruptedException("Invalid DER: data length too big: " + len + " (max=" + MAX_DER_VALUE_LENGTH + ")"); + } + + // we know the cast is safe since it is less than MAX_DER_VALUE_LENGTH which is ~64K + return (int) len; + } + + public ASN1Object readObject() throws IOException { + int tag = read(); + if (tag == -1) { + return null; + } + + int length = readLength(); + byte[] value = new byte[length]; + int n = read(value); + if (n < length) { + throw new StreamCorruptedException("Invalid DER: stream too short, missing value: read " + n + " out of required " + length); + } + + return new ASN1Object((byte) tag, length, value); + } + + public BigInteger readBigInteger() throws IOException { + int type = read(); + if (type != 0x02) { + throw new StreamCorruptedException("Invalid DER: data type is not an INTEGER: 0x" + Integer.toHexString(type)); + } + + int len = readLength(); + byte[] value = new byte[len]; + int n = read(value); + if (n < len) { + throw new StreamCorruptedException("Invalid DER: stream too short, missing value: read " + n + " out of required " + len); + } + + return new BigInteger(value); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/DERWriter.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/DERWriter.java b/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/DERWriter.java new file mode 100644 index 0000000..bc603ee --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/io/der/DERWriter.java @@ -0,0 +1,172 @@ +/* + * 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.sshd.common.util.io.der; + +import java.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StreamCorruptedException; +import java.math.BigInteger; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.sshd.common.util.NumberUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; + +/** + * A bare-minimum DER encoder - just enough so we can encoder signatures + * and keys data + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class DERWriter extends FilterOutputStream { + private final byte[] lenBytes = new byte[Integer.BYTES]; + + public DERWriter() { + this(ByteArrayBuffer.DEFAULT_SIZE); + } + + public DERWriter(int initialSize) { + this(new ByteArrayOutputStream(initialSize)); + } + + public DERWriter(OutputStream stream) { + super(Objects.requireNonNull(stream, "No output stream")); + } + + public DERWriter startSequence() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + AtomicBoolean dataWritten = new AtomicBoolean(false); + @SuppressWarnings("resource") + DERWriter encloser = this; + return new DERWriter(baos) { + @Override + public void close() throws IOException { + baos.close(); + + if (!dataWritten.getAndSet(true)) { // detect repeated calls and write this only once + encloser.writeObject(new ASN1Object(ASN1Class.UNIVERSAL, ASN1Type.SEQUENCE, false, baos.size(), baos.toByteArray())); + } + } + }; + } + + public void writeBigInteger(BigInteger value) throws IOException { + writeBigInteger(Objects.requireNonNull(value, "No value").toByteArray()); + } + + /** + * The integer is always considered to be positive, so if the first byte is < 0, + * we pad with a zero to make it positive + * + * @param bytes {@link BigInteger} bytes + * @throws IOException If failed to write the bytes + */ + public void writeBigInteger(byte... bytes) throws IOException { + writeBigInteger(bytes, 0, NumberUtils.length(bytes)); + } + + /** + * The integer is always considered to be positive, so if the first byte is < 0, + * we pad with a zero to make it positive + * + * @param bytes {@link BigInteger} bytes + * @param off Offset in bytes data + * @param len Number of bytes to write + * @throws IOException If failed to write the bytes + */ + public void writeBigInteger(byte[] bytes, int off, int len) throws IOException { + // Strip leading zeroes + while (len > 1 && bytes[off] == 0 && isPositive(bytes[off + 1])) { + off++; + len--; + } + // indicate it is an INTEGER + write(0x02); + // Pad with a zero if needed + if (isPositive(bytes[off])) { + writeLength(len); + } else { + writeLength(len + 1); + write(0); + } + // Write data + write(bytes, off, len); + } + + private boolean isPositive(byte b) { + return (b & 0x80) == 0; + } + + public void writeObject(ASN1Object obj) throws IOException { + Objects.requireNonNull(obj, "No ASN.1 object"); + + ASN1Type type = obj.getObjType(); + byte typeValue = type.getTypeValue(); + ASN1Class clazz = obj.getObjClass(); + byte classValue = clazz.getClassValue(); + byte tagValue = (byte) (((classValue << 6) & 0xC0) | (typeValue & 0x1F)); + writeObject(tagValue, obj.getLength(), obj.getValue()); + } + + public void writeObject(byte tag, int len, byte... data) throws IOException { + write(tag & 0xFF); + writeLength(len); + write(data, 0, len); + } + + public void writeLength(int len) throws IOException { + ValidateUtils.checkTrue(len >= 0, "Invalid length: %d", len); + + // short form - MSBit is zero + if (len <= 127) { + write(len); + return; + } + + BufferUtils.putUInt(len, lenBytes); + + int nonZeroPos = 0; + for (; nonZeroPos < lenBytes.length; nonZeroPos++) { + if (lenBytes[nonZeroPos] != 0) { + break; + } + } + + if (nonZeroPos >= lenBytes.length) { + throw new StreamCorruptedException("All zeroes length representation for len=" + len); + } + + int bytesLen = lenBytes.length - nonZeroPos; + write(0x80 | bytesLen); // indicate number of octets + write(lenBytes, nonZeroPos, bytesLen); + } + + public byte[] toByteArray() throws IOException { + if (this.out instanceof ByteArrayOutputStream) { + return ((ByteArrayOutputStream) this.out).toByteArray(); + } else { + throw new IOException("The underlying stream is not a byte[] stream"); + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java b/sshd-common/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java new file mode 100644 index 0000000..a55ac2e --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java @@ -0,0 +1,86 @@ +/* + * 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.sshd.common.util.io.functors; + +import java.io.IOException; +import java.util.Objects; + +/** + * Invokes some I/O function on the input returning some output + * and potentially throwing an {@link IOException} in the process + * + * @param <T> Type of input + * @param <R> Type of output + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@FunctionalInterface +public interface IOFunction<T, R> { + R apply(T t) throws IOException; + + /** + * Returns a composed function that first applies the {@code before} + * function to its input, and then applies this function to the result. + * If evaluation of either function throws an exception, it is relayed to + * the caller of the composed function. + * + * @param <V> the type of input to the {@code before} function, and to the + * composed function + * @param before the function to apply before this function is applied + * @return a composed function that first applies the {@code before} + * function and then applies this function + * @throws NullPointerException if before is null + * + * @see #andThen(IOFunction) + */ + default <V> IOFunction<V, R> compose(IOFunction<? super V, ? extends T> before) { + Objects.requireNonNull(before, "No composing function provided"); + return (V v) -> apply(before.apply(v)); + } + + /** + * Returns a composed function that first applies this function to + * its input, and then applies the {@code after} function to the result. + * If evaluation of either function throws an exception, it is relayed to + * the caller of the composed function. + * + * @param <V> the type of output of the {@code after} function, and of the + * composed function + * @param after the function to apply after this function is applied + * @return a composed function that first applies this function and then + * applies the {@code after} function + * @throws NullPointerException if after is null + * + * @see #compose(IOFunction) + */ + default <V> IOFunction<T, V> andThen(IOFunction<? super R, ? extends V> after) { + Objects.requireNonNull(after, "No composing function provided"); + return (T t) -> after.apply(apply(t)); + } + + /** + * Returns a function that always returns its input argument. + * + * @param <T> the type of the input and output objects to the function + * @return a function that always returns its input argument + */ + static <T> IOFunction<T, T> identity() { + return t -> t; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/logging/AbstractLoggingBean.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/logging/AbstractLoggingBean.java b/sshd-common/src/main/java/org/apache/sshd/common/util/logging/AbstractLoggingBean.java new file mode 100644 index 0000000..27fef2f --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/logging/AbstractLoggingBean.java @@ -0,0 +1,72 @@ +/* + * 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.sshd.common.util.logging; + +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.common.util.GenericUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Serves as a common base class for the vast majority of classes that require + * some kind of logging. Facilitates quick and easy replacement of the actual used + * logger from one framework to another + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractLoggingBean { + protected final Logger log; + private final AtomicReference<SimplifiedLog> simplifiedLog = new AtomicReference<>(); + + /** + * Default constructor - creates a logger using the full class name + */ + protected AbstractLoggingBean() { + this(""); + } + + /** + * Create a logger for instances of the same class for which we might + * want to have a "discriminator" for them + * + * @param discriminator The discriminator value - ignored if {@code null} + * or empty + */ + protected AbstractLoggingBean(String discriminator) { + String name = getClass().getName(); + if (GenericUtils.length(discriminator) > 0) { + name += "[" + discriminator + "]"; + } + log = LoggerFactory.getLogger(name); + } + + protected SimplifiedLog getSimplifiedLogger() { + SimplifiedLog logger; + synchronized (simplifiedLog) { + logger = simplifiedLog.get(); + if (logger == null) { + logger = LoggingUtils.wrap(log); + } + } + + return logger; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/logging/LoggingUtils.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/logging/LoggingUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/logging/LoggingUtils.java new file mode 100644 index 0000000..674ed1a --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/logging/LoggingUtils.java @@ -0,0 +1,549 @@ +/* + * 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.sshd.common.util.logging; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.TreeMap; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.logging.Level; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.NumberUtils; +import org.apache.sshd.common.util.ReflectionUtils; +import org.slf4j.Logger; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public final class LoggingUtils { + + private LoggingUtils() { + throw new UnsupportedOperationException("No instance"); + } + + /** + * Scans using reflection API for all fields that are {@code public static final} + * that start with the given common prefix (case <U>sensitive</U>) and are of type + * {@link Number}. + * + * @param clazz The {@link Class} to query + * @param commonPrefix The expected common prefix + * @return A {@link NavigableMap} of all the matching fields, where key=the field's {@link Integer} + * value and mapping=the field's name + * @see #generateMnemonicMap(Class, Predicate) + */ + public static NavigableMap<Integer, String> generateMnemonicMap(Class<?> clazz, final String commonPrefix) { + return generateMnemonicMap(clazz, f -> { + String name = f.getName(); + return name.startsWith(commonPrefix); + }); + } + + /** + * Scans using reflection API for all <U>numeric {@code public static final}</U> fields + * that are also accepted by the predicate. Any field that is not such or fail to retrieve + * its value, or has a duplicate value is <U>silently</U> skipped. + * + * @param clazz The {@link Class} to query + * @param acceptor The {@link Predicate} used to decide whether to process the {@link Field} + * (besides being a {@link Number} and {@code public static final}). + * @return A {@link NavigableMap} of all the matching fields, where key=the field's {@link Integer} + * value and mapping=the field's name + * @see #getMnemonicFields(Class, Predicate) + */ + public static NavigableMap<Integer, String> generateMnemonicMap(Class<?> clazz, Predicate<? super Field> acceptor) { + Collection<Field> fields = getMnemonicFields(clazz, acceptor); + if (GenericUtils.isEmpty(fields)) { + return Collections.emptyNavigableMap(); + } + + NavigableMap<Integer, String> result = new TreeMap<>(Comparator.naturalOrder()); + for (Field f : fields) { + String name = f.getName(); + try { + Number value = (Number) f.get(null); + String prev = result.put(NumberUtils.toInteger(value), name); + if (prev != null) { + //noinspection UnnecessaryContinue + continue; // debug breakpoint + } + } catch (Exception e) { + //noinspection UnnecessaryContinue + continue; // debug breakpoint + } + } + + return result; + } + + /** + * Scans using reflection API for all <U>numeric {@code public static final}</U> fields + * that have a common prefix and whose value is used by several of the other + * matching fields + * + * @param clazz The {@link Class} to query + * @param commonPrefix The expected common prefix + * @return A {@link Map} of all the mnemonic fields names whose value is the same as other + * fields in this map. The key is the field's name and value is its associated opcode. + * @see #getAmbiguousMenmonics(Class, Predicate) + */ + public static Map<String, Integer> getAmbiguousMenmonics(Class<?> clazz, String commonPrefix) { + return getAmbiguousMenmonics(clazz, f -> { + String name = f.getName(); + return name.startsWith(commonPrefix); + }); + } + + /** + * Scans using reflection API for all <U>numeric {@code public static final}</U> fields + * that are also accepted by the predicate and whose value is used by several of the other + * matching fields + * + * @param clazz The {@link Class} to query + * @param acceptor The {@link Predicate} used to decide whether to process the {@link Field} + * (besides being a {@link Number} and {@code public static final}). + * @return A {@link Map} of all the mnemonic fields names whose value is the same as other + * fields in this map. The key is the field's name and value is its associated opcode. + * @see #getMnemonicFields(Class, Predicate) + */ + public static Map<String, Integer> getAmbiguousMenmonics(Class<?> clazz, Predicate<? super Field> acceptor) { + Collection<Field> fields = getMnemonicFields(clazz, acceptor); + if (GenericUtils.isEmpty(fields)) { + return Collections.emptyMap(); + } + + Map<String, Integer> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + Map<Integer, List<String>> opcodesMap = new TreeMap<>(Comparator.naturalOrder()); + for (Field f : fields) { + String name = f.getName(); + try { + Number value = (Number) f.get(null); + Integer key = NumberUtils.toInteger(value); + List<String> nameList = opcodesMap.get(key); + if (nameList == null) { + nameList = new ArrayList<>(); + opcodesMap.put(key, nameList); + } + nameList.add(name); + + int numOpcodes = nameList.size(); + if (numOpcodes > 1) { + result.put(name, key); + if (numOpcodes == 2) { // add the 1st name as well + result.put(nameList.get(0), key); + } + } + } catch (Exception e) { + continue; // debug breakpoint + } + } + + return result; + } + + /** + * Scans using reflection API for all <U>numeric {@code public static final}</U> fields + * that are also accepted by the predicate. + * + * @param clazz The {@link Class} to query + * @param acceptor The {@link Predicate} used to decide whether to process the {@link Field} + * (besides being a {@link Number} and {@code public static final}). + * @return A {@link Collection} of all the fields that have satisfied all conditions + */ + public static Collection<Field> getMnemonicFields(Class<?> clazz, Predicate<? super Field> acceptor) { + return ReflectionUtils.getMatchingFields(clazz, f -> { + int mods = f.getModifiers(); + if ((!Modifier.isPublic(mods)) || (!Modifier.isStatic(mods)) || (!Modifier.isFinal(mods))) { + return false; + } + + Class<?> type = f.getType(); + if (!NumberUtils.isNumericClass(type)) { + return false; + } + + return acceptor.test(f); + }); + } + + /** + * Verifies if the given level is above the required threshold for logging. + * + * @param level The {@link Level} to evaluate + * @param threshold The threshold {@link Level} + * @return {@code true} if the evaluated level is above the required + * threshold. + * <P> + * <B>Note(s):</B> + * </P> + * <UL> + * <LI><P> + * If either argument is {@code null} then result is {@code false}. + * </P></LI> + * + * <LI><P> + * If the evaluated level is {@link Level#OFF} then result is {@code false} + * regardless of the threshold. + * </P></LI> + * + * <LI><P> + * If the threshold is {@link Level#ALL} and the evaluated level is + * <U>not</U> {@link Level#OFF} the result is {@code true}. + * </P></LI> + * + * <LI><P> + * Otherwise, the evaluated level {@link Level#intValue()} must be + * greater or equal to the threshold. + * </P></LI> + * </UL> + */ + public static boolean isLoggable(Level level, Level threshold) { + if ((level == null) || (threshold == null)) { + return false; + } else if (Level.OFF.equals(level) || Level.OFF.equals(threshold)) { + return false; + } else if (Level.ALL.equals(threshold)) { + return true; + } else { + return level.intValue() >= threshold.intValue(); + } + } + + public static SimplifiedLog wrap(final Logger logger) { + if (logger == null) { + return SimplifiedLog.EMPTY; + } else { + return new SimplifiedLog() { + @Override + public void log(Level level, Object message, Throwable t) { + if (isEnabled(level)) { + logMessage(logger, level, message, t); + } + + } + + @Override + public boolean isEnabled(Level level) { + return isLoggable(logger, level); + } + }; + } + } + + // NOTE: assume that level enabled has been checked !!! + public static void logMessage(Logger logger, Level level, Object message, Throwable t) { + if ((logger == null) || (level == null) || Level.OFF.equals(level)) { + return; + } else if (Level.SEVERE.equals(level)) { + logger.error(Objects.toString(message), t); + } else if (Level.WARNING.equals(level)) { + logger.warn(Objects.toString(message), t); + } else if (Level.INFO.equals(level) || Level.ALL.equals(level)) { + logger.info(Objects.toString(message), t); + } else if (Level.CONFIG.equals(level) || Level.FINE.equals(level)) { + logger.debug(Objects.toString(message), t); + } else { + logger.trace(Objects.toString(message), t); + } + } + + /** + * @param logger The {@link Logger} instance - ignored if {@code null} + * @param level The validate log {@link Level} - ignored if {@code null} + * @return <P>{@code true} if the level is enabled for the logger. The + * mapping of the level to the logger is as follows:</P> + * <UL> + * <LI>{@link Level#OFF} always returns {@code false}</LI> + * <LI>{@link Level#SEVERE} returns {@link Logger#isErrorEnabled()}</LI> + * <LI>{@link Level#WARNING} returns {@link Logger#isWarnEnabled()}</LI> + * <LI>{@link Level#INFO} and {@link Level#ALL} returns {@link Logger#isInfoEnabled()}</LI> + * <LI>{@link Level#CONFIG} and {@link Level#FINE} returns {@link Logger#isDebugEnabled()}</LI> + * <LI>All other levels return {@link Logger#isTraceEnabled()}</LI> + * </UL> + */ + public static boolean isLoggable(Logger logger, Level level) { + if ((logger == null) || (level == null) || Level.OFF.equals(level)) { + return false; + } else if (Level.SEVERE.equals(level)) { + return logger.isErrorEnabled(); + } else if (Level.WARNING.equals(level)) { + return logger.isWarnEnabled(); + } else if (Level.INFO.equals(level) || Level.ALL.equals(level)) { + return logger.isInfoEnabled(); + } else if (Level.CONFIG.equals(level) || Level.FINE.equals(level)) { + return logger.isDebugEnabled(); + } else { + return logger.isTraceEnabled(); + } + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @param level The log {@link Level} mapped as follows:</BR> + * + * <UL> + * <LI>{@link Level#OFF} - {@link #nologClosure(Logger)}</LI> + * <LI>{@link Level#SEVERE} - {@link #errorClosure(Logger)}</LI> + * <LI>{@link Level#WARNING} - {@link #warnClosure(Logger)}</LI> + * <LI>{@link Level#INFO}/{@link Level#ALL} - {@link #infoClosure(Logger)}</LI> + * <LI>{@link Level#CONFIG}/{@link Level#FINE} - {@link #debugClosure(Logger)}</LI> + * <LI>All others - {@link #traceClosure(Logger)}</LI> + * </UL> + * @return A consumer whose {@link Consumer#accept(Object)} method logs the + * {@link String#valueOf(Object)} value of its argument if the specific level is enabled + */ + public static <T> Consumer<T> loggingClosure(Logger logger, Level level) { + return loggingClosure(logger, level, null); + } + + public static <T> Consumer<T> loggingClosure(Logger logger, Level level, Throwable t) { + Objects.requireNonNull(level, "No level provided"); + + if (Level.OFF.equals(level)) { + return nologClosure(logger); + } else if (Level.SEVERE.equals(level)) { + return errorClosure(logger, t); + } else if (Level.WARNING.equals(level)) { + return warnClosure(logger, t); + } else if (Level.INFO.equals(level) || Level.ALL.equals(level)) { + return infoClosure(logger, t); + } else if (Level.CONFIG.equals(level) || Level.FINE.equals(level)) { + return debugClosure(logger, t); + } else { + return traceClosure(logger, t); + } + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @return A consumer whose {@link Consumer#accept(Object)} method logs nothing when invoked + */ + public static <T> Consumer<T> nologClosure(Logger logger) { + Objects.requireNonNull(logger, "No logger provided"); + return t -> { /* do nothing */ }; + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @return A consumer whose {@link Consumer#accept(Object)} method logs the + * {@link String#valueOf(Object)} value of its argument if {@link Logger#isErrorEnabled()} + */ + public static <T> Consumer<T> errorClosure(Logger logger) { + return errorClosure(logger, null); + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @param thrown A {@link Throwable} to attach to the message - ignored if {@code null} + * @return A consumer whose {@link Consumer#accept(Object)} method logs the + * {@link String#valueOf(Object)} value of its argument if {@link Logger#isErrorEnabled()} + */ + public static <T> Consumer<T> errorClosure(Logger logger, Throwable thrown) { + Objects.requireNonNull(logger, "No logger provided"); + return new Consumer<T>() { + @Override + public void accept(T input) { + if (logger.isErrorEnabled()) { + String msg = String.valueOf(input); + if (thrown == null) { + logger.error(msg); + } else { + logger.error(msg, thrown); + } + } + } + + @Override + public String toString() { + return "ERROR"; + } + }; + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @return A consumer whose {@link Consumer#accept(Object)} method logs the + * {@link String#valueOf(Object)} value of its argument if {@link Logger#isWarnEnabled()} + */ + public static <T> Consumer<T> warnClosure(Logger logger) { + return warnClosure(logger, null); + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @param thrown A {@link Throwable} to attach to the message - ignored if {@code null} + * @return A consumer whose {@link Consumer#accept(Object)} method logs the {@link String#valueOf(Object)} + * value of its argument if {@link Logger#isWarnEnabled()} + */ + public static <T> Consumer<T> warnClosure(Logger logger, Throwable thrown) { + Objects.requireNonNull(logger, "No logger provided"); + return new Consumer<T>() { + @Override + public void accept(T input) { + if (logger.isWarnEnabled()) { + String msg = String.valueOf(input); + if (thrown == null) { + logger.warn(msg); + } else { + logger.warn(msg, thrown); + } + } + } + + @Override + public String toString() { + return "WARN"; + } + }; + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @return A consumer whose {@link Consumer#accept(Object)} method logs the {@link String#valueOf(Object)} + * value of its argument if {@link Logger#isInfoEnabled()} + */ + public static <T> Consumer<T> infoClosure(Logger logger) { + return infoClosure(logger, null); + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @param thrown A {@link Throwable} to attach to the message - ignored if {@code null} + * @return A consumer whose {@link Consumer#accept(Object)} method logs the + * {@link String#valueOf(Object)} value of its argument if {@link Logger#isInfoEnabled()} + */ + public static <T> Consumer<T> infoClosure(Logger logger, Throwable thrown) { + Objects.requireNonNull(logger, "No logger provided"); + return new Consumer<T>() { + @Override + public void accept(T input) { + if (logger.isInfoEnabled()) { + String msg = String.valueOf(input); + if (thrown == null) { + logger.info(msg); + } else { + logger.info(msg, thrown); + } + } + } + + @Override + public String toString() { + return "INFO"; + } + }; + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @return A consumer whose {@link Consumer#accept(Object)} method logs the + * {@link String#valueOf(Object)} value of its argument if {@link Logger#isDebugEnabled()} + */ + public static <T> Consumer<T> debugClosure(Logger logger) { + return debugClosure(logger, null); + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @param thrown A {@link Throwable} to attach to the message - ignored if {@code null} + * @return A consumer whose {@link Consumer#accept(Object)} method logs the + * {@link String#valueOf(Object)} value of its argument if {@link Logger#isDebugEnabled()} + */ + public static <T> Consumer<T> debugClosure(Logger logger, Throwable thrown) { + Objects.requireNonNull(logger, "No logger provided"); + return new Consumer<T>() { + @Override + public void accept(T input) { + if (logger.isDebugEnabled()) { + String msg = String.valueOf(input); + if (thrown == null) { + logger.debug(msg); + } else { + logger.debug(msg, thrown); + } + } + } + + @Override + public String toString() { + return "DEBUG"; + } + }; + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @return A consumer whose {@link Consumer#accept(Object)} method logs the + * {@link String#valueOf(Object)} value of its argument if {@link Logger#isTraceEnabled()} + */ + public static <T> Consumer<T> traceClosure(Logger logger) { + return traceClosure(logger, null); + } + + /** + * @param <T> Generic message type consumer + * @param logger The {@link Logger} instance to use + * @param thrown A {@link Throwable} to attach to the message - ignored if {@code null} + * @return A consumer whose {@link Consumer#accept(Object)} method logs the + * {@link String#valueOf(Object)} value of its argument if {@link Logger#isTraceEnabled()} + */ + public static <T> Consumer<T> traceClosure(Logger logger, Throwable thrown) { + Objects.requireNonNull(logger, "No logger provided"); + return new Consumer<T>() { + @Override + public void accept(T input) { + if (logger.isTraceEnabled()) { + String msg = String.valueOf(input); + if (thrown == null) { + logger.trace(msg); + } else { + logger.trace(msg, thrown); + } + } + } + + @Override + public String toString() { + return "TRACE"; + } + }; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/logging/SimplifiedLog.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/logging/SimplifiedLog.java b/sshd-common/src/main/java/org/apache/sshd/common/util/logging/SimplifiedLog.java new file mode 100644 index 0000000..58d58ee --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/logging/SimplifiedLog.java @@ -0,0 +1,55 @@ +/* + * 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.sshd.common.util.logging; + +import java.util.logging.Level; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface SimplifiedLog { + + /** + * An "empty" {@link SimplifiedLog} that does nothing + */ + SimplifiedLog EMPTY = new SimplifiedLog() { + @Override + public boolean isEnabled(Level level) { + return false; + } + + @Override + public void log(Level level, Object message, Throwable t) { + // ignored + } + + @Override + public String toString() { + return "EMPTY"; + } + }; + + boolean isEnabled(Level level); + + default void log(Level level, Object message) { + log(level, message, null); + } + + void log(Level level, Object message, Throwable t); +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/net/NetworkConnector.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/net/NetworkConnector.java b/sshd-common/src/main/java/org/apache/sshd/common/util/net/NetworkConnector.java new file mode 100644 index 0000000..df7683a --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/net/NetworkConnector.java @@ -0,0 +1,90 @@ +/* + * 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.sshd.common.util.net; + +import java.util.concurrent.TimeUnit; + +import org.apache.sshd.common.util.logging.AbstractLoggingBean; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class NetworkConnector extends AbstractLoggingBean { + public static final String DEFAULT_HOST = SshdSocketAddress.LOCALHOST_IPV4; + public static final long DEFAULT_CONNECT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L); + public static final long DEFAULT_READ_TIMEOUT = TimeUnit.SECONDS.toMillis(15L); + + private String protocol; + private String host = DEFAULT_HOST; + private int port; + private long connectTimeout = DEFAULT_CONNECT_TIMEOUT; + private long readTimeout = DEFAULT_READ_TIMEOUT; + + public NetworkConnector() { + super(); + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public long getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(long connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public long getReadTimeout() { + return readTimeout; + } + + public void setReadTimeout(long readTimeout) { + this.readTimeout = readTimeout; + } + + @Override + public String toString() { + return getProtocol() + "://" + getHost() + ":" + getPort() + + ";connect=" + getConnectTimeout() + + ";read=" + getReadTimeout(); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java ---------------------------------------------------------------------- diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java b/sshd-common/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java new file mode 100644 index 0000000..b465d14 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java @@ -0,0 +1,639 @@ +/* + * 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.sshd.common.util.net; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.NumberUtils; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * <P>A simple socket address holding the host name and port number. The reason + * it does not extend {@link InetSocketAddress} is twofold:</P> + * <OL> + * <LI><P> + * The {@link InetSocketAddress} performs a DNS resolution on the + * provided host name - which we don't want do use until we want to + * create a connection using this address (thus the {@link #toInetSocketAddress()} + * call which executes this query + * </P></LI> + * + * <LI><P> + * If empty host name is provided we replace it with the <I>any</I> + * address of 0.0.0.0 + * </P></LI> + * </OL> + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class SshdSocketAddress extends SocketAddress { + public static final String LOCALHOST_NAME = "localhost"; + public static final String LOCALHOST_IPV4 = "127.0.0.1"; + public static final String IPV4_ANYADDR = "0.0.0.0"; + + public static final Set<String> WELL_KNOWN_IPV4_ADDRESSES = + Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(LOCALHOST_IPV4, IPV4_ANYADDR))); + + // 10.0.0.0 - 10.255.255.255 + public static final String PRIVATE_CLASS_A_PREFIX = "10."; + // 172.16.0.0 - 172.31.255.255 + public static final String PRIVATE_CLASS_B_PREFIX = "172."; + // 192.168.0.0 - 192.168.255.255 + public static final String PRIVATE_CLASS_C_PREFIX = "192.168."; + // 100.64.0.0 - 100.127.255.255 + public static final String CARRIER_GRADE_NAT_PREFIX = "100."; + // The IPv4 broadcast address + public static final String BROADCAST_ADDRESS = "255.255.255.255"; + + /** Max. number of hex groups (separated by ":") in an IPV6 address */ + public static final int IPV6_MAX_HEX_GROUPS = 8; + + /** Max. hex digits in each IPv6 group */ + public static final int IPV6_MAX_HEX_DIGITS_PER_GROUP = 4; + + public static final String IPV6_LONG_ANY_ADDRESS = "0:0:0:0:0:0:0:0"; + public static final String IPV6_SHORT_ANY_ADDRESS = "::"; + + public static final String IPV6_LONG_LOCALHOST = "0:0:0:0:0:0:0:1"; + public static final String IPV6_SHORT_LOCALHOST = "::1"; + + public static final Set<String> WELL_KNOWN_IPV6_ADDRESSES = + Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList( + IPV6_LONG_LOCALHOST, IPV6_SHORT_LOCALHOST, + IPV6_LONG_ANY_ADDRESS, IPV6_SHORT_ANY_ADDRESS))); + + /** + * A dummy placeholder that can be used instead of {@code null}s + */ + public static final SshdSocketAddress LOCALHOST_ADDRESS = new SshdSocketAddress(LOCALHOST_IPV4, 0); + + /** + * Compares {@link InetAddress}-es according to their {@link InetAddress#getHostAddress()} + * value case <U>insensitive</U> + * + * @see #toAddressString(InetAddress) + */ + public static final Comparator<InetAddress> BY_HOST_ADDRESS = (a1, a2) -> { + String n1 = GenericUtils.trimToEmpty(toAddressString(a1)); + String n2 = GenericUtils.trimToEmpty(toAddressString(a2)); + return String.CASE_INSENSITIVE_ORDER.compare(n1, n2); + }; + + /** + * Compares {@link SocketAddress}-es according to their host case <U>insensitive</U> + * and if equals, then according to their port value (if any) + * + * @see #toAddressString(SocketAddress) + * @see #toAddressPort(SocketAddress) + */ + public static final Comparator<SocketAddress> BY_HOST_AND_PORT = (a1, a2) -> { + String n1 = GenericUtils.trimToEmpty(toAddressString(a1)); + String n2 = GenericUtils.trimToEmpty(toAddressString(a2)); + int nRes = String.CASE_INSENSITIVE_ORDER.compare(n1, n2); + if (nRes != 0) { + return nRes; + } + + int p1 = toAddressPort(a1); + int p2 = toAddressPort(a2); + nRes = Integer.compare(p1, p2); + if (nRes != 0) { + return nRes; + } + + return 0; + }; + + private static final long serialVersionUID = 6461645947151952729L; + + private final String hostName; + private final int port; + + public SshdSocketAddress(int port) { + this(IPV4_ANYADDR, port); + } + + public SshdSocketAddress(InetSocketAddress addr) { + Objects.requireNonNull(addr, "No address provided"); + + String host = addr.getHostString(); + hostName = GenericUtils.isEmpty(host) ? IPV4_ANYADDR : host; + port = addr.getPort(); + ValidateUtils.checkTrue(port >= 0, "Port must be >= 0: %d", port); + } + + public SshdSocketAddress(String hostName, int port) { + Objects.requireNonNull(hostName, "Host name may not be null"); + this.hostName = GenericUtils.isEmpty(hostName) ? IPV4_ANYADDR : hostName; + + ValidateUtils.checkTrue(port >= 0, "Port must be >= 0: %d", port); + this.port = port; + } + + public String getHostName() { + return hostName; + } + + public int getPort() { + return port; + } + + public InetSocketAddress toInetSocketAddress() { + return new InetSocketAddress(getHostName(), getPort()); + } + + @Override + public String toString() { + return getHostName() + ":" + getPort(); + } + + protected boolean isEquivalent(SshdSocketAddress that) { + if (that == null) { + return false; + } else if (that == this) { + return true; + } else { + return (this.getPort() == that.getPort()) + && (GenericUtils.safeCompare(this.getHostName(), that.getHostName(), false) == 0); + } + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + if (getClass() != o.getClass()) { + return false; + } + return isEquivalent((SshdSocketAddress) o); + } + + @Override + public int hashCode() { + return GenericUtils.hashCode(getHostName(), Boolean.FALSE) + getPort(); + } + + + /** + * Returns the first external network address assigned to this + * machine or null if one is not found. + * @return Inet4Address associated with an external interface + * DevNote: We actually return InetAddress here, as Inet4Addresses are final and cannot be mocked. + */ + public static InetAddress getFirstExternalNetwork4Address() { + List<? extends InetAddress> addresses = getExternalNetwork4Addresses(); + return (GenericUtils.size(addresses) > 0) ? addresses.get(0) : null; + } + + /** + * @return a {@link List} of local network addresses which are not multicast + * or localhost sorted according to {@link #BY_HOST_ADDRESS} + */ + public static List<InetAddress> getExternalNetwork4Addresses() { + List<InetAddress> addresses = new ArrayList<>(); + try { + for (Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces(); (nets != null) && nets.hasMoreElements();) { + NetworkInterface netint = nets.nextElement(); + /* TODO - uncomment when 1.5 compatibility no longer required + if (!netint.isUp()) { + continue; // ignore non-running interfaces + } + */ + + for (Enumeration<InetAddress> inetAddresses = netint.getInetAddresses(); (inetAddresses != null) && inetAddresses.hasMoreElements();) { + InetAddress inetAddress = inetAddresses.nextElement(); + if (isValidHostAddress(inetAddress)) { + addresses.add(inetAddress); + } + } + } + } catch (SocketException e) { + // swallow + } + + if (GenericUtils.size(addresses) > 1) { + Collections.sort(addresses, BY_HOST_ADDRESS); + } + + return addresses; + } + + /** + * @param addr The {@link InetAddress} to be verified + * @return <P><code>true</code> if the address is:</P></BR> + * <UL> + * <LI>Not {@code null}</LI> + * <LI>An {@link Inet4Address}</LI> + * <LI>Not link local</LI> + * <LI>Not a multicast</LI> + * <LI>Not a loopback</LI> + * </UL> + * @see InetAddress#isLinkLocalAddress() + * @see InetAddress#isMulticastAddress() + * @see InetAddress#isMulticastAddress() + */ + public static boolean isValidHostAddress(InetAddress addr) { + if (addr == null) { + return false; + } + + if (addr.isLinkLocalAddress()) { + return false; + } + + if (addr.isMulticastAddress()) { + return false; + } + + if (!(addr instanceof Inet4Address)) { + return false; // TODO add support for IPv6 - see SSHD-746 + } + + return !isLoopback(addr); + + } + + /** + * @param addr The {@link InetAddress} to be considered + * @return <code>true</code> if the address is a loopback one. + * <B>Note:</B> if {@link InetAddress#isLoopbackAddress()} + * returns <code>false</code> the address <U>string</U> is checked + * @see #toAddressString(InetAddress) + * @see #isLoopback(String) + */ + public static boolean isLoopback(InetAddress addr) { + if (addr == null) { + return false; + } + + if (addr.isLoopbackAddress()) { + return true; + } + + String ip = toAddressString(addr); + return isLoopback(ip); + } + + /** + * @param ip IP value to be tested + * @return <code>true</code> if the IP is "localhost" or + * "127.x.x.x". + */ + public static boolean isLoopback(String ip) { + if (GenericUtils.isEmpty(ip)) { + return false; + } + + if (LOCALHOST_NAME.equals(ip) || LOCALHOST_IPV4.equals(ip)) { + return true; + } + + // TODO add support for IPv6 - see SSHD-746 + String[] values = GenericUtils.split(ip, '.'); + if (GenericUtils.length(values) != 4) { + return false; + } + + for (int index = 0; index < values.length; index++) { + String val = values[index]; + if (!isValidIPv4AddressComponent(val)) { + return false; + } + + if (index == 0) { + int number = Integer.parseInt(val); + if (number != 127) { + return false; + } + } + } + + return true; + } + + public static SshdSocketAddress toSshdSocketAddress(SocketAddress addr) { + if (addr == null) { + return null; + } else if (addr instanceof SshdSocketAddress) { + return (SshdSocketAddress) addr; + } else if (addr instanceof InetSocketAddress) { + InetSocketAddress isockAddress = (InetSocketAddress) addr; + return new SshdSocketAddress(isockAddress.getHostName(), isockAddress.getPort()); + } else { + throw new UnsupportedOperationException("Cannot convert " + addr.getClass().getSimpleName() + + "=" + addr + " to " + SshdSocketAddress.class.getSimpleName()); + } + } + + public static String toAddressString(SocketAddress addr) { + if (addr == null) { + return null; + } else if (addr instanceof InetSocketAddress) { + return ((InetSocketAddress) addr).getHostString(); + } else if (addr instanceof SshdSocketAddress) { + return ((SshdSocketAddress) addr).getHostName(); + } else { + return addr.toString(); + } + } + + /** + * Attempts to resolve the port value + * + * @param addr The {@link SocketAddress} to examine + * @return The associated port value - negative if failed to resolve + */ + public static int toAddressPort(SocketAddress addr) { + if (addr instanceof InetSocketAddress) { + return ((InetSocketAddress) addr).getPort(); + } else if (addr instanceof SshdSocketAddress) { + return ((SshdSocketAddress) addr).getPort(); + } else { + return -1; + } + } + + /** + * <P>Converts a {@code SocketAddress} into an {@link InetSocketAddress} if possible:</P></BR> + * <UL> + * <LI>If already an {@link InetSocketAddress} then cast it as such</LI> + * <LI>If an {@code SshdSocketAddress} then invoke {@link #toInetSocketAddress()}</LI> + * <LI>Otherwise, throw an exception</LI> + * </UL> + * + * @param remoteAddress The {@link SocketAddress} - ignored if {@code null} + * @return The {@link InetSocketAddress} instance + * @throws ClassCastException if argument is not already an {@code InetSocketAddress} + * or a {@code SshdSocketAddress} + */ + public static InetSocketAddress toInetSocketAddress(SocketAddress remoteAddress) { + if (remoteAddress == null) { + return null; + } else if (remoteAddress instanceof InetSocketAddress) { + return (InetSocketAddress) remoteAddress; + } else if (remoteAddress instanceof SshdSocketAddress) { + return ((SshdSocketAddress) remoteAddress).toInetSocketAddress(); + } else { + throw new ClassCastException("Unknown remote address type: " + remoteAddress); + } + } + + public static String toAddressString(InetAddress addr) { + String ip = (addr == null) ? null : addr.toString(); + if (GenericUtils.isEmpty(ip)) { + return null; + } else { + return ip.replaceAll(".*/", ""); + } + } + + public static boolean isIPv4Address(String addr) { + addr = GenericUtils.trimToEmpty(addr); + if (GenericUtils.isEmpty(addr)) { + return false; + } + + if (WELL_KNOWN_IPV4_ADDRESSES.contains(addr)) { + return true; + } + + String[] comps = GenericUtils.split(addr, '.'); + if (GenericUtils.length(comps) != 4) { + return false; + } + + for (String c : comps) { + if (!isValidIPv4AddressComponent(c)) { + return false; + } + } + + return true; + } + + /** + * Checks if the address is one of the allocated private blocks + * @param addr The address string + * @return {@code true} if this is one of the allocated private + * blocks. <B>Note:</B> it assumes that the address string is + * indeed an IPv4 address + * @see #isIPv4Address(String) + * @see #PRIVATE_CLASS_A_PREFIX + * @see #PRIVATE_CLASS_B_PREFIX + * @see #PRIVATE_CLASS_C_PREFIX + * @see <A HREF="http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces">Wiki page</A> + */ + public static boolean isPrivateIPv4Address(String addr) { + if (GenericUtils.isEmpty(addr)) { + return false; + } + + if (addr.startsWith(PRIVATE_CLASS_A_PREFIX) || addr.startsWith(PRIVATE_CLASS_C_PREFIX)) { + return true; + } + + // for 172.x.x.x we need further checks + if (!addr.startsWith(PRIVATE_CLASS_B_PREFIX)) { + return false; + } + + int nextCompPos = addr.indexOf('.', PRIVATE_CLASS_B_PREFIX.length()); + if (nextCompPos <= PRIVATE_CLASS_B_PREFIX.length()) { + return false; + } + + String value = addr.substring(PRIVATE_CLASS_B_PREFIX.length(), nextCompPos); + if (!isValidIPv4AddressComponent(value)) { + return false; + } + + int v = Integer.parseInt(value); + return (v >= 16) && (v <= 31); + } + + /** + * @param addr The address to be checked + * @return {@code true} if the address is in the 100.64.0.0/10 range + * @see <A HREF="http://tools.ietf.org/html/rfc6598">RFC6598</A> + */ + public static boolean isCarrierGradeNatIPv4Address(String addr) { + if (GenericUtils.isEmpty(addr)) { + return false; + } + + if (!addr.startsWith(CARRIER_GRADE_NAT_PREFIX)) { + return false; + } + + int nextCompPos = addr.indexOf('.', CARRIER_GRADE_NAT_PREFIX.length()); + if (nextCompPos <= CARRIER_GRADE_NAT_PREFIX.length()) { + return false; + } + + String value = addr.substring(CARRIER_GRADE_NAT_PREFIX.length(), nextCompPos); + if (!isValidIPv4AddressComponent(value)) { + return false; + } + + int v = Integer.parseInt(value); + return (v >= 64) && (v <= 127); + } + + /** + * <P>Checks if the provided argument is a valid IPv4 address component:</P></BR> + * <UL> + * <LI>Not {@code null}/empty</LI> + * <LI>Has at most 3 <U>digits</U></LI> + * <LI>Its value is ≤ 255</LI> + * </UL> + * @param c The {@link CharSequence} to be validate + * @return {@code true} if valid IPv4 address component + */ + public static boolean isValidIPv4AddressComponent(CharSequence c) { + if (GenericUtils.isEmpty(c) || (c.length() > 3)) { + return false; + } + + char ch = c.charAt(0); + if ((ch < '0') || (ch > '9')) { + return false; + } + + if (!NumberUtils.isIntegerNumber(c)) { + return false; + } + + int v = Integer.parseInt(c.toString()); + return (v >= 0) && (v <= 255); + } + + // Based on org.apache.commons.validator.routines.InetAddressValidator#isValidInet6Address + public static boolean isIPv6Address(String address) { + address = GenericUtils.trimToEmpty(address); + if (GenericUtils.isEmpty(address)) { + return false; + } + + if (WELL_KNOWN_IPV6_ADDRESSES.contains(address)) { + return true; + } + + boolean containsCompressedZeroes = address.contains("::"); + if (containsCompressedZeroes && (address.indexOf("::") != address.lastIndexOf("::"))) { + return false; + } + + if (((address.indexOf(':') == 0) && (!address.startsWith("::"))) + || (address.endsWith(":") && (!address.endsWith("::")))) { + return false; + } + + String[] splitOctets = GenericUtils.split(address, ':'); + List<String> octetList = new ArrayList<>(Arrays.asList(splitOctets)); + if (containsCompressedZeroes) { + if (address.endsWith("::")) { + // String.split() drops ending empty segments + octetList.add(""); + } else if (address.startsWith("::") && (!octetList.isEmpty())) { + octetList.remove(0); + } + } + + int numOctests = octetList.size(); + if (numOctests > IPV6_MAX_HEX_GROUPS) { + return false; + } + + int validOctets = 0; + int emptyOctets = 0; // consecutive empty chunks + for (int index = 0; index < numOctests; index++) { + String octet = octetList.get(index); + int pos = octet.indexOf('%'); // is it a zone index + if (pos >= 0) { + // zone index must come last + if (index != (numOctests - 1)) { + return false; + } + + octet = (pos > 0) ? octet.substring(0, pos) : ""; + } + + int octetLength = octet.length(); + if (octetLength == 0) { + emptyOctets++; + if (emptyOctets > 1) { + return false; + } + + validOctets++; + continue; + } + + emptyOctets = 0; + + // Is last chunk an IPv4 address? + if ((index == (numOctests - 1)) && (octet.indexOf('.') > 0)) { + if (!isIPv4Address(octet)) { + return false; + } + validOctets += 2; + continue; + } + + if (octetLength > IPV6_MAX_HEX_DIGITS_PER_GROUP) { + return false; + } + + int octetInt = 0; + try { + octetInt = Integer.parseInt(octet, 16); + } catch (NumberFormatException e) { + return false; + } + + if ((octetInt < 0) || (octetInt > 0x000ffff)) { + return false; + } + + validOctets++; + } + + if ((validOctets > IPV6_MAX_HEX_GROUPS) + || ((validOctets < IPV6_MAX_HEX_GROUPS) && (!containsCompressedZeroes))) { + return false; + } + return true; + } +}
