initial commit

stable
Jörg Prante 3 years ago
commit 6eca3c6d2b

1
.gitattributes vendored

@ -0,0 +1 @@
gradlew.bat text eol=crlf

20
.gitignore vendored

@ -0,0 +1,20 @@
data
work
out
logs
/.idea
/target
/.settings
/.classpath
/.project
/.gradle
/plugins
/sessions
.DS_Store
*.iml
*~
.secret
build
**/*.crt
**/*.pkcs8
**/*.gz

@ -0,0 +1,34 @@
plugins {
id "de.marcphilipp.nexus-publish" version "0.4.0"
id "io.codearte.nexus-staging" version "0.21.1"
}
wrapper {
gradleVersion = "${rootProject.property('gradle.wrapper.version')}"
distributionType = Wrapper.DistributionType.ALL
}
ext {
user = 'jprante'
nme = 'files'
description = 'Java Filesystem implementations (FTP, SFTP, WebDAV)'
inceptionYear = '2012'
url = 'https://github.com/' + user + '/' + name
scmUrl = 'https://github.com/' + user + '/' + name
scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
scmDeveloperConnection = 'scm:git:ssh://git@github.com:' + user + '/' + name + '.git'
issueManagementSystem = 'Github'
issueManagementUrl = ext.scmUrl + '/issues'
licenseName = 'The Apache License, Version 2.0'
licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
subprojects {
apply plugin: 'java-library'
apply from: rootProject.file('gradle/ide/idea.gradle')
apply from: rootProject.file('gradle/compile/java.gradle')
apply from: rootProject.file('gradle/test/junit5.gradle')
apply from: rootProject.file('gradle/publishing/publication.gradle')
}
apply from: rootProject.file('gradle/publishing/sonatype.gradle')

@ -0,0 +1,5 @@
This work is derived from
https://github.com/str4d/ed25519-java/
released under Creative Commons Legal Code, CC0 1.0 Universal

@ -0,0 +1,4 @@
dependencies {
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${project.property('junit.version')}"
testImplementation "junit:junit:${project.property('junit4.version')}"
}

@ -0,0 +1,6 @@
module org.xbib.eddsa {
exports org.xbib.io.sshd.eddsa;
exports org.xbib.io.sshd.eddsa.spec;
provides java.security.Provider
with org.xbib.io.sshd.eddsa.EdDSASecurityProvider;
}

@ -0,0 +1,455 @@
package org.xbib.io.sshd.eddsa;
import org.xbib.io.sshd.eddsa.math.Curve;
import org.xbib.io.sshd.eddsa.math.GroupElement;
import org.xbib.io.sshd.eddsa.math.ScalarOps;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
/**
* Signing and verification for EdDSA.
* <p>
* The EdDSA sign and verify algorithms do not interact well with
* the Java Signature API, as one or more update() methods must be
* called before sign() or verify(). Using the standard API,
* this implementation must copy and buffer all data passed in
* via update().
* </p><p>
* This implementation offers two ways to avoid this copying,
* but only if all data to be signed or verified is available
* in a single byte array.
* </p><p>
* Option 1:
* </p><ol>
* <li>Call initSign() or initVerify() as usual.
* </li><li>Call setParameter(ONE_SHOT_MODE)
* </li><li>Call update(byte[]) or update(byte[], int, int) exactly once
* </li><li>Call sign() or verify() as usual.
* </li><li>If doing additional one-shot signs or verifies with this object, you must
* call setParameter(ONE_SHOT_MODE) each time
* </li></ol>
* <p>
* Option 2:
* </p><ol>
* <li>Call initSign() or initVerify() as usual.
* </li><li>Call one of the signOneShot() or verifyOneShot() methods.
* </li><li>If doing additional one-shot signs or verifies with this object,
* just call signOneShot() or verifyOneShot() again.
* </li></ol>
*/
public final class EdDSAEngine extends Signature {
public static final String SIGNATURE_ALGORITHM = "NONEwithEdDSA";
/**
* To efficiently sign or verify data in one shot, pass this to setParameters()
* after initSign() or initVerify() but BEFORE THE FIRST AND ONLY
* update(data) or update(data, off, len). The data reference will be saved
* and then used in sign() or verify() without copying the data.
* Violate these rules and you will get a SignatureException.
*/
public static final AlgorithmParameterSpec ONE_SHOT_MODE = new OneShotSpec();
private MessageDigest digest;
private ByteArrayOutputStream baos;
private EdDSAKey key;
private boolean oneShotMode;
private byte[] oneShotBytes;
private int oneShotOffset;
private int oneShotLength;
/**
* No specific EdDSA-internal hash requested, allows any EdDSA key.
*/
public EdDSAEngine() {
super(SIGNATURE_ALGORITHM);
}
/**
* Specific EdDSA-internal hash requested, only matching keys will be allowed.
*
* @param digest the hash algorithm that keys must have to sign or verify.
*/
public EdDSAEngine(MessageDigest digest) {
this();
this.digest = digest;
}
private void reset() {
if (digest != null) {
digest.reset();
}
if (baos != null) {
baos.reset();
}
oneShotMode = false;
oneShotBytes = null;
}
@Override
protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
reset();
if (privateKey instanceof EdDSAPrivateKey) {
EdDSAPrivateKey privKey = (EdDSAPrivateKey) privateKey;
key = privKey;
if (digest == null) {
try {
digest = MessageDigest.getInstance(key.getParams().getHashAlgorithm());
} catch (NoSuchAlgorithmException e) {
throw new InvalidKeyException("cannot get required digest " + key.getParams().getHashAlgorithm() + " for private key.");
}
} else if (!key.getParams().getHashAlgorithm().equals(digest.getAlgorithm()))
throw new InvalidKeyException("Key hash algorithm does not match chosen digest");
digestInitSign(privKey);
} else {
throw new InvalidKeyException("cannot identify EdDSA private key: " + privateKey.getClass());
}
}
private void digestInitSign(EdDSAPrivateKey privKey) {
// Preparing for hash
// r = H(h_b,...,h_2b-1,M)
int b = privKey.getParams().getCurve().getField().getb();
digest.update(privKey.getH(), b / 8, b / 4 - b / 8);
}
@Override
protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
reset();
if (publicKey instanceof EdDSAPublicKey) {
key = (EdDSAPublicKey) publicKey;
if (digest == null) {
try {
digest = MessageDigest.getInstance(key.getParams().getHashAlgorithm());
} catch (NoSuchAlgorithmException e) {
throw new InvalidKeyException("cannot get required digest " + key.getParams().getHashAlgorithm() + " for private key.");
}
} else if (!key.getParams().getHashAlgorithm().equals(digest.getAlgorithm()))
throw new InvalidKeyException("Key hash algorithm does not match chosen digest");
} else {
throw new InvalidKeyException("cannot identify EdDSA public key: " + publicKey.getClass());
}
}
/**
* @throws SignatureException if in one-shot mode
*/
@Override
protected void engineUpdate(byte b) throws SignatureException {
if (oneShotMode)
throw new SignatureException("unsupported in one-shot mode");
if (baos == null)
baos = new ByteArrayOutputStream(256);
baos.write(b);
}
/**
* @throws SignatureException if one-shot rules are violated
*/
@Override
protected void engineUpdate(byte[] b, int off, int len)
throws SignatureException {
if (oneShotMode) {
if (oneShotBytes != null)
throw new SignatureException("update() already called");
oneShotBytes = b;
oneShotOffset = off;
oneShotLength = len;
} else {
if (baos == null)
baos = new ByteArrayOutputStream(256);
baos.write(b, off, len);
}
}
@Override
protected byte[] engineSign() throws SignatureException {
try {
return x_engineSign();
} finally {
reset();
// must leave the object ready to sign again with
// the same key, as required by the API
EdDSAPrivateKey privKey = (EdDSAPrivateKey) key;
digestInitSign(privKey);
}
}
private byte[] x_engineSign() throws SignatureException {
Curve curve = key.getParams().getCurve();
ScalarOps sc = key.getParams().getScalarOps();
byte[] a = ((EdDSAPrivateKey) key).geta();
byte[] message;
int offset, length;
if (oneShotMode) {
if (oneShotBytes == null)
throw new SignatureException("update() not called first");
message = oneShotBytes;
offset = oneShotOffset;
length = oneShotLength;
} else {
if (baos == null)
message = new byte[0];
else
message = baos.toByteArray();
offset = 0;
length = message.length;
}
// r = H(h_b,...,h_2b-1,M)
digest.update(message, offset, length);
byte[] r = digest.digest();
// r mod l
// Reduces r from 64 bytes to 32 bytes
r = sc.reduce(r);
// R = rB
GroupElement R = key.getParams().getB().scalarMultiply(r);
byte[] Rbyte = R.toByteArray();
// S = (r + H(Rbar,Abar,M)*a) mod l
digest.update(Rbyte);
digest.update(((EdDSAPrivateKey) key).getAbyte());
digest.update(message, offset, length);
byte[] h = digest.digest();
h = sc.reduce(h);
byte[] S = sc.multiplyAndAdd(h, a, r);
// R+S
int b = curve.getField().getb();
ByteBuffer out = ByteBuffer.allocate(b / 4);
out.put(Rbyte).put(S);
return out.array();
}
@Override
protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
try {
return x_engineVerify(sigBytes);
} finally {
reset();
}
}
private boolean x_engineVerify(byte[] sigBytes) throws SignatureException {
Curve curve = key.getParams().getCurve();
int b = curve.getField().getb();
if (sigBytes.length != b / 4)
throw new SignatureException("signature length is wrong");
// R is first b/8 bytes of sigBytes, S is second b/8 bytes
digest.update(sigBytes, 0, b / 8);
digest.update(((EdDSAPublicKey) key).getAbyte());
// h = H(Rbar,Abar,M)
byte[] message;
int offset, length;
if (oneShotMode) {
if (oneShotBytes == null)
throw new SignatureException("update() not called first");
message = oneShotBytes;
offset = oneShotOffset;
length = oneShotLength;
} else {
if (baos == null)
message = new byte[0];
else
message = baos.toByteArray();
offset = 0;
length = message.length;
}
digest.update(message, offset, length);
byte[] h = digest.digest();
// h mod l
h = key.getParams().getScalarOps().reduce(h);
byte[] Sbyte = Arrays.copyOfRange(sigBytes, b / 8, b / 4);
// R = SB - H(Rbar,Abar,M)A
GroupElement R = key.getParams().getB().doubleScalarMultiplyVariableTime(
((EdDSAPublicKey) key).getNegativeA(), h, Sbyte);
// Variable time. This should be okay, because there are no secret
// values used anywhere in verification.
byte[] Rcalc = R.toByteArray();
for (int i = 0; i < Rcalc.length; i++) {
if (Rcalc[i] != sigBytes[i])
return false;
}
return true;
}
/**
* To efficiently sign all the data in one shot, if it is available,
* use this method, which will avoid copying the data.
* <p>
* Same as:
* <pre>
* setParameter(ONE_SHOT_MODE)
* update(data)
* sig = sign()
* </pre>
*
* @param data the message to be signed
* @return the signature
* @throws SignatureException if update() already called
* @see #ONE_SHOT_MODE
*/
public byte[] signOneShot(byte[] data) throws SignatureException {
return signOneShot(data, 0, data.length);
}
/**
* To efficiently sign all the data in one shot, if it is available,
* use this method, which will avoid copying the data.
* <p>
* Same as:
* <pre>
* setParameter(ONE_SHOT_MODE)
* update(data, off, len)
* sig = sign()
* </pre>
*
* @param data byte array containing the message to be signed
* @param off the start of the message inside data
* @param len the length of the message
* @return the signature
* @throws SignatureException if update() already called
* @see #ONE_SHOT_MODE
*/
public byte[] signOneShot(byte[] data, int off, int len) throws SignatureException {
oneShotMode = true;
update(data, off, len);
return sign();
}
/**
* To efficiently verify all the data in one shot, if it is available,
* use this method, which will avoid copying the data.
* <p>
* Same as:
* <pre>
* setParameter(ONE_SHOT_MODE)
* update(data)
* ok = verify(signature)
* </pre>
*
* @param data the message that was signed
* @param signature of the message
* @return true if the signature is valid, false otherwise
* @throws SignatureException if update() already called
* @see #ONE_SHOT_MODE
*/
public boolean verifyOneShot(byte[] data, byte[] signature) throws SignatureException {
return verifyOneShot(data, 0, data.length, signature, 0, signature.length);
}
/**
* To efficiently verify all the data in one shot, if it is available,
* use this method, which will avoid copying the data.
* <p>
* Same as:
* <pre>
* setParameter(ONE_SHOT_MODE)
* update(data, off, len)
* ok = verify(signature)
* </pre>
*
* @param data byte array containing the message that was signed
* @param off the start of the message inside data
* @param len the length of the message
* @param signature of the message
* @return true if the signature is valid, false otherwise
* @throws SignatureException if update() already called
* @see #ONE_SHOT_MODE
*/
public boolean verifyOneShot(byte[] data, int off, int len, byte[] signature) throws SignatureException {
return verifyOneShot(data, off, len, signature, 0, signature.length);
}
/**
* To efficiently verify all the data in one shot, if it is available,
* use this method, which will avoid copying the data.
* <p>
* Same as:
* <pre>
* setParameter(ONE_SHOT_MODE)
* update(data)
* ok = verify(signature, sigoff, siglen)
* </pre>
*
* @param data the message that was signed
* @param signature byte array containing the signature
* @param sigoff the start of the signature
* @param siglen the length of the signature
* @return true if the signature is valid, false otherwise
* @throws SignatureException if update() already called
* @see #ONE_SHOT_MODE
*/
public boolean verifyOneShot(byte[] data, byte[] signature, int sigoff, int siglen) throws SignatureException {
return verifyOneShot(data, 0, data.length, signature, sigoff, siglen);
}
/**
* To efficiently verify all the data in one shot, if it is available,
* use this method, which will avoid copying the data.
* <p>
* Same as:
* <pre>
* setParameter(ONE_SHOT_MODE)
* update(data, off, len)
* ok = verify(signature, sigoff, siglen)
* </pre>
*
* @param data byte array containing the message that was signed
* @param off the start of the message inside data
* @param len the length of the message
* @param signature byte array containing the signature
* @param sigoff the start of the signature
* @param siglen the length of the signature
* @return true if the signature is valid, false otherwise
* @throws SignatureException if update() already called
* @see #ONE_SHOT_MODE
*/
public boolean verifyOneShot(byte[] data, int off, int len, byte[] signature, int sigoff, int siglen) throws SignatureException {
oneShotMode = true;
update(data, off, len);
return verify(signature, sigoff, siglen);
}
/**
* @throws InvalidAlgorithmParameterException if spec is ONE_SHOT_MODE and update() already called
* @see #ONE_SHOT_MODE
*/
@Override
protected void engineSetParameter(AlgorithmParameterSpec spec) throws InvalidAlgorithmParameterException {
if (spec.equals(ONE_SHOT_MODE)) {
if (oneShotBytes != null || (baos != null && baos.size() > 0))
throw new InvalidAlgorithmParameterException("update() already called");
oneShotMode = true;
} else {
super.engineSetParameter(spec);
}
}
@Override
protected void engineSetParameter(String param, Object value) {
throw new UnsupportedOperationException("engineSetParameter unsupported");
}
@Override
protected Object engineGetParameter(String param) {
throw new UnsupportedOperationException("engineSetParameter unsupported");
}
private static class OneShotSpec implements AlgorithmParameterSpec {
}
}

@ -0,0 +1,19 @@
package org.xbib.io.sshd.eddsa;
import org.xbib.io.sshd.eddsa.spec.EdDSAParameterSpec;
/**
* Common interface for all EdDSA keys.
*/
public interface EdDSAKey {
/**
* The reported key algorithm for all EdDSA keys
*/
String KEY_ALGORITHM = "EdDSA";
/**
* @return a parameter specification representing the EdDSA domain
* parameters for the key.
*/
EdDSAParameterSpec getParams();
}

@ -0,0 +1,321 @@
package org.xbib.io.sshd.eddsa;
import org.xbib.io.sshd.eddsa.math.GroupElement;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveTable;
import org.xbib.io.sshd.eddsa.spec.EdDSAParameterSpec;
import org.xbib.io.sshd.eddsa.spec.EdDSAPrivateKeySpec;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
/**
* An EdDSA private key.
* <p>
* Warning: Private key encoding is based on the current curdle WG draft,
* and is subject to change. See getEncoded().
* </p><p>
* For compatibility with older releases, decoding supports both the old and new
* draft specifications. See decode().
* </p><p>
* Ref: https://tools.ietf.org/html/draft-ietf-curdle-pkix-04
* </p><p>
* Old Ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04
* </p>
*/
public class EdDSAPrivateKey implements EdDSAKey, PrivateKey {
// compatible with JDK 1.1
private static final long serialVersionUID = 23495873459878957L;
// OID 1.3.101.xxx
private static final int OID_OLD = 100;
private static final int OID_ED25519 = 112;
private static final int OID_BYTE = 11;
private static final int IDLEN_BYTE = 6;
private final byte[] seed;
private final byte[] h;
private final byte[] a;
private final GroupElement A;
private final byte[] Abyte;
private final EdDSAParameterSpec edDsaSpec;
public EdDSAPrivateKey(EdDSAPrivateKeySpec spec) {
this.seed = spec.getSeed();
this.h = spec.getH();
this.a = spec.geta();
this.A = spec.getA();
this.Abyte = this.A.toByteArray();
this.edDsaSpec = spec.getParams();
}
public EdDSAPrivateKey(PKCS8EncodedKeySpec spec) throws InvalidKeySpecException {
this(new EdDSAPrivateKeySpec(decode(spec.getEncoded()),
EdDSANamedCurveTable.getByName("Ed25519")));
}
/**
* Extracts the private key bytes from the provided encoding.
* <p>
* This will decode data conforming to the current spec at
* https://tools.ietf.org/html/draft-ietf-curdle-pkix-04
* or as inferred from the old spec at
* https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04.
* </p><p>
* Contrary to draft-ietf-curdle-pkix-04, it WILL accept a parameter value
* of NULL, as it is required for interoperability with the default Java
* keystore. Other implementations MUST NOT copy this behaviour from here
* unless they also need to read keys from the default Java keystore.
* </p><p>
* This is really dumb for now. It does not use a general-purpose ASN.1 decoder.
* See also getEncoded().
*
* @return 32 bytes for Ed25519, throws for other curves
*/
private static byte[] decode(byte[] d) throws InvalidKeySpecException {
try {
//
// Setup and OID check
//
int totlen = 48;
int idlen = 5;
int doid = d[OID_BYTE];
if (doid == OID_OLD) {
totlen = 49;
idlen = 8;
} else if (doid == OID_ED25519) {
// Detect parameter value of NULL
if (d[IDLEN_BYTE] == 7) {
totlen = 50;
idlen = 7;
}
} else {
throw new InvalidKeySpecException("unsupported key spec");
}
//
// Pre-decoding check
//
if (d.length != totlen) {
throw new InvalidKeySpecException("invalid key spec length");
}
//
// Decoding
//
int idx = 0;
if (d[idx++] != 0x30 ||
d[idx++] != (totlen - 2) ||
d[idx++] != 0x02 ||
d[idx++] != 1 ||
d[idx++] != 0 ||
d[idx++] != 0x30 ||
d[idx++] != idlen ||
d[idx++] != 0x06 ||
d[idx++] != 3 ||
d[idx++] != (1 * 40) + 3 ||
d[idx++] != 101) {
throw new InvalidKeySpecException("unsupported key spec");
}
idx++; // OID, checked above
// parameters only with old OID
if (doid == OID_OLD) {
if (d[idx++] != 0x0a ||
d[idx++] != 1 ||
d[idx++] != 1) {
throw new InvalidKeySpecException("unsupported key spec");
}
} else {
// Handle parameter value of NULL
//
// Quote https://tools.ietf.org/html/draft-ietf-curdle-pkix-04 :
// For all of the OIDs, the parameters MUST be absent.
// Regardless of the defect in the original 1997 syntax,
// implementations MUST NOT accept a parameters value of NULL.
//
// But Java's default keystore puts it in (when decoding as
// PKCS8 and then re-encoding to pass on), so we must accept it.
if (idlen == 7) {
if (d[idx++] != 0x05 ||
d[idx++] != 0) {
throw new InvalidKeySpecException("unsupported key spec");
}
}
// PrivateKey wrapping the CurvePrivateKey
if (d[idx++] != 0x04 ||
d[idx++] != 34) {
throw new InvalidKeySpecException("unsupported key spec");
}
}
if (d[idx++] != 0x04 ||
d[idx++] != 32) {
throw new InvalidKeySpecException("unsupported key spec");
}
byte[] rv = new byte[32];
System.arraycopy(d, idx, rv, 0, 32);
return rv;
} catch (IndexOutOfBoundsException ioobe) {
throw new InvalidKeySpecException(ioobe);
}
}
@Override
public String getAlgorithm() {
return KEY_ALGORITHM;
}
@Override
public String getFormat() {
return "PKCS#8";
}
/**
* Returns the public key in its canonical encoding.
* This implements the following specs:
* <ul>
* <li>General encoding: https://tools.ietf.org/html/draft-ietf-curdle-pkix-04</li>
* <li>Key encoding: https://tools.ietf.org/html/rfc8032</li>
* </ul>
* <p>
* This encodes the seed. It will return null if constructed from
* a spec which was directly constructed from H, in which case seed is null.
* </p><p>
* For keys in older formats, decoding and then re-encoding is sufficient to
* migrate them to the canonical encoding.
* </p>
* Relevant spec quotes:
* <pre>
* OneAsymmetricKey ::= SEQUENCE {
* version Version,
* privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
* privateKey PrivateKey,
* attributes [0] Attributes OPTIONAL,
* ...,
* [[2: publicKey [1] PublicKey OPTIONAL ]],
* ...
* }
*
* Version ::= INTEGER
* PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
* PrivateKey ::= OCTET STRING
* PublicKey ::= OCTET STRING
* Attributes ::= SET OF Attribute
* </pre>
* <pre>
* ... when encoding a OneAsymmetricKey object, the private key is wrapped
* in a CurvePrivateKey object and wrapped by the OCTET STRING of the
* 'privateKey' field.
*
* CurvePrivateKey ::= OCTET STRING
* </pre>
* <pre>
* AlgorithmIdentifier ::= SEQUENCE {
* algorithm OBJECT IDENTIFIER,
* parameters ANY DEFINED BY algorithm OPTIONAL
* }
*
* For all of the OIDs, the parameters MUST be absent.
* </pre>
* <pre>
* id-Ed25519 OBJECT IDENTIFIER ::= { 1 3 101 112 }
* </pre>
*
* @return 48 bytes for Ed25519, null for other curves
*/
@Override
public byte[] getEncoded() {
if (!edDsaSpec.equals(EdDSANamedCurveTable.getByName("Ed25519")))
return null;
if (seed == null)
return null;
int totlen = 16 + seed.length;
byte[] rv = new byte[totlen];
int idx = 0;
// sequence
rv[idx++] = 0x30;
rv[idx++] = (byte) (totlen - 2);
// version
rv[idx++] = 0x02;
rv[idx++] = 1;
// v1 - no public key included
rv[idx++] = 0;
// Algorithm Identifier
// sequence
rv[idx++] = 0x30;
rv[idx++] = 5;
// OID
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb540809%28v=vs.85%29.aspx
rv[idx++] = 0x06;
rv[idx++] = 3;
rv[idx++] = (1 * 40) + 3;
rv[idx++] = 101;
rv[idx++] = (byte) OID_ED25519;
// params - absent
// PrivateKey
rv[idx++] = 0x04; // octet string
rv[idx++] = (byte) (2 + seed.length);
// CurvePrivateKey
rv[idx++] = 0x04; // octet string
rv[idx++] = (byte) seed.length;
// the key
System.arraycopy(seed, 0, rv, idx, seed.length);
return rv;
}
@Override
public EdDSAParameterSpec getParams() {
return edDsaSpec;
}
/**
* @return will be null if constructed from a spec which was
* directly constructed from H
*/
public byte[] getSeed() {
return seed;
}
/**
* @return the hash of the seed
*/
public byte[] getH() {
return h;
}
/**
* @return the private key
*/
public byte[] geta() {
return a;
}
/**
* @return the public key
*/
public GroupElement getA() {
return A;
}
/**
* @return the public key
*/
public byte[] getAbyte() {
return Abyte;
}
@Override
public int hashCode() {
return Arrays.hashCode(seed);
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof EdDSAPrivateKey))
return false;
EdDSAPrivateKey pk = (EdDSAPrivateKey) o;
return Arrays.equals(seed, pk.getSeed()) &&
edDsaSpec.equals(pk.getParams());
}
}

@ -0,0 +1,257 @@
package org.xbib.io.sshd.eddsa;
import org.xbib.io.sshd.eddsa.math.GroupElement;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveTable;
import org.xbib.io.sshd.eddsa.spec.EdDSAParameterSpec;
import org.xbib.io.sshd.eddsa.spec.EdDSAPublicKeySpec;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
/**
* An EdDSA public key.
* <p>
* Warning: Public key encoding is is based on the current curdle WG draft,
* and is subject to change. See getEncoded().
* </p><p>
* For compatibility with older releases, decoding supports both the old and new
* draft specifications. See decode().
* </p><p>
* Ref: https://tools.ietf.org/html/draft-ietf-curdle-pkix-04
* </p><p>
* Old Ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04
* </p>
*/
public class EdDSAPublicKey implements EdDSAKey, PublicKey {
private static final long serialVersionUID = 9837459837498475L;
// OID 1.3.101.xxx
private static final int OID_OLD = 100;
private static final int OID_ED25519 = 112;
private static final int OID_BYTE = 8;
private static final int IDLEN_BYTE = 3;
private final GroupElement A;
private final GroupElement Aneg;
private final byte[] Abyte;
private final EdDSAParameterSpec edDsaSpec;
public EdDSAPublicKey(EdDSAPublicKeySpec spec) {
this.A = spec.getA();
this.Aneg = spec.getNegativeA();
this.Abyte = this.A.toByteArray();
this.edDsaSpec = spec.getParams();
}
public EdDSAPublicKey(X509EncodedKeySpec spec) throws InvalidKeySpecException {
this(new EdDSAPublicKeySpec(decode(spec.getEncoded()),
EdDSANamedCurveTable.getByName("Ed25519")));
}
/**
* Extracts the public key bytes from the provided encoding.
* <p>
* This will decode data conforming to the current spec at
* https://tools.ietf.org/html/draft-ietf-curdle-pkix-04
* or the old spec at
* https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04.
* </p><p>
* Contrary to draft-ietf-curdle-pkix-04, it WILL accept a parameter value
* of NULL, as it is required for interoperability with the default Java
* keystore. Other implementations MUST NOT copy this behaviour from here
* unless they also need to read keys from the default Java keystore.
* </p><p>
* This is really dumb for now. It does not use a general-purpose ASN.1 decoder.
* See also getEncoded().
* </p>
*
* @return 32 bytes for Ed25519, throws for other curves
*/
private static byte[] decode(byte[] d) throws InvalidKeySpecException {
try {
//
// Setup and OID check
//
int totlen = 44;
int idlen = 5;
int doid = d[OID_BYTE];
if (doid == OID_OLD) {
totlen = 47;
idlen = 8;
} else if (doid == OID_ED25519) {
// Detect parameter value of NULL
if (d[IDLEN_BYTE] == 7) {
totlen = 46;
idlen = 7;
}
} else {
throw new InvalidKeySpecException("unsupported key spec");
}
//
// Pre-decoding check
//
if (d.length != totlen) {
throw new InvalidKeySpecException("invalid key spec length");
}
//
// Decoding
//
int idx = 0;
if (d[idx++] != 0x30 ||
d[idx++] != (totlen - 2) ||
d[idx++] != 0x30 ||
d[idx++] != idlen ||
d[idx++] != 0x06 ||
d[idx++] != 3 ||
d[idx++] != (1 * 40) + 3 ||
d[idx++] != 101) {
throw new InvalidKeySpecException("unsupported key spec");
}
idx++; // OID, checked above
// parameters only with old OID
if (doid == OID_OLD) {
if (d[idx++] != 0x0a ||
d[idx++] != 1 ||
d[idx++] != 1) {
throw new InvalidKeySpecException("unsupported key spec");
}
} else {
// Handle parameter value of NULL
//
// Quote https://tools.ietf.org/html/draft-ietf-curdle-pkix-04 :
// For all of the OIDs, the parameters MUST be absent.
// Regardless of the defect in the original 1997 syntax,
// implementations MUST NOT accept a parameters value of NULL.
//
// But Java's default keystore puts it in (when decoding as
// PKCS8 and then re-encoding to pass on), so we must accept it.
if (idlen == 7) {
if (d[idx++] != 0x05 ||
d[idx++] != 0) {
throw new InvalidKeySpecException("unsupported key spec");
}
}
}
if (d[idx++] != 0x03 ||
d[idx++] != 33 ||
d[idx++] != 0) {
throw new InvalidKeySpecException("unsupported key spec");
}
byte[] rv = new byte[32];
System.arraycopy(d, idx, rv, 0, 32);
return rv;
} catch (IndexOutOfBoundsException ioobe) {
throw new InvalidKeySpecException(ioobe);
}
}
@Override
public String getAlgorithm() {
return KEY_ALGORITHM;
}
@Override
public String getFormat() {
return "X.509";
}
/**
* Returns the public key in its canonical encoding.
* This implements the following specs:
* <ul>
* <li>General encoding: https://tools.ietf.org/html/draft-ietf-curdle-pkix-04</li>
* <li>Key encoding: https://tools.ietf.org/html/rfc8032</li>
* </ul>
* <p>
* For keys in older formats, decoding and then re-encoding is sufficient to
* migrate them to the canonical encoding.
* </p>
* Relevant spec quotes:
* <pre>
* In the X.509 certificate, the subjectPublicKeyInfo field has the
* SubjectPublicKeyInfo type, which has the following ASN.1 syntax:
*
* SubjectPublicKeyInfo ::= SEQUENCE {
* algorithm AlgorithmIdentifier,
* subjectPublicKey BIT STRING
* }
* </pre>
* <pre>
* AlgorithmIdentifier ::= SEQUENCE {
* algorithm OBJECT IDENTIFIER,
* parameters ANY DEFINED BY algorithm OPTIONAL
* }
*
* For all of the OIDs, the parameters MUST be absent.
* </pre>
* <pre>
* id-Ed25519 OBJECT IDENTIFIER ::= { 1 3 101 112 }
* </pre>
*
* @return 44 bytes for Ed25519, null for other curves
*/
@Override
public byte[] getEncoded() {
if (!edDsaSpec.equals(EdDSANamedCurveTable.getByName("Ed25519")))
return null;
int totlen = 12 + Abyte.length;
byte[] rv = new byte[totlen];
int idx = 0;
// sequence
rv[idx++] = 0x30;
rv[idx++] = (byte) (totlen - 2);
// Algorithm Identifier
// sequence
rv[idx++] = 0x30;
rv[idx++] = 5;
// OID
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb540809%28v=vs.85%29.aspx
rv[idx++] = 0x06;
rv[idx++] = 3;
rv[idx++] = (1 * 40) + 3;
rv[idx++] = 101;
rv[idx++] = (byte) OID_ED25519;
// params - absent
// the key
rv[idx++] = 0x03; // bit string
rv[idx++] = (byte) (1 + Abyte.length);
rv[idx++] = 0; // number of trailing unused bits
System.arraycopy(Abyte, 0, rv, idx, Abyte.length);
return rv;
}
@Override
public EdDSAParameterSpec getParams() {
return edDsaSpec;
}
public GroupElement getA() {
return A;
}
public GroupElement getNegativeA() {
return Aneg;
}
public byte[] getAbyte() {
return Abyte;
}
@Override
public int hashCode() {
return Arrays.hashCode(Abyte);
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof EdDSAPublicKey))
return false;
EdDSAPublicKey pk = (EdDSAPublicKey) o;
return Arrays.equals(Abyte, pk.getAbyte()) &&
edDsaSpec.equals(pk.getParams());
}
}

@ -0,0 +1,45 @@
package org.xbib.io.sshd.eddsa;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.Provider;
import java.security.Security;
/**
* A security {@link Provider} that can be registered via {@link Security#addProvider(Provider)}.
*/
public class EdDSASecurityProvider extends Provider {
public static final String PROVIDER_NAME = "EdDSA";
private static final long serialVersionUID = 1210027906682292307L;
public EdDSASecurityProvider() {
super(PROVIDER_NAME, 0.1, "xbib " + PROVIDER_NAME + " security provider wrapper");
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
setup();
return null;
});
}
protected void setup() {
// See https://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/HowToImplAProvider.html
put("KeyFactory." + EdDSAKey.KEY_ALGORITHM, "org.xbib.io.sshd.eddsa.KeyFactory");
put("KeyPairGenerator." + EdDSAKey.KEY_ALGORITHM, "org.xbib.io.sshd.eddsa.KeyPairGenerator");
put("Signature." + EdDSAEngine.SIGNATURE_ALGORITHM, "org.xbib.io.sshd.eddsa.EdDSAEngine");
// OID Mappings
// See section "Mapping from OID to name".
// The Key* -> OID mappings correspond to the default algorithm in KeyPairGenerator.
//
// From draft-ieft-curdle-pkix-04:
// id-Ed25519 OBJECT IDENTIFIER ::= { 1 3 101 112 }
put("Alg.Alias.KeyFactory.1.3.101.112", EdDSAKey.KEY_ALGORITHM);
put("Alg.Alias.KeyFactory.OID.1.3.101.112", EdDSAKey.KEY_ALGORITHM);
put("Alg.Alias.KeyPairGenerator.1.3.101.112", EdDSAKey.KEY_ALGORITHM);
put("Alg.Alias.KeyPairGenerator.OID.1.3.101.112", EdDSAKey.KEY_ALGORITHM);
put("Alg.Alias.Signature.1.3.101.112", EdDSAEngine.SIGNATURE_ALGORITHM);
put("Alg.Alias.Signature.OID.1.3.101.112", EdDSAEngine.SIGNATURE_ALGORITHM);
}
}

@ -0,0 +1,62 @@
package org.xbib.io.sshd.eddsa;
import org.xbib.io.sshd.eddsa.spec.EdDSAPrivateKeySpec;
import org.xbib.io.sshd.eddsa.spec.EdDSAPublicKeySpec;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactorySpi;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
*/
public final class KeyFactory extends KeyFactorySpi {
protected PrivateKey engineGeneratePrivate(KeySpec keySpec)
throws InvalidKeySpecException {
if (keySpec instanceof EdDSAPrivateKeySpec) {
return new EdDSAPrivateKey((EdDSAPrivateKeySpec) keySpec);
}
if (keySpec instanceof PKCS8EncodedKeySpec) {
return new EdDSAPrivateKey((PKCS8EncodedKeySpec) keySpec);
}
throw new InvalidKeySpecException("key spec not recognised: " + keySpec.getClass());
}
protected PublicKey engineGeneratePublic(KeySpec keySpec)
throws InvalidKeySpecException {
if (keySpec instanceof EdDSAPublicKeySpec) {
return new EdDSAPublicKey((EdDSAPublicKeySpec) keySpec);
}
if (keySpec instanceof X509EncodedKeySpec) {
return new EdDSAPublicKey((X509EncodedKeySpec) keySpec);
}
throw new InvalidKeySpecException("key spec not recognised: " + keySpec.getClass());
}
@SuppressWarnings("unchecked")
protected <T extends KeySpec> T engineGetKeySpec(Key key, Class<T> keySpec)
throws InvalidKeySpecException {
if (keySpec.isAssignableFrom(EdDSAPublicKeySpec.class) && key instanceof EdDSAPublicKey) {
EdDSAPublicKey k = (EdDSAPublicKey) key;
if (k.getParams() != null) {
return (T) new EdDSAPublicKeySpec(k.getA(), k.getParams());
}
} else if (keySpec.isAssignableFrom(EdDSAPrivateKeySpec.class) && key instanceof EdDSAPrivateKey) {
EdDSAPrivateKey k = (EdDSAPrivateKey) key;
if (k.getParams() != null) {
return (T) new EdDSAPrivateKeySpec(k.getSeed(), k.getH(), k.geta(), k.getA(), k.getParams());
}
}
throw new InvalidKeySpecException("not implemented yet " + key + " " + keySpec);
}
protected Key engineTranslateKey(Key key) throws InvalidKeyException {
throw new InvalidKeyException("No other EdDSA key providers known");
}
}

@ -0,0 +1,86 @@
package org.xbib.io.sshd.eddsa;
import org.xbib.io.sshd.eddsa.spec.EdDSAGenParameterSpec;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveSpec;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveTable;
import org.xbib.io.sshd.eddsa.spec.EdDSAParameterSpec;
import org.xbib.io.sshd.eddsa.spec.EdDSAPrivateKeySpec;
import org.xbib.io.sshd.eddsa.spec.EdDSAPublicKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidParameterException;
import java.security.KeyPair;
import java.security.KeyPairGeneratorSpi;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Hashtable;
/**
* Default keysize is 256 (Ed25519).
*/
public final class KeyPairGenerator extends KeyPairGeneratorSpi {
private static final int DEFAULT_KEYSIZE = 256;
private static final Hashtable<Integer, AlgorithmParameterSpec> edParameters;
static {
edParameters = new Hashtable<>();
edParameters.put(256, new EdDSAGenParameterSpec("Ed25519"));
}
private EdDSAParameterSpec edParams;
private SecureRandom random;
private boolean initialized;
public void initialize(int keysize, SecureRandom random) {
AlgorithmParameterSpec edParams = edParameters.get(keysize);
if (edParams == null)
throw new InvalidParameterException("unknown key type.");
try {
initialize(edParams, random);
} catch (InvalidAlgorithmParameterException e) {
throw new InvalidParameterException("key type not configurable.");
}
}
@Override
public void initialize(AlgorithmParameterSpec params, SecureRandom random) throws InvalidAlgorithmParameterException {
if (params instanceof EdDSAParameterSpec) {
edParams = (EdDSAParameterSpec) params;
} else if (params instanceof EdDSAGenParameterSpec) {
edParams = createNamedCurveSpec(((EdDSAGenParameterSpec) params).getName());
} else
throw new InvalidAlgorithmParameterException("parameter object not a EdDSAParameterSpec");
this.random = random;
initialized = true;
}
public KeyPair generateKeyPair() {
if (!initialized)
initialize(DEFAULT_KEYSIZE, new SecureRandom());
byte[] seed = new byte[edParams.getCurve().getField().getb() / 8];
random.nextBytes(seed);
EdDSAPrivateKeySpec privKey = new EdDSAPrivateKeySpec(seed, edParams);
EdDSAPublicKeySpec pubKey = new EdDSAPublicKeySpec(privKey.getA(), edParams);
return new KeyPair(new EdDSAPublicKey(pubKey), new EdDSAPrivateKey(privKey));
}
/**
* Create an EdDSANamedCurveSpec from the provided curve name. The current
* implementation fetches the pre-created curve spec from a table.
*
* @param curveName the EdDSA named curve.
* @return the specification for the named curve.
* @throws InvalidAlgorithmParameterException if the named curve is unknown.
*/
protected EdDSANamedCurveSpec createNamedCurveSpec(String curveName) throws InvalidAlgorithmParameterException {
EdDSANamedCurveSpec spec = EdDSANamedCurveTable.getByName(curveName);
if (spec == null) {
throw new InvalidAlgorithmParameterException("unknown curve name: " + curveName);
}
return spec;
}
}

@ -0,0 +1,95 @@
package org.xbib.io.sshd.eddsa;
/**
* Basic utilities for EdDSA.
* Not for external use, not maintained as a public API.
*/
public class Utils {
/**
* Constant-time byte comparison.
*
* @param b a byte
* @param c a byte
* @return 1 if b and c are equal, 0 otherwise.
*/
public static int equal(int b, int c) {
int result = 0;
int xor = b ^ c;
for (int i = 0; i < 8; i++) {
result |= xor >> i;
}
return (result ^ 0x01) & 0x01;
}
/**
* Constant-time byte[] comparison.
*
* @param b a byte[]
* @param c a byte[]
* @return 1 if b and c are equal, 0 otherwise.
*/
public static int equal(byte[] b, byte[] c) {
int result = 0;
for (int i = 0; i < 32; i++) {
result |= b[i] ^ c[i];
}
return equal(result, 0);
}
/**
* Constant-time determine if byte is negative.
*
* @param b the byte to check.
* @return 1 if the byte is negative, 0 otherwise.
*/
public static int negative(int b) {
return (b >> 8) & 1;
}
/**
* Get the i'th bit of a byte array.
*
* @param h the byte array.
* @param i the bit index.
* @return 0 or 1, the value of the i'th bit in h
*/
public static int bit(byte[] h, int i) {
return (h[i >> 3] >> (i & 7)) & 1;
}
/**
* Converts a hex string to bytes.
*
* @param s the hex string to be converted.
* @return the byte[]
*/
public static byte[] hexToBytes(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
return data;
}
/**
* Converts bytes to a hex string.
*
* @param raw the byte[] to be converted.
* @return the hex representation as a string.
*/
public static String bytesToHex(byte[] raw) {
if (raw == null) {
return null;
}
final StringBuilder hex = new StringBuilder(2 * raw.length);
for (final byte b : raw) {
hex.append(Character.forDigit((b & 0xF0) >> 4, 16))
.append(Character.forDigit((b & 0x0F), 16));
}
return hex.toString();
}
}

@ -0,0 +1,15 @@
package org.xbib.io.sshd.eddsa.math;
import org.xbib.io.sshd.eddsa.Utils;
/**
*
*/
final class Constants {
public static final byte[] ZERO = Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
public static final byte[] ONE = Utils.hexToBytes("0100000000000000000000000000000000000000000000000000000000000000");
public static final byte[] TWO = Utils.hexToBytes("0200000000000000000000000000000000000000000000000000000000000000");
public static final byte[] FOUR = Utils.hexToBytes("0400000000000000000000000000000000000000000000000000000000000000");
public static final byte[] FIVE = Utils.hexToBytes("0500000000000000000000000000000000000000000000000000000000000000");
public static final byte[] EIGHT = Utils.hexToBytes("0800000000000000000000000000000000000000000000000000000000000000");
}

@ -0,0 +1,87 @@
package org.xbib.io.sshd.eddsa.math;
//import java.io.Serializable;
/**
* A twisted Edwards curve.
* Points on the curve satisfy $-x^2 + y^2 = 1 + d x^2y^2$
*/
public class Curve /*implements Serializable*/ {
//private static final long serialVersionUID = 4578920872509827L;
private final Field f;
private final FieldElement d;
private final FieldElement d2;
private final FieldElement I;
private final GroupElement zeroP2;
private final GroupElement zeroP3;
private final GroupElement zeroPrecomp;
public Curve(Field f, byte[] d, FieldElement I) {
this.f = f;
this.d = f.fromByteArray(d);
this.d2 = this.d.add(this.d);
this.I = I;
FieldElement zero = f.ZERO;
FieldElement one = f.ONE;
zeroP2 = GroupElement.p2(this, zero, one, one);
zeroP3 = GroupElement.p3(this, zero, one, one, zero);
zeroPrecomp = GroupElement.precomp(this, one, one, zero);
}
public Field getField() {
return f;
}
public FieldElement getD() {
return d;
}
public FieldElement get2D() {
return d2;
}
public FieldElement getI() {
return I;
}
public GroupElement getZero(GroupElement.Representation repr) {
switch (repr) {
case P2:
return zeroP2;
case P3:
return zeroP3;
case PRECOMP:
return zeroPrecomp;
default:
return null;
}
}
public GroupElement createPoint(byte[] P, boolean precompute) {
GroupElement ge = new GroupElement(this, P);
if (precompute)
ge.precompute(true);
return ge;
}
@Override
public int hashCode() {
return f.hashCode() ^
d.hashCode() ^
I.hashCode();
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Curve))
return false;
Curve c = (Curve) o;
return f.equals(c.getField()) &&
d.equals(c.getD()) &&
I.equals(c.getI());
}
}

@ -0,0 +1,43 @@
package org.xbib.io.sshd.eddsa.math;
/**
* Common interface for all $(b-1)$-bit encodings of elements of EdDSA finite fields.
*/
public abstract class Encoding {
protected Field f;
public synchronized void setField(Field f) {
if (this.f != null)
throw new IllegalStateException("already set");
this.f = f;
}
/**
* Encode a FieldElement in its $(b-1)$-bit encoding.
*
* @param x the FieldElement to encode
* @return the $(b-1)$-bit encoding of this FieldElement.
*/
public abstract byte[] encode(FieldElement x);
/**
* Decode a FieldElement from its $(b-1)$-bit encoding.
* The highest bit is masked out.
*
* @param in the $(b-1)$-bit encoding of a FieldElement.
* @return the FieldElement represented by 'val'.
*/
public abstract FieldElement decode(byte[] in);
/**
* From the Ed25519 paper:<br>
* $x$ is negative if the $(b-1)$-bit encoding of $x$ is lexicographically larger
* than the $(b-1)$-bit encoding of -x. If $q$ is an odd prime and the encoding
* is the little-endian representation of $\{0, 1,\dots, q-1\}$ then the negative
* elements of $F_q$ are $\{1, 3, 5,\dots, q-2\}$.
*
* @param x the FieldElement to check
* @return true if negative
*/
public abstract boolean isNegative(FieldElement x);
}

@ -0,0 +1,84 @@
package org.xbib.io.sshd.eddsa.math;
/**
* An EdDSA finite field. Includes several pre-computed values.
*/
public class Field /*implements Serializable*/ {
//private static final long serialVersionUID = 8746587465875676L;
public final FieldElement ZERO;
public final FieldElement ONE;
public final FieldElement TWO;
public final FieldElement FOUR;
public final FieldElement FIVE;
public final FieldElement EIGHT;
private final int b;
private final FieldElement q;
/**
* q-2
*/
private final FieldElement qm2;
/**
* (q-5) / 8
*/
private final FieldElement qm5d8;
private final Encoding enc;
public Field(int b, byte[] q, Encoding enc) {
this.b = b;
this.enc = enc;
this.enc.setField(this);
this.q = fromByteArray(q);
// Set up constants
ZERO = fromByteArray(Constants.ZERO);
ONE = fromByteArray(Constants.ONE);
TWO = fromByteArray(Constants.TWO);
FOUR = fromByteArray(Constants.FOUR);
FIVE = fromByteArray(Constants.FIVE);
EIGHT = fromByteArray(Constants.EIGHT);
// Precompute values
qm2 = this.q.subtract(TWO);
qm5d8 = this.q.subtract(FIVE).divide(EIGHT);
}
public FieldElement fromByteArray(byte[] x) {
return enc.decode(x);
}
public int getb() {
return b;
}
public FieldElement getQ() {
return q;
}
public FieldElement getQm2() {
return qm2;
}
public FieldElement getQm5d8() {
return qm5d8;
}
public Encoding getEncoding() {
return enc;
}
@Override
public int hashCode() {
return q.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Field))
return false;
Field f = (Field) obj;
return b == f.b && q.equals(f.q);
}
}

@ -0,0 +1,66 @@
package org.xbib.io.sshd.eddsa.math;
//import java.io.Serializable;
/**
* Note: concrete subclasses must implement hashCode() and equals()
*/
public abstract class FieldElement /*implements Serializable*/ {
//private static final long serialVersionUID = 1239527465875676L;
protected final Field f;
public FieldElement(Field f) {
if (null == f) {
throw new IllegalArgumentException("field cannot be null");
}
this.f = f;
}
/**
* Encode a FieldElement in its $(b-1)$-bit encoding.
*
* @return the $(b-1)$-bit encoding of this FieldElement.
*/
public byte[] toByteArray() {
return f.getEncoding().encode(this);
}
public abstract boolean isNonZero();
public boolean isNegative() {
return f.getEncoding().isNegative(this);
}
public abstract FieldElement add(FieldElement val);
public FieldElement addOne() {
return add(f.ONE);
}
public abstract FieldElement subtract(FieldElement val);
public FieldElement subtractOne() {
return subtract(f.ONE);
}
public abstract FieldElement negate();
public FieldElement divide(FieldElement val) {
return multiply(val.invert());
}
public abstract FieldElement multiply(FieldElement val);
public abstract FieldElement square();
public abstract FieldElement squareAndDouble();
public abstract FieldElement invert();
public abstract FieldElement pow22523();
public abstract FieldElement cmov(FieldElement val, final int b);
// Note: concrete subclasses must implement hashCode() and equals()
}

@ -0,0 +1,27 @@
package org.xbib.io.sshd.eddsa.math;
/**
*
*/
public interface ScalarOps {
/**
* Reduce the given scalar mod $l$.
* From the Ed25519 paper:
* Here we interpret $2b$-bit strings in little-endian form as integers in
* $\{0, 1,..., 2^{(2b)}-1\}$.
*
* @param s the scalar to reduce
* @return $s \bmod l$
*/
byte[] reduce(byte[] s);
/**
* $r = (a * b + c) \bmod l$
*
* @param a a scalar
* @param b a scalar
* @param c a scalar
* @return $(a*b + c) \bmod l$
*/
byte[] multiplyAndAdd(byte[] a, byte[] b, byte[] c);
}

@ -0,0 +1,118 @@
package org.xbib.io.sshd.eddsa.math.bigint;
import org.xbib.io.sshd.eddsa.math.Field;
import org.xbib.io.sshd.eddsa.math.FieldElement;
import java.io.Serializable;
import java.math.BigInteger;
/**
* A particular element of the field \Z/(2^255-19).
*/
public class BigIntegerFieldElement extends FieldElement implements Serializable {
private static final long serialVersionUID = 4890398908392808L;
/**
* Variable is package private for encoding.
*/
final BigInteger bi;
public BigIntegerFieldElement(Field f, BigInteger bi) {
super(f);
this.bi = bi;
}
public boolean isNonZero() {
return !bi.equals(BigInteger.ZERO);
}
public FieldElement add(FieldElement val) {
return new BigIntegerFieldElement(f, bi.add(((BigIntegerFieldElement) val).bi)).mod(f.getQ());
}
@Override
public FieldElement addOne() {
return new BigIntegerFieldElement(f, bi.add(BigInteger.ONE)).mod(f.getQ());
}
public FieldElement subtract(FieldElement val) {
return new BigIntegerFieldElement(f, bi.subtract(((BigIntegerFieldElement) val).bi)).mod(f.getQ());
}
@Override
public FieldElement subtractOne() {
return new BigIntegerFieldElement(f, bi.subtract(BigInteger.ONE)).mod(f.getQ());
}
public FieldElement negate() {
return f.getQ().subtract(this);
}
@Override
public FieldElement divide(FieldElement val) {
return divide(((BigIntegerFieldElement) val).bi);
}
public FieldElement divide(BigInteger val) {
return new BigIntegerFieldElement(f, bi.divide(val)).mod(f.getQ());
}
public FieldElement multiply(FieldElement val) {
return new BigIntegerFieldElement(f, bi.multiply(((BigIntegerFieldElement) val).bi)).mod(f.getQ());
}
public FieldElement square() {
return multiply(this);
}
public FieldElement squareAndDouble() {
FieldElement sq = square();
return sq.add(sq);
}
public FieldElement invert() {
// Euler's theorem
//return modPow(f.getQm2(), f.getQ());
return new BigIntegerFieldElement(f, bi.modInverse(((BigIntegerFieldElement) f.getQ()).bi));
}
public FieldElement mod(FieldElement m) {
return new BigIntegerFieldElement(f, bi.mod(((BigIntegerFieldElement) m).bi));
}
public FieldElement modPow(FieldElement e, FieldElement m) {
return new BigIntegerFieldElement(f, bi.modPow(((BigIntegerFieldElement) e).bi, ((BigIntegerFieldElement) m).bi));
}
public FieldElement pow(FieldElement e) {
return modPow(e, f.getQ());
}
public FieldElement pow22523() {
return pow(f.getQm5d8());
}
@Override
public FieldElement cmov(FieldElement val, int b) {
// Not constant-time, but it doesn't really matter because none of the underlying BigInteger operations
// are either, so there's not much point in trying hard here ...
return b == 0 ? this : val;
}
@Override
public int hashCode() {
return bi.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof BigIntegerFieldElement))
return false;
BigIntegerFieldElement fe = (BigIntegerFieldElement) obj;
return bi.equals(fe.bi);
}
@Override
public String toString() {
return "[BigIntegerFieldElement val=" + bi + "]";
}
}

@ -0,0 +1,95 @@
package org.xbib.io.sshd.eddsa.math.bigint;
import org.xbib.io.sshd.eddsa.math.Encoding;
import org.xbib.io.sshd.eddsa.math.Field;
import org.xbib.io.sshd.eddsa.math.FieldElement;
import java.io.Serializable;
import java.math.BigInteger;
/**
*
*/
public class BigIntegerLittleEndianEncoding extends Encoding implements Serializable {
private static final long serialVersionUID = 3984579843759837L;
/**
* Mask where only the first b-1 bits are set.
*/
private BigInteger mask;
@Override
public synchronized void setField(Field f) {
super.setField(f);
mask = BigInteger.ONE.shiftLeft(f.getb() - 1).subtract(BigInteger.ONE);
}
public byte[] encode(FieldElement x) {
return encode(((BigIntegerFieldElement) x).bi.and(mask));
}
/**
* Convert $x$ to little endian.
* Constant time.
*
* @param x the BigInteger value to encode
* @return array of length $b/8$
* @throws IllegalStateException if field not set
*/
public byte[] encode(BigInteger x) {
if (f == null)
throw new IllegalStateException("field not set");
byte[] in = x.toByteArray();
byte[] out = new byte[f.getb() / 8];
for (int i = 0; i < in.length; i++) {
out[i] = in[in.length - 1 - i];
}
for (int i = in.length; i < out.length; i++) {
out[i] = 0;
}
return out;
}
/**
* Decode a FieldElement from its $(b-1)$-bit encoding.
* The highest bit is masked out.
*
* @param in the $(b-1)$-bit encoding of a FieldElement.
* @return the FieldElement represented by 'val'.
* @throws IllegalStateException if field not set
* @throws IllegalArgumentException if encoding is invalid
*/
public FieldElement decode(byte[] in) {
if (f == null)
throw new IllegalStateException("field not set");
if (in.length != f.getb() / 8)
throw new IllegalArgumentException("Not a valid encoding");
return new BigIntegerFieldElement(f, toBigInteger(in).and(mask));
}
/**
* Convert in to big endian
*
* @param in the $(b-1)$-bit encoding of a FieldElement.
* @return the decoded value as a BigInteger
*/
public BigInteger toBigInteger(byte[] in) {
byte[] out = new byte[in.length];
for (int i = 0; i < in.length; i++) {
out[i] = in[in.length - 1 - i];
}
return new BigInteger(1, out);
}
/**
* From the Ed25519 paper:<br>
* $x$ is negative if the $(b-1)$-bit encoding of $x$ is lexicographically larger
* than the $(b-1)$-bit encoding of $-x$. If $q$ is an odd prime and the encoding
* is the little-endian representation of $\{0, 1,\dots, q-1\}$ then the negative
* elements of $F_q$ are $\{1, 3, 5,\dots, q-2\}$.
*
* @return true if negative
*/
public boolean isNegative(FieldElement x) {
return ((BigIntegerFieldElement) x).bi.testBit(0);
}
}

@ -0,0 +1,36 @@
/**
* EdDSA-Java by str4d
* <p>
* To the extent possible under law, the person who associated CC0 with
* EdDSA-Java has waived all copyright and related or neighboring rights
* to EdDSA-Java.
* <p>
* You should have received a copy of the CC0 legalcode along with this
* work. If not, see <https://creativecommons.org/publicdomain/zero/1.0/>.
*/
package org.xbib.io.sshd.eddsa.math.bigint;
import org.xbib.io.sshd.eddsa.math.Field;
import org.xbib.io.sshd.eddsa.math.ScalarOps;
import java.math.BigInteger;
public class BigIntegerScalarOps implements ScalarOps {
private final BigInteger l;
private final BigIntegerLittleEndianEncoding enc;
public BigIntegerScalarOps(Field f, BigInteger l) {
this.l = l;
enc = new BigIntegerLittleEndianEncoding();
enc.setField(f);
}
public byte[] reduce(byte[] s) {
return enc.encode(enc.toBigInteger(s).mod(l));
}
public byte[] multiplyAndAdd(byte[] a, byte[] b, byte[] c) {
return enc.encode(enc.toBigInteger(a).multiply(enc.toBigInteger(b)).add(enc.toBigInteger(c)).mod(l));
}
}

@ -0,0 +1,4 @@
/**
* Low-level, non-optimized implementation using BigIntegers for any curve.
*/
package org.xbib.io.sshd.eddsa.math.bigint;

@ -0,0 +1,284 @@
package org.xbib.io.sshd.eddsa.math.ed25519;
import org.xbib.io.sshd.eddsa.math.Encoding;
import org.xbib.io.sshd.eddsa.math.FieldElement;
/**
* Helper class for encoding/decoding from/to the 32 byte representation.
* Reviewed/commented by Bloody Rookie (nemproject@gmx.de)
*/
public class Ed25519LittleEndianEncoding extends Encoding {
static int load_3(byte[] in, int offset) {
int result = in[offset++] & 0xff;
result |= (in[offset++] & 0xff) << 8;
result |= (in[offset] & 0xff) << 16;
return result;
}
static long load_4(byte[] in, int offset) {
int result = in[offset++] & 0xff;
result |= (in[offset++] & 0xff) << 8;
result |= (in[offset++] & 0xff) << 16;
result |= in[offset] << 24;
return ((long) result) & 0xffffffffL;
}
/**
* Encodes a given field element in its 32 byte representation. This is done in two steps:
* <ol>
* <li>Reduce the value of the field element modulo $p$.
* <li>Convert the field element to the 32 byte representation.
* </ol><p>
* The idea for the modulo $p$ reduction algorithm is as follows:
* </p>
* <h2>Assumption:</h2>
* <ul>
* <li>$p = 2^{255} - 19$
* <li>$h = h_0 + 2^{25} * h_1 + 2^{(26+25)} * h_2 + \dots + 2^{230} * h_9$ where $0 \le |h_i| \lt 2^{27}$ for all $i=0,\dots,9$.
* <li>$h \cong r \mod p$, i.e. $h = r + q * p$ for some suitable $0 \le r \lt p$ and an integer $q$.
* </ul><p>
* Then $q = [2^{-255} * (h + 19 * 2^{-25} * h_9 + 1/2)]$ where $[x] = floor(x)$.
* </p>
* <h2>Proof:</h2>
* <p>
* We begin with some very raw estimation for the bounds of some expressions:
* <p>
* $$
* \begin{equation}
* |h| \lt 2^{230} * 2^{30} = 2^{260} \Rightarrow |r + q * p| \lt 2^{260} \Rightarrow |q| \lt 2^{10}. \\
* \Rightarrow -1/4 \le a := 19^2 * 2^{-255} * q \lt 1/4. \\
* |h - 2^{230} * h_9| = |h_0 + \dots + 2^{204} * h_8| \lt 2^{204} * 2^{30} = 2^{234}. \\
* \Rightarrow -1/4 \le b := 19 * 2^{-255} * (h - 2^{230} * h_9) \lt 1/4
* \end{equation}
* $$
* <p>
* Therefore $0 \lt 1/2 - a - b \lt 1$.
* <p>
* Set $x := r + 19 * 2^{-255} * r + 1/2 - a - b$. Then:
* <p>
* $$
* 0 \le x \lt 255 - 20 + 19 + 1 = 2^{255} \\
* \Rightarrow 0 \le 2^{-255} * x \lt 1.
* $$
* <p>
* Since $q$ is an integer we have
* <p>
* $$
* [q + 2^{-255} * x] = q \quad (1)
* $$
* <p>
* Have a closer look at $x$:
* <p>
* $$
* \begin{align}
* x &amp;= h - q * (2^{255} - 19) + 19 * 2^{-255} * (h - q * (2^{255} - 19)) + 1/2 - 19^2 * 2^{-255} * q - 19 * 2^{-255} * (h - 2^{230} * h_9) \\
* &amp;= h - q * 2^{255} + 19 * q + 19 * 2^{-255} * h - 19 * q + 19^2 * 2^{-255} * q + 1/2 - 19^2 * 2^{-255} * q - 19 * 2^{-255} * h + 19 * 2^{-25} * h_9 \\
* &amp;= h + 19 * 2^{-25} * h_9 + 1/2 - q^{255}.
* \end{align}
* $$
* <p>
* Inserting the expression for $x$ into $(1)$ we get the desired expression for $q$.
*/
public byte[] encode(FieldElement x) {
int[] h = ((Ed25519FieldElement) x).t;
int h0 = h[0];
int h1 = h[1];
int h2 = h[2];
int h3 = h[3];
int h4 = h[4];
int h5 = h[5];
int h6 = h[6];
int h7 = h[7];
int h8 = h[8];
int h9 = h[9];
int q;
int carry0;
int carry1;
int carry2;
int carry3;
int carry4;
int carry5;
int carry6;
int carry7;
int carry8;
int carry9;
// Step 1:
// Calculate q
q = (19 * h9 + (1 << 24)) >> 25;
q = (h0 + q) >> 26;
q = (h1 + q) >> 25;
q = (h2 + q) >> 26;
q = (h3 + q) >> 25;
q = (h4 + q) >> 26;
q = (h5 + q) >> 25;
q = (h6 + q) >> 26;
q = (h7 + q) >> 25;
q = (h8 + q) >> 26;
q = (h9 + q) >> 25;
// r = h - q * p = h - 2^255 * q + 19 * q
// First add 19 * q then discard the bit 255
h0 += 19 * q;
carry0 = h0 >> 26;
h1 += carry0;
h0 -= carry0 << 26;
carry1 = h1 >> 25;
h2 += carry1;
h1 -= carry1 << 25;
carry2 = h2 >> 26;
h3 += carry2;
h2 -= carry2 << 26;
carry3 = h3 >> 25;
h4 += carry3;
h3 -= carry3 << 25;
carry4 = h4 >> 26;
h5 += carry4;
h4 -= carry4 << 26;
carry5 = h5 >> 25;
h6 += carry5;
h5 -= carry5 << 25;
carry6 = h6 >> 26;
h7 += carry6;
h6 -= carry6 << 26;
carry7 = h7 >> 25;
h8 += carry7;
h7 -= carry7 << 25;
carry8 = h8 >> 26;
h9 += carry8;
h8 -= carry8 << 26;
carry9 = h9 >> 25;
h9 -= carry9 << 25;
// Step 2 (straight forward conversion):
byte[] s = new byte[32];
s[0] = (byte) h0;
s[1] = (byte) (h0 >> 8);
s[2] = (byte) (h0 >> 16);
s[3] = (byte) ((h0 >> 24) | (h1 << 2));
s[4] = (byte) (h1 >> 6);
s[5] = (byte) (h1 >> 14);
s[6] = (byte) ((h1 >> 22) | (h2 << 3));
s[7] = (byte) (h2 >> 5);
s[8] = (byte) (h2 >> 13);
s[9] = (byte) ((h2 >> 21) | (h3 << 5));
s[10] = (byte) (h3 >> 3);
s[11] = (byte) (h3 >> 11);
s[12] = (byte) ((h3 >> 19) | (h4 << 6));
s[13] = (byte) (h4 >> 2);
s[14] = (byte) (h4 >> 10);
s[15] = (byte) (h4 >> 18);
s[16] = (byte) h5;
s[17] = (byte) (h5 >> 8);
s[18] = (byte) (h5 >> 16);
s[19] = (byte) ((h5 >> 24) | (h6 << 1));
s[20] = (byte) (h6 >> 7);
s[21] = (byte) (h6 >> 15);
s[22] = (byte) ((h6 >> 23) | (h7 << 3));
s[23] = (byte) (h7 >> 5);
s[24] = (byte) (h7 >> 13);
s[25] = (byte) ((h7 >> 21) | (h8 << 4));
s[26] = (byte) (h8 >> 4);
s[27] = (byte) (h8 >> 12);
s[28] = (byte) ((h8 >> 20) | (h9 << 6));
s[29] = (byte) (h9 >> 2);
s[30] = (byte) (h9 >> 10);
s[31] = (byte) (h9 >> 18);
return s;
}
/**
* Decodes a given field element in its 10 byte $2^{25.5}$ representation.
*
* @param in The 32 byte representation.
* @return The field element in its $2^{25.5}$ bit representation.
*/
public FieldElement decode(byte[] in) {
long h0 = load_4(in, 0);
long h1 = load_3(in, 4) << 6;
long h2 = load_3(in, 7) << 5;
long h3 = load_3(in, 10) << 3;
long h4 = load_3(in, 13) << 2;
long h5 = load_4(in, 16);
long h6 = load_3(in, 20) << 7;
long h7 = load_3(in, 23) << 5;
long h8 = load_3(in, 26) << 4;
long h9 = (load_3(in, 29) & 0x7FFFFF) << 2;
long carry0;
long carry1;
long carry2;
long carry3;
long carry4;
long carry5;
long carry6;
long carry7;
long carry8;
long carry9;
// Remember: 2^255 congruent 19 modulo p
carry9 = (h9 + (long) (1 << 24)) >> 25;
h0 += carry9 * 19;
h9 -= carry9 << 25;
carry1 = (h1 + (long) (1 << 24)) >> 25;
h2 += carry1;
h1 -= carry1 << 25;
carry3 = (h3 + (long) (1 << 24)) >> 25;
h4 += carry3;
h3 -= carry3 << 25;
carry5 = (h5 + (long) (1 << 24)) >> 25;
h6 += carry5;
h5 -= carry5 << 25;
carry7 = (h7 + (long) (1 << 24)) >> 25;
h8 += carry7;
h7 -= carry7 << 25;
carry0 = (h0 + (long) (1 << 25)) >> 26;
h1 += carry0;
h0 -= carry0 << 26;
carry2 = (h2 + (long) (1 << 25)) >> 26;
h3 += carry2;
h2 -= carry2 << 26;
carry4 = (h4 + (long) (1 << 25)) >> 26;
h5 += carry4;
h4 -= carry4 << 26;
carry6 = (h6 + (long) (1 << 25)) >> 26;
h7 += carry6;
h6 -= carry6 << 26;
carry8 = (h8 + (long) (1 << 25)) >> 26;
h9 += carry8;
h8 -= carry8 << 26;
int[] h = new int[10];
h[0] = (int) h0;
h[1] = (int) h1;
h[2] = (int) h2;
h[3] = (int) h3;
h[4] = (int) h4;
h[5] = (int) h5;
h[6] = (int) h6;
h[7] = (int) h7;
h[8] = (int) h8;
h[9] = (int) h9;
return new Ed25519FieldElement(f, h);
}
/**
* Is the FieldElement negative in this encoding?
* <p>
* Return true if $x$ is in $\{1,3,5,\dots,q-2\}$<br>
* Return false if $x$ is in $\{0,2,4,\dots,q-1\}$
* <p>
* Preconditions:
* </p><ul>
* <li>$|x|$ bounded by $1.1*2^{26},1.1*2^{25},1.1*2^{26},1.1*2^{25}$, etc.
* </ul>
*
* @return true if $x$ is in $\{1,3,5,\dots,q-2\}$, false otherwise.
*/
public boolean isNegative(FieldElement x) {
byte[] s = encode(x);
return (s[0] & 1) != 0;
}
}

@ -0,0 +1,913 @@
package org.xbib.io.sshd.eddsa.math.ed25519;
import org.xbib.io.sshd.eddsa.math.ScalarOps;
import static org.xbib.io.sshd.eddsa.math.ed25519.Ed25519LittleEndianEncoding.load_3;
import static org.xbib.io.sshd.eddsa.math.ed25519.Ed25519LittleEndianEncoding.load_4;
/**
* Class for reducing a huge integer modulo the group order q and
* doing a combined multiply plus add plus reduce operation.
* <p>
* $q = 2^{252} + 27742317777372353535851937790883648493$.
* <p>
* Reviewed/commented by Bloody Rookie (nemproject@gmx.de)
*/
public class Ed25519ScalarOps implements ScalarOps {
/**
* Reduction modulo the group order $q$.
* <p>
* Input:
* $s[0]+256*s[1]+\dots+256^{63}*s[63] = s$
* <p>
* Output:
* $s[0]+256*s[1]+\dots+256^{31}*s[31] = s \bmod q$
* where $q = 2^{252} + 27742317777372353535851937790883648493$.
*/
public byte[] reduce(byte[] s) {
// s0,..., s22 have 21 bits, s23 has 29 bits
long s0 = 0x1FFFFF & load_3(s, 0);
long s1 = 0x1FFFFF & (load_4(s, 2) >> 5);
long s2 = 0x1FFFFF & (load_3(s, 5) >> 2);
long s3 = 0x1FFFFF & (load_4(s, 7) >> 7);
long s4 = 0x1FFFFF & (load_4(s, 10) >> 4);
long s5 = 0x1FFFFF & (load_3(s, 13) >> 1);
long s6 = 0x1FFFFF & (load_4(s, 15) >> 6);
long s7 = 0x1FFFFF & (load_3(s, 18) >> 3);
long s8 = 0x1FFFFF & load_3(s, 21);
long s9 = 0x1FFFFF & (load_4(s, 23) >> 5);
long s10 = 0x1FFFFF & (load_3(s, 26) >> 2);
long s11 = 0x1FFFFF & (load_4(s, 28) >> 7);
long s12 = 0x1FFFFF & (load_4(s, 31) >> 4);
long s13 = 0x1FFFFF & (load_3(s, 34) >> 1);
long s14 = 0x1FFFFF & (load_4(s, 36) >> 6);
long s15 = 0x1FFFFF & (load_3(s, 39) >> 3);
long s16 = 0x1FFFFF & load_3(s, 42);
long s17 = 0x1FFFFF & (load_4(s, 44) >> 5);
long s18 = 0x1FFFFF & (load_3(s, 47) >> 2);
long s19 = 0x1FFFFF & (load_4(s, 49) >> 7);
long s20 = 0x1FFFFF & (load_4(s, 52) >> 4);
long s21 = 0x1FFFFF & (load_3(s, 55) >> 1);
long s22 = 0x1FFFFF & (load_4(s, 57) >> 6);
long s23 = (load_4(s, 60) >> 3);
long carry0;
long carry1;
long carry2;
long carry3;
long carry4;
long carry5;
long carry6;
long carry7;
long carry8;
long carry9;
long carry10;
long carry11;
long carry12;
long carry13;
long carry14;
long carry15;
long carry16;
/**
* Lots of magic numbers :)
* To understand what's going on below, note that
*
* (1) q = 2^252 + q0 where q0 = 27742317777372353535851937790883648493.
* (2) s11 is the coefficient of 2^(11*21), s23 is the coefficient of 2^(^23*21) and 2^252 = 2^((23-11) * 21)).
* (3) 2^252 congruent -q0 modulo q.
* (4) -q0 = 666643 * 2^0 + 470296 * 2^21 + 654183 * 2^(2*21) - 997805 * 2^(3*21) + 136657 * 2^(4*21) - 683901 * 2^(5*21)
*
* Thus
* s23 * 2^(23*11) = s23 * 2^(12*21) * 2^(11*21) = s3 * 2^252 * 2^(11*21) congruent
* s23 * (666643 * 2^0 + 470296 * 2^21 + 654183 * 2^(2*21) - 997805 * 2^(3*21) + 136657 * 2^(4*21) - 683901 * 2^(5*21)) * 2^(11*21) modulo q =
* s23 * (666643 * 2^(11*21) + 470296 * 2^(12*21) + 654183 * 2^(13*21) - 997805 * 2^(14*21) + 136657 * 2^(15*21) - 683901 * 2^(16*21)).
*
* The same procedure is then applied for s22,...,s18.
*/
s11 += s23 * 666643;
s12 += s23 * 470296;
s13 += s23 * 654183;
s14 -= s23 * 997805;
s15 += s23 * 136657;
s16 -= s23 * 683901;
// not used again
//s23 = 0;
s10 += s22 * 666643;
s11 += s22 * 470296;
s12 += s22 * 654183;
s13 -= s22 * 997805;
s14 += s22 * 136657;
s15 -= s22 * 683901;
// not used again
//s22 = 0;
s9 += s21 * 666643;
s10 += s21 * 470296;
s11 += s21 * 654183;
s12 -= s21 * 997805;
s13 += s21 * 136657;
s14 -= s21 * 683901;
// not used again
//s21 = 0;
s8 += s20 * 666643;
s9 += s20 * 470296;
s10 += s20 * 654183;
s11 -= s20 * 997805;
s12 += s20 * 136657;
s13 -= s20 * 683901;
// not used again
//s20 = 0;
s7 += s19 * 666643;
s8 += s19 * 470296;
s9 += s19 * 654183;
s10 -= s19 * 997805;
s11 += s19 * 136657;
s12 -= s19 * 683901;
// not used again
//s19 = 0;
s6 += s18 * 666643;
s7 += s18 * 470296;
s8 += s18 * 654183;
s9 -= s18 * 997805;
s10 += s18 * 136657;
s11 -= s18 * 683901;
// not used again
//s18 = 0;
/**
* Time to reduce the coefficient in order not to get an overflow.
*/
carry6 = (s6 + (1 << 20)) >> 21;
s7 += carry6;
s6 -= carry6 << 21;
carry8 = (s8 + (1 << 20)) >> 21;
s9 += carry8;
s8 -= carry8 << 21;
carry10 = (s10 + (1 << 20)) >> 21;
s11 += carry10;
s10 -= carry10 << 21;
carry12 = (s12 + (1 << 20)) >> 21;
s13 += carry12;
s12 -= carry12 << 21;
carry14 = (s14 + (1 << 20)) >> 21;
s15 += carry14;
s14 -= carry14 << 21;
carry16 = (s16 + (1 << 20)) >> 21;
s17 += carry16;
s16 -= carry16 << 21;
carry7 = (s7 + (1 << 20)) >> 21;
s8 += carry7;
s7 -= carry7 << 21;
carry9 = (s9 + (1 << 20)) >> 21;
s10 += carry9;
s9 -= carry9 << 21;
carry11 = (s11 + (1 << 20)) >> 21;
s12 += carry11;
s11 -= carry11 << 21;
carry13 = (s13 + (1 << 20)) >> 21;
s14 += carry13;
s13 -= carry13 << 21;
carry15 = (s15 + (1 << 20)) >> 21;
s16 += carry15;
s15 -= carry15 << 21;
/**
* Continue with above procedure.
*/
s5 += s17 * 666643;
s6 += s17 * 470296;
s7 += s17 * 654183;
s8 -= s17 * 997805;
s9 += s17 * 136657;
s10 -= s17 * 683901;
// not used again
//s17 = 0;
s4 += s16 * 666643;
s5 += s16 * 470296;
s6 += s16 * 654183;
s7 -= s16 * 997805;
s8 += s16 * 136657;
s9 -= s16 * 683901;
// not used again
//s16 = 0;
s3 += s15 * 666643;
s4 += s15 * 470296;
s5 += s15 * 654183;
s6 -= s15 * 997805;
s7 += s15 * 136657;
s8 -= s15 * 683901;
// not used again
//s15 = 0;
s2 += s14 * 666643;
s3 += s14 * 470296;
s4 += s14 * 654183;
s5 -= s14 * 997805;
s6 += s14 * 136657;
s7 -= s14 * 683901;
// not used again
//s14 = 0;
s1 += s13 * 666643;
s2 += s13 * 470296;
s3 += s13 * 654183;
s4 -= s13 * 997805;
s5 += s13 * 136657;
s6 -= s13 * 683901;
// not used again
//s13 = 0;
s0 += s12 * 666643;
s1 += s12 * 470296;
s2 += s12 * 654183;
s3 -= s12 * 997805;
s4 += s12 * 136657;
s5 -= s12 * 683901;
// set below
//s12 = 0;
/**
* Reduce coefficients again.
*/
carry0 = (s0 + (1 << 20)) >> 21;
s1 += carry0;
s0 -= carry0 << 21;
carry2 = (s2 + (1 << 20)) >> 21;
s3 += carry2;
s2 -= carry2 << 21;
carry4 = (s4 + (1 << 20)) >> 21;
s5 += carry4;
s4 -= carry4 << 21;
carry6 = (s6 + (1 << 20)) >> 21;
s7 += carry6;
s6 -= carry6 << 21;
carry8 = (s8 + (1 << 20)) >> 21;
s9 += carry8;
s8 -= carry8 << 21;
carry10 = (s10 + (1 << 20)) >> 21;
s11 += carry10;
s10 -= carry10 << 21;
carry1 = (s1 + (1 << 20)) >> 21;
s2 += carry1;
s1 -= carry1 << 21;
carry3 = (s3 + (1 << 20)) >> 21;
s4 += carry3;
s3 -= carry3 << 21;
carry5 = (s5 + (1 << 20)) >> 21;
s6 += carry5;
s5 -= carry5 << 21;
carry7 = (s7 + (1 << 20)) >> 21;
s8 += carry7;
s7 -= carry7 << 21;
carry9 = (s9 + (1 << 20)) >> 21;
s10 += carry9;
s9 -= carry9 << 21;
//carry11 = (s11 + (1<<20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
carry11 = (s11 + (1 << 20)) >> 21;
s12 = carry11;
s11 -= carry11 << 21;
s0 += s12 * 666643;
s1 += s12 * 470296;
s2 += s12 * 654183;
s3 -= s12 * 997805;
s4 += s12 * 136657;
s5 -= s12 * 683901;
// set below
//s12 = 0;
carry0 = s0 >> 21;
s1 += carry0;
s0 -= carry0 << 21;
carry1 = s1 >> 21;
s2 += carry1;
s1 -= carry1 << 21;
carry2 = s2 >> 21;
s3 += carry2;
s2 -= carry2 << 21;
carry3 = s3 >> 21;
s4 += carry3;
s3 -= carry3 << 21;
carry4 = s4 >> 21;
s5 += carry4;
s4 -= carry4 << 21;
carry5 = s5 >> 21;
s6 += carry5;
s5 -= carry5 << 21;
carry6 = s6 >> 21;
s7 += carry6;
s6 -= carry6 << 21;
carry7 = s7 >> 21;
s8 += carry7;
s7 -= carry7 << 21;
carry8 = s8 >> 21;
s9 += carry8;
s8 -= carry8 << 21;
carry9 = s9 >> 21;
s10 += carry9;
s9 -= carry9 << 21;
carry10 = s10 >> 21;
s11 += carry10;
s10 -= carry10 << 21;
//carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21;
carry11 = s11 >> 21;
s12 = carry11;
s11 -= carry11 << 21;
// TODO-CR BR: Is it really needed to do it TWO times? (it doesn't hurt, just a question).
s0 += s12 * 666643;
s1 += s12 * 470296;
s2 += s12 * 654183;
s3 -= s12 * 997805;
s4 += s12 * 136657;
s5 -= s12 * 683901;
// not used again
//s12 = 0;
carry0 = s0 >> 21;
s1 += carry0;
s0 -= carry0 << 21;
carry1 = s1 >> 21;
s2 += carry1;
s1 -= carry1 << 21;
carry2 = s2 >> 21;
s3 += carry2;
s2 -= carry2 << 21;
carry3 = s3 >> 21;
s4 += carry3;
s3 -= carry3 << 21;
carry4 = s4 >> 21;
s5 += carry4;
s4 -= carry4 << 21;
carry5 = s5 >> 21;
s6 += carry5;
s5 -= carry5 << 21;
carry6 = s6 >> 21;
s7 += carry6;
s6 -= carry6 << 21;
carry7 = s7 >> 21;
s8 += carry7;
s7 -= carry7 << 21;
carry8 = s8 >> 21;
s9 += carry8;
s8 -= carry8 << 21;
carry9 = s9 >> 21;
s10 += carry9;
s9 -= carry9 << 21;
carry10 = s10 >> 21;
s11 += carry10;
s10 -= carry10 << 21;
// s0, ..., s11 got 21 bits each.
byte[] result = new byte[32];
result[0] = (byte) s0;
result[1] = (byte) (s0 >> 8);
result[2] = (byte) ((s0 >> 16) | (s1 << 5));
result[3] = (byte) (s1 >> 3);
result[4] = (byte) (s1 >> 11);
result[5] = (byte) ((s1 >> 19) | (s2 << 2));
result[6] = (byte) (s2 >> 6);
result[7] = (byte) ((s2 >> 14) | (s3 << 7));
result[8] = (byte) (s3 >> 1);
result[9] = (byte) (s3 >> 9);
result[10] = (byte) ((s3 >> 17) | (s4 << 4));
result[11] = (byte) (s4 >> 4);
result[12] = (byte) (s4 >> 12);
result[13] = (byte) ((s4 >> 20) | (s5 << 1));
result[14] = (byte) (s5 >> 7);
result[15] = (byte) ((s5 >> 15) | (s6 << 6));
result[16] = (byte) (s6 >> 2);
result[17] = (byte) (s6 >> 10);
result[18] = (byte) ((s6 >> 18) | (s7 << 3));
result[19] = (byte) (s7 >> 5);
result[20] = (byte) (s7 >> 13);
result[21] = (byte) s8;
result[22] = (byte) (s8 >> 8);
result[23] = (byte) ((s8 >> 16) | (s9 << 5));
result[24] = (byte) (s9 >> 3);
result[25] = (byte) (s9 >> 11);
result[26] = (byte) ((s9 >> 19) | (s10 << 2));
result[27] = (byte) (s10 >> 6);
result[28] = (byte) ((s10 >> 14) | (s11 << 7));
result[29] = (byte) (s11 >> 1);
result[30] = (byte) (s11 >> 9);
result[31] = (byte) (s11 >> 17);
return result;
}
/**
* $(ab+c) \bmod q$
* <p>
* Input:
* </p><ul>
* <li>$a[0]+256*a[1]+\dots+256^{31}*a[31] = a$
* <li>$b[0]+256*b[1]+\dots+256^{31}*b[31] = b$
* <li>$c[0]+256*c[1]+\dots+256^{31}*c[31] = c$
* </ul><p>
* Output:
* $result[0]+256*result[1]+\dots+256^{31}*result[31] = (ab+c) \bmod q$
* where $q = 2^{252} + 27742317777372353535851937790883648493$.
* <p>
* See the comments in {@link #reduce(byte[])} for an explanation of the algorithm.
*/
public byte[] multiplyAndAdd(byte[] a, byte[] b, byte[] c) {
long a0 = 0x1FFFFF & load_3(a, 0);
long a1 = 0x1FFFFF & (load_4(a, 2) >> 5);
long a2 = 0x1FFFFF & (load_3(a, 5) >> 2);
long a3 = 0x1FFFFF & (load_4(a, 7) >> 7);
long a4 = 0x1FFFFF & (load_4(a, 10) >> 4);
long a5 = 0x1FFFFF & (load_3(a, 13) >> 1);
long a6 = 0x1FFFFF & (load_4(a, 15) >> 6);
long a7 = 0x1FFFFF & (load_3(a, 18) >> 3);
long a8 = 0x1FFFFF & load_3(a, 21);
long a9 = 0x1FFFFF & (load_4(a, 23) >> 5);
long a10 = 0x1FFFFF & (load_3(a, 26) >> 2);
long a11 = (load_4(a, 28) >> 7);
long b0 = 0x1FFFFF & load_3(b, 0);
long b1 = 0x1FFFFF & (load_4(b, 2) >> 5);
long b2 = 0x1FFFFF & (load_3(b, 5) >> 2);
long b3 = 0x1FFFFF & (load_4(b, 7) >> 7);
long b4 = 0x1FFFFF & (load_4(b, 10) >> 4);
long b5 = 0x1FFFFF & (load_3(b, 13) >> 1);
long b6 = 0x1FFFFF & (load_4(b, 15) >> 6);
long b7 = 0x1FFFFF & (load_3(b, 18) >> 3);
long b8 = 0x1FFFFF & load_3(b, 21);
long b9 = 0x1FFFFF & (load_4(b, 23) >> 5);
long b10 = 0x1FFFFF & (load_3(b, 26) >> 2);
long b11 = (load_4(b, 28) >> 7);
long c0 = 0x1FFFFF & load_3(c, 0);
long c1 = 0x1FFFFF & (load_4(c, 2) >> 5);
long c2 = 0x1FFFFF & (load_3(c, 5) >> 2);
long c3 = 0x1FFFFF & (load_4(c, 7) >> 7);
long c4 = 0x1FFFFF & (load_4(c, 10) >> 4);
long c5 = 0x1FFFFF & (load_3(c, 13) >> 1);
long c6 = 0x1FFFFF & (load_4(c, 15) >> 6);
long c7 = 0x1FFFFF & (load_3(c, 18) >> 3);
long c8 = 0x1FFFFF & load_3(c, 21);
long c9 = 0x1FFFFF & (load_4(c, 23) >> 5);
long c10 = 0x1FFFFF & (load_3(c, 26) >> 2);
long c11 = (load_4(c, 28) >> 7);
long s0;
long s1;
long s2;
long s3;
long s4;
long s5;
long s6;
long s7;
long s8;
long s9;
long s10;
long s11;
long s12;
long s13;
long s14;
long s15;
long s16;
long s17;
long s18;
long s19;
long s20;
long s21;
long s22;
long s23;
long carry0;
long carry1;
long carry2;
long carry3;
long carry4;
long carry5;
long carry6;
long carry7;
long carry8;
long carry9;
long carry10;
long carry11;
long carry12;
long carry13;
long carry14;
long carry15;
long carry16;
long carry17;
long carry18;
long carry19;
long carry20;
long carry21;
long carry22;
s0 = c0 + a0 * b0;
s1 = c1 + a0 * b1 + a1 * b0;
s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0;
s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0;
s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0;
s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0;
s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0;
s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0;
s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 + a8 * b0;
s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0;
s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0;
s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0;
s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1;
s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2;
s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 + a11 * b3;
s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4;
s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5;
s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6;
s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7;
s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8;
s20 = a9 * b11 + a10 * b10 + a11 * b9;
s21 = a10 * b11 + a11 * b10;
s22 = a11 * b11;
// set below
//s23 = 0;
carry0 = (s0 + (1 << 20)) >> 21;
s1 += carry0;
s0 -= carry0 << 21;
carry2 = (s2 + (1 << 20)) >> 21;
s3 += carry2;
s2 -= carry2 << 21;
carry4 = (s4 + (1 << 20)) >> 21;
s5 += carry4;
s4 -= carry4 << 21;
carry6 = (s6 + (1 << 20)) >> 21;
s7 += carry6;
s6 -= carry6 << 21;
carry8 = (s8 + (1 << 20)) >> 21;
s9 += carry8;
s8 -= carry8 << 21;
carry10 = (s10 + (1 << 20)) >> 21;
s11 += carry10;
s10 -= carry10 << 21;
carry12 = (s12 + (1 << 20)) >> 21;
s13 += carry12;
s12 -= carry12 << 21;
carry14 = (s14 + (1 << 20)) >> 21;
s15 += carry14;
s14 -= carry14 << 21;
carry16 = (s16 + (1 << 20)) >> 21;
s17 += carry16;
s16 -= carry16 << 21;
carry18 = (s18 + (1 << 20)) >> 21;
s19 += carry18;
s18 -= carry18 << 21;
carry20 = (s20 + (1 << 20)) >> 21;
s21 += carry20;
s20 -= carry20 << 21;
//carry22 = (s22 + (1<<20)) >> 21; s23 += carry22; s22 -= carry22 << 21;
carry22 = (s22 + (1 << 20)) >> 21;
s23 = carry22;
s22 -= carry22 << 21;
carry1 = (s1 + (1 << 20)) >> 21;
s2 += carry1;
s1 -= carry1 << 21;
carry3 = (s3 + (1 << 20)) >> 21;
s4 += carry3;
s3 -= carry3 << 21;
carry5 = (s5 + (1 << 20)) >> 21;
s6 += carry5;
s5 -= carry5 << 21;
carry7 = (s7 + (1 << 20)) >> 21;
s8 += carry7;
s7 -= carry7 << 21;
carry9 = (s9 + (1 << 20)) >> 21;
s10 += carry9;
s9 -= carry9 << 21;
carry11 = (s11 + (1 << 20)) >> 21;
s12 += carry11;
s11 -= carry11 << 21;
carry13 = (s13 + (1 << 20)) >> 21;
s14 += carry13;
s13 -= carry13 << 21;
carry15 = (s15 + (1 << 20)) >> 21;
s16 += carry15;
s15 -= carry15 << 21;
carry17 = (s17 + (1 << 20)) >> 21;
s18 += carry17;
s17 -= carry17 << 21;
carry19 = (s19 + (1 << 20)) >> 21;
s20 += carry19;
s19 -= carry19 << 21;
carry21 = (s21 + (1 << 20)) >> 21;
s22 += carry21;
s21 -= carry21 << 21;
s11 += s23 * 666643;
s12 += s23 * 470296;
s13 += s23 * 654183;
s14 -= s23 * 997805;
s15 += s23 * 136657;
s16 -= s23 * 683901;
// not used again
//s23 = 0;
s10 += s22 * 666643;
s11 += s22 * 470296;
s12 += s22 * 654183;
s13 -= s22 * 997805;
s14 += s22 * 136657;
s15 -= s22 * 683901;
// not used again
//s22 = 0;
s9 += s21 * 666643;
s10 += s21 * 470296;
s11 += s21 * 654183;
s12 -= s21 * 997805;
s13 += s21 * 136657;
s14 -= s21 * 683901;
// not used again
//s21 = 0;
s8 += s20 * 666643;
s9 += s20 * 470296;
s10 += s20 * 654183;
s11 -= s20 * 997805;
s12 += s20 * 136657;
s13 -= s20 * 683901;
// not used again
//s20 = 0;
s7 += s19 * 666643;
s8 += s19 * 470296;
s9 += s19 * 654183;
s10 -= s19 * 997805;
s11 += s19 * 136657;
s12 -= s19 * 683901;
// not used again
//s19 = 0;
s6 += s18 * 666643;
s7 += s18 * 470296;
s8 += s18 * 654183;
s9 -= s18 * 997805;
s10 += s18 * 136657;
s11 -= s18 * 683901;
// not used again
//s18 = 0;
carry6 = (s6 + (1 << 20)) >> 21;
s7 += carry6;
s6 -= carry6 << 21;
carry8 = (s8 + (1 << 20)) >> 21;
s9 += carry8;
s8 -= carry8 << 21;
carry10 = (s10 + (1 << 20)) >> 21;
s11 += carry10;
s10 -= carry10 << 21;
carry12 = (s12 + (1 << 20)) >> 21;
s13 += carry12;
s12 -= carry12 << 21;
carry14 = (s14 + (1 << 20)) >> 21;
s15 += carry14;
s14 -= carry14 << 21;
carry16 = (s16 + (1 << 20)) >> 21;
s17 += carry16;
s16 -= carry16 << 21;
carry7 = (s7 + (1 << 20)) >> 21;
s8 += carry7;
s7 -= carry7 << 21;
carry9 = (s9 + (1 << 20)) >> 21;
s10 += carry9;
s9 -= carry9 << 21;
carry11 = (s11 + (1 << 20)) >> 21;
s12 += carry11;
s11 -= carry11 << 21;
carry13 = (s13 + (1 << 20)) >> 21;
s14 += carry13;
s13 -= carry13 << 21;
carry15 = (s15 + (1 << 20)) >> 21;
s16 += carry15;
s15 -= carry15 << 21;
s5 += s17 * 666643;
s6 += s17 * 470296;
s7 += s17 * 654183;
s8 -= s17 * 997805;
s9 += s17 * 136657;
s10 -= s17 * 683901;
// not used again
//s17 = 0;
s4 += s16 * 666643;
s5 += s16 * 470296;
s6 += s16 * 654183;
s7 -= s16 * 997805;
s8 += s16 * 136657;
s9 -= s16 * 683901;
// not used again
//s16 = 0;
s3 += s15 * 666643;
s4 += s15 * 470296;
s5 += s15 * 654183;
s6 -= s15 * 997805;
s7 += s15 * 136657;
s8 -= s15 * 683901;
// not used again
//s15 = 0;
s2 += s14 * 666643;
s3 += s14 * 470296;
s4 += s14 * 654183;
s5 -= s14 * 997805;
s6 += s14 * 136657;
s7 -= s14 * 683901;
// not used again
//s14 = 0;
s1 += s13 * 666643;
s2 += s13 * 470296;
s3 += s13 * 654183;
s4 -= s13 * 997805;
s5 += s13 * 136657;
s6 -= s13 * 683901;
// not used again
//s13 = 0;
s0 += s12 * 666643;
s1 += s12 * 470296;
s2 += s12 * 654183;
s3 -= s12 * 997805;
s4 += s12 * 136657;
s5 -= s12 * 683901;
// set below
//s12 = 0;
carry0 = (s0 + (1 << 20)) >> 21;
s1 += carry0;
s0 -= carry0 << 21;
carry2 = (s2 + (1 << 20)) >> 21;
s3 += carry2;
s2 -= carry2 << 21;
carry4 = (s4 + (1 << 20)) >> 21;
s5 += carry4;
s4 -= carry4 << 21;
carry6 = (s6 + (1 << 20)) >> 21;
s7 += carry6;
s6 -= carry6 << 21;
carry8 = (s8 + (1 << 20)) >> 21;
s9 += carry8;
s8 -= carry8 << 21;
carry10 = (s10 + (1 << 20)) >> 21;
s11 += carry10;
s10 -= carry10 << 21;
carry1 = (s1 + (1 << 20)) >> 21;
s2 += carry1;
s1 -= carry1 << 21;
carry3 = (s3 + (1 << 20)) >> 21;
s4 += carry3;
s3 -= carry3 << 21;
carry5 = (s5 + (1 << 20)) >> 21;
s6 += carry5;
s5 -= carry5 << 21;
carry7 = (s7 + (1 << 20)) >> 21;
s8 += carry7;
s7 -= carry7 << 21;
carry9 = (s9 + (1 << 20)) >> 21;
s10 += carry9;
s9 -= carry9 << 21;
//carry11 = (s11 + (1<<20)) >> 21; s12 += carry11; s11 -= carry11 << 21;
carry11 = (s11 + (1 << 20)) >> 21;
s12 = carry11;
s11 -= carry11 << 21;
s0 += s12 * 666643;
s1 += s12 * 470296;
s2 += s12 * 654183;
s3 -= s12 * 997805;
s4 += s12 * 136657;
s5 -= s12 * 683901;
// set below
//s12 = 0;
carry0 = s0 >> 21;
s1 += carry0;
s0 -= carry0 << 21;
carry1 = s1 >> 21;
s2 += carry1;
s1 -= carry1 << 21;
carry2 = s2 >> 21;
s3 += carry2;
s2 -= carry2 << 21;
carry3 = s3 >> 21;
s4 += carry3;
s3 -= carry3 << 21;
carry4 = s4 >> 21;
s5 += carry4;
s4 -= carry4 << 21;
carry5 = s5 >> 21;
s6 += carry5;
s5 -= carry5 << 21;
carry6 = s6 >> 21;
s7 += carry6;
s6 -= carry6 << 21;
carry7 = s7 >> 21;
s8 += carry7;
s7 -= carry7 << 21;
carry8 = s8 >> 21;
s9 += carry8;
s8 -= carry8 << 21;
carry9 = s9 >> 21;
s10 += carry9;
s9 -= carry9 << 21;
carry10 = s10 >> 21;
s11 += carry10;
s10 -= carry10 << 21;
//carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21;
carry11 = s11 >> 21;
s12 = carry11;
s11 -= carry11 << 21;
s0 += s12 * 666643;
s1 += s12 * 470296;
s2 += s12 * 654183;
s3 -= s12 * 997805;
s4 += s12 * 136657;
s5 -= s12 * 683901;
// not used again
//s12 = 0;
carry0 = s0 >> 21;
s1 += carry0;
s0 -= carry0 << 21;
carry1 = s1 >> 21;
s2 += carry1;
s1 -= carry1 << 21;
carry2 = s2 >> 21;
s3 += carry2;
s2 -= carry2 << 21;
carry3 = s3 >> 21;
s4 += carry3;
s3 -= carry3 << 21;
carry4 = s4 >> 21;
s5 += carry4;
s4 -= carry4 << 21;
carry5 = s5 >> 21;
s6 += carry5;
s5 -= carry5 << 21;
carry6 = s6 >> 21;
s7 += carry6;
s6 -= carry6 << 21;
carry7 = s7 >> 21;
s8 += carry7;
s7 -= carry7 << 21;
carry8 = s8 >> 21;
s9 += carry8;
s8 -= carry8 << 21;
carry9 = s9 >> 21;
s10 += carry9;
s9 -= carry9 << 21;
carry10 = s10 >> 21;
s11 += carry10;
s10 -= carry10 << 21;
byte[] result = new byte[32];
result[0] = (byte) s0;
result[1] = (byte) (s0 >> 8);
result[2] = (byte) ((s0 >> 16) | (s1 << 5));
result[3] = (byte) (s1 >> 3);
result[4] = (byte) (s1 >> 11);
result[5] = (byte) ((s1 >> 19) | (s2 << 2));
result[6] = (byte) (s2 >> 6);
result[7] = (byte) ((s2 >> 14) | (s3 << 7));
result[8] = (byte) (s3 >> 1);
result[9] = (byte) (s3 >> 9);
result[10] = (byte) ((s3 >> 17) | (s4 << 4));
result[11] = (byte) (s4 >> 4);
result[12] = (byte) (s4 >> 12);
result[13] = (byte) ((s4 >> 20) | (s5 << 1));
result[14] = (byte) (s5 >> 7);
result[15] = (byte) ((s5 >> 15) | (s6 << 6));
result[16] = (byte) (s6 >> 2);
result[17] = (byte) (s6 >> 10);
result[18] = (byte) ((s6 >> 18) | (s7 << 3));
result[19] = (byte) (s7 >> 5);
result[20] = (byte) (s7 >> 13);
result[21] = (byte) s8;
result[22] = (byte) (s8 >> 8);
result[23] = (byte) ((s8 >> 16) | (s9 << 5));
result[24] = (byte) (s9 >> 3);
result[25] = (byte) (s9 >> 11);
result[26] = (byte) ((s9 >> 19) | (s10 << 2));
result[27] = (byte) (s10 >> 6);
result[28] = (byte) ((s10 >> 14) | (s11 << 7));
result[29] = (byte) (s11 >> 1);
result[30] = (byte) (s11 >> 9);
result[31] = (byte) (s11 >> 17);
return result;
}
}

@ -0,0 +1,4 @@
/**
* Low-level, optimized implementation using Radix $2^{51}$ for Curve 25519.
*/
package org.xbib.io.sshd.eddsa.math.ed25519;

@ -0,0 +1,6 @@
/**
* Data structures that definie curves and fields, and the mathematical operaions on them.
* Low-level implementation in bigint for any curve using BigIntegers,
* and in ed25519 for Curve 25519 using Radix $2^{51}$.
*/
package org.xbib.io.sshd.eddsa.math;

@ -0,0 +1,19 @@
package org.xbib.io.sshd.eddsa.spec;
import java.security.spec.AlgorithmParameterSpec;
/**
* Implementation of AlgorithmParameterSpec that holds the name of a named
* EdDSA curve specification.
*/
public class EdDSAGenParameterSpec implements AlgorithmParameterSpec {
private final String name;
public EdDSAGenParameterSpec(String stdName) {
name = stdName;
}
public String getName() {
return name;
}
}

@ -0,0 +1,23 @@
package org.xbib.io.sshd.eddsa.spec;
import org.xbib.io.sshd.eddsa.math.Curve;
import org.xbib.io.sshd.eddsa.math.GroupElement;
import org.xbib.io.sshd.eddsa.math.ScalarOps;
/**
* EdDSA Curve specification that can also be referred to by name.
*/
public class EdDSANamedCurveSpec extends EdDSAParameterSpec {
private final String name;
public EdDSANamedCurveSpec(String name, Curve curve,
String hashAlgo, ScalarOps sc, GroupElement B) {
super(curve, hashAlgo, sc, B);
this.name = name;
}
public String getName() {
return name;
}
}

@ -0,0 +1,56 @@
package org.xbib.io.sshd.eddsa.spec;
import org.xbib.io.sshd.eddsa.Utils;
import org.xbib.io.sshd.eddsa.math.Curve;
import org.xbib.io.sshd.eddsa.math.Field;
import org.xbib.io.sshd.eddsa.math.ed25519.Ed25519LittleEndianEncoding;
import org.xbib.io.sshd.eddsa.math.ed25519.Ed25519ScalarOps;
import java.util.Hashtable;
import java.util.Locale;
/**
* The named EdDSA curves.
*/
public class EdDSANamedCurveTable {
private static final Field ed25519field = new Field(
256, // b
Utils.hexToBytes("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), // q
new Ed25519LittleEndianEncoding());
private static final Curve ed25519curve = new Curve(ed25519field,
Utils.hexToBytes("a3785913ca4deb75abd841414d0a700098e879777940c78c73fe6f2bee6c0352"), // d
ed25519field.fromByteArray(Utils.hexToBytes("b0a00e4a271beec478e42fad0618432fa7d7fb3d99004d2b0bdfc14f8024832b"))); // I
private static final EdDSANamedCurveSpec ed25519 = new EdDSANamedCurveSpec(
"Ed25519",
ed25519curve,
"SHA-512", // H
new Ed25519ScalarOps(), // l
ed25519curve.createPoint( // B
Utils.hexToBytes("5866666666666666666666666666666666666666666666666666666666666666"),
true)); // Precompute tables for B
private static final Hashtable<String, EdDSANamedCurveSpec> curves = new Hashtable<String, EdDSANamedCurveSpec>();
static {
// RFC 8032
defineCurve(ed25519);
}
public static void defineCurve(EdDSANamedCurveSpec curve) {
curves.put(curve.getName().toLowerCase(Locale.ENGLISH), curve);
}
static void defineCurveAlias(String name, String alias) {
EdDSANamedCurveSpec curve = curves.get(name.toLowerCase(Locale.ENGLISH));
if (curve == null) {
throw new IllegalStateException();
}
curves.put(alias.toLowerCase(Locale.ENGLISH), curve);
}
public static EdDSANamedCurveSpec getByName(String name) {
return curves.get(name.toLowerCase(Locale.ENGLISH));
}
}

@ -0,0 +1,84 @@
package org.xbib.io.sshd.eddsa.spec;
import org.xbib.io.sshd.eddsa.math.Curve;
import org.xbib.io.sshd.eddsa.math.GroupElement;
import org.xbib.io.sshd.eddsa.math.ScalarOps;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
//import java.io.Serializable;
/**
* Parameter specification for an EdDSA algorithm.
*/
public class EdDSAParameterSpec implements AlgorithmParameterSpec/*, Serializable*/ {
// private static final long serialVersionUID = 8274987108472012L;
private final Curve curve;
private final String hashAlgo;
private final ScalarOps sc;
private final GroupElement B;
/**
* @param curve the curve
* @param hashAlgo the JCA string for the hash algorithm
* @param sc the parameter L represented as ScalarOps
* @param B the parameter B
* @throws IllegalArgumentException if hash algorithm is unsupported or length is wrong
*/
public EdDSAParameterSpec(Curve curve, String hashAlgo,
ScalarOps sc, GroupElement B) {
try {
MessageDigest hash = MessageDigest.getInstance(hashAlgo);
// EdDSA hash function must produce 2b-bit output
if (curve.getField().getb() / 4 != hash.getDigestLength())
throw new IllegalArgumentException("Hash output is not 2b-bit");
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("Unsupported hash algorithm");
}
this.curve = curve;
this.hashAlgo = hashAlgo;
this.sc = sc;
this.B = B;
}
public Curve getCurve() {
return curve;
}
public String getHashAlgorithm() {
return hashAlgo;
}
public ScalarOps getScalarOps() {
return sc;
}
/**
* @return the base (generator)
*/
public GroupElement getB() {
return B;
}
@Override
public int hashCode() {
return hashAlgo.hashCode() ^
curve.hashCode() ^
B.hashCode();
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof EdDSAParameterSpec))
return false;
EdDSAParameterSpec s = (EdDSAParameterSpec) o;
return hashAlgo.equals(s.getHashAlgorithm()) &&
curve.equals(s.getCurve()) &&
B.equals(s.getB());
}
}

@ -0,0 +1,120 @@
package org.xbib.io.sshd.eddsa.spec;
import org.xbib.io.sshd.eddsa.math.GroupElement;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.KeySpec;
import java.util.Arrays;
/**
*
*/
public class EdDSAPrivateKeySpec implements KeySpec {
private final byte[] seed;
private final byte[] h;
private final byte[] a;
private final GroupElement A;
private final EdDSAParameterSpec spec;
/**
* @param seed the private key
* @param spec the parameter specification for this key
* @throws IllegalArgumentException if seed length is wrong or hash algorithm is unsupported
*/
public EdDSAPrivateKeySpec(byte[] seed, EdDSAParameterSpec spec) {
if (seed.length != spec.getCurve().getField().getb() / 8)
throw new IllegalArgumentException("seed length is wrong");
this.spec = spec;
this.seed = seed;
try {
MessageDigest hash = MessageDigest.getInstance(spec.getHashAlgorithm());
int b = spec.getCurve().getField().getb();
// H(k)
h = hash.digest(seed);
/*a = BigInteger.valueOf(2).pow(b-2);
for (int i=3;i<(b-2);i++) {
a = a.add(BigInteger.valueOf(2).pow(i).multiply(BigInteger.valueOf(Utils.bit(h,i))));
}*/
// Saves ~0.4ms per key when running signing tests.
// TODO: are these bitflips the same for any hash function?
h[0] &= 248;
h[(b / 8) - 1] &= 63;
h[(b / 8) - 1] |= 64;
a = Arrays.copyOfRange(h, 0, b / 8);
A = spec.getB().scalarMultiply(a);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("Unsupported hash algorithm");
}
}
/**
* Initialize directly from the hash.
* getSeed() will return null if this constructor is used.
*
* @param spec the parameter specification for this key
* @param h the private key
* @throws IllegalArgumentException if hash length is wrong
*/
public EdDSAPrivateKeySpec(EdDSAParameterSpec spec, byte[] h) {
if (h.length != spec.getCurve().getField().getb() / 4)
throw new IllegalArgumentException("hash length is wrong");
this.seed = null;
this.h = h;
this.spec = spec;
int b = spec.getCurve().getField().getb();
h[0] &= 248;
h[(b / 8) - 1] &= 63;
h[(b / 8) - 1] |= 64;
a = Arrays.copyOfRange(h, 0, b / 8);
A = spec.getB().scalarMultiply(a);
}
public EdDSAPrivateKeySpec(byte[] seed, byte[] h, byte[] a, GroupElement A, EdDSAParameterSpec spec) {
this.seed = seed;
this.h = h;
this.a = a;
this.A = A;
this.spec = spec;
}
/**
* @return will be null if constructed directly from the private key
*/
public byte[] getSeed() {
return seed;
}
/**
* @return the hash
*/
public byte[] getH() {
return h;
}
/**
* @return the private key
*/
public byte[] geta() {
return a;
}
/**
* @return the public key
*/
public GroupElement getA() {
return A;
}
public EdDSAParameterSpec getParams() {
return spec;
}
}

@ -0,0 +1,49 @@
package org.xbib.io.sshd.eddsa.spec;
import org.xbib.io.sshd.eddsa.math.GroupElement;
import java.security.spec.KeySpec;
/**
*
*/
public class EdDSAPublicKeySpec implements KeySpec {
private final GroupElement A;
private final GroupElement Aneg;
private final EdDSAParameterSpec spec;
/**
* @param pk the public key
* @param spec the parameter specification for this key
* @throws IllegalArgumentException if key length is wrong
*/
public EdDSAPublicKeySpec(byte[] pk, EdDSAParameterSpec spec) {
if (pk.length != spec.getCurve().getField().getb() / 8)
throw new IllegalArgumentException("public-key length is wrong");
this.A = new GroupElement(spec.getCurve(), pk);
// Precompute -A for use in verification.
this.Aneg = A.negate();
Aneg.precompute(false);
this.spec = spec;
}
public EdDSAPublicKeySpec(GroupElement A, EdDSAParameterSpec spec) {
this.A = A;
this.Aneg = A.negate();
Aneg.precompute(false);
this.spec = spec;
}
public GroupElement getA() {
return A;
}
public GroupElement getNegativeA() {
return Aneg;
}
public EdDSAParameterSpec getParams() {
return spec;
}
}

@ -0,0 +1,5 @@
/**
* Specifications for curves and keys, and a table for named curves.
* Contains the following curves: Ed25519
*/
package org.xbib.io.sshd.eddsa.spec;

@ -0,0 +1,54 @@
package org.xbib.io.sshd.eddsa;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
*
*/
public class Ed25519TestVectors {
public static class TestTuple {
public static int numCases;
public int caseNum;
public byte[] seed;
public byte[] pk;
public byte[] message;
public byte[] sig;
public TestTuple(String line) {
caseNum = ++numCases;
String[] x = line.split(":");
seed = Utils.hexToBytes(x[0].substring(0, 64));
pk = Utils.hexToBytes(x[1]);
message = Utils.hexToBytes(x[2]);
sig = Utils.hexToBytes(x[3].substring(0, 128));
}
}
public static Collection<TestTuple> testCases = getTestData("test.data");
public static Collection<TestTuple> getTestData(String fileName) {
List<TestTuple> testCases = new ArrayList<TestTuple>();
BufferedReader file = null;
try {
InputStream is = Ed25519TestVectors.class.getResourceAsStream(fileName);
if (is == null)
throw new IOException("Resource not found: " + fileName);
file = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = file.readLine()) != null) {
testCases.add(new TestTuple(line));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (file != null) try { file.close(); } catch (IOException e) {}
}
return testCases;
}
}

@ -0,0 +1,204 @@
package org.xbib.io.sshd.eddsa;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveTable;
import org.xbib.io.sshd.eddsa.spec.EdDSAParameterSpec;
import org.xbib.io.sshd.eddsa.spec.EdDSAPrivateKeySpec;
import org.xbib.io.sshd.eddsa.spec.EdDSAPublicKeySpec;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
/**
*
*/
public class EdDSAEngineTest {
static final byte[] TEST_SEED = Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
static final byte[] TEST_PK = Utils.hexToBytes("3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29");
static final byte[] TEST_MSG = "This is a secret message".getBytes(Charset.forName("UTF-8"));
static final byte[] TEST_MSG_SIG = Utils.hexToBytes("94825896c7075c31bcb81f06dba2bdcd9dcf16e79288d4b9f87c248215c8468d475f429f3de3b4a2cf67fe17077ae19686020364d6d4fa7a0174bab4a123ba0f");
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void testSign() throws Exception {
EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
//Signature sgr = Signature.getInstance("EdDSA", "I2P");
Signature sgr = new EdDSAEngine(MessageDigest.getInstance(spec.getHashAlgorithm()));
for (Ed25519TestVectors.TestTuple testCase : Ed25519TestVectors.testCases) {
EdDSAPrivateKeySpec privKey = new EdDSAPrivateKeySpec(testCase.seed, spec);
PrivateKey sKey = new EdDSAPrivateKey(privKey);
sgr.initSign(sKey);
sgr.update(testCase.message);
assertThat("Test case " + testCase.caseNum + " failed",
sgr.sign(), is(equalTo(testCase.sig)));
}
}
@Test
public void testVerify() throws Exception {
EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
//Signature sgr = Signature.getInstance("EdDSA", "I2P");
Signature sgr = new EdDSAEngine(MessageDigest.getInstance(spec.getHashAlgorithm()));
for (Ed25519TestVectors.TestTuple testCase : Ed25519TestVectors.testCases) {
EdDSAPublicKeySpec pubKey = new EdDSAPublicKeySpec(testCase.pk, spec);
PublicKey vKey = new EdDSAPublicKey(pubKey);
sgr.initVerify(vKey);
sgr.update(testCase.message);
assertThat("Test case " + testCase.caseNum + " failed",
sgr.verify(testCase.sig), is(true));
}
}
/**
* Checks that a wrong-length signature throws an IAE.
*/
@Test
public void testVerifyWrongSigLength() throws Exception {
EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
//Signature sgr = Signature.getInstance("EdDSA", "I2P");
Signature sgr = new EdDSAEngine(MessageDigest.getInstance(spec.getHashAlgorithm()));
EdDSAPublicKeySpec pubKey = new EdDSAPublicKeySpec(TEST_PK, spec);
PublicKey vKey = new EdDSAPublicKey(pubKey);
sgr.initVerify(vKey);
sgr.update(TEST_MSG);
exception.expect(SignatureException.class);
exception.expectMessage("signature length is wrong");
sgr.verify(new byte[] {0});
}
@Test
public void testSignResetsForReuse() throws Exception {
EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
Signature sgr = new EdDSAEngine(MessageDigest.getInstance(spec.getHashAlgorithm()));
EdDSAPrivateKeySpec privKey = new EdDSAPrivateKeySpec(TEST_SEED, spec);
PrivateKey sKey = new EdDSAPrivateKey(privKey);
sgr.initSign(sKey);
// First usage
sgr.update(new byte[] {0});
sgr.sign();
// Second usage
sgr.update(TEST_MSG);
assertThat("Second sign failed", sgr.sign(), is(equalTo(TEST_MSG_SIG)));
}
@Test
public void testVerifyResetsForReuse() throws Exception {
EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
Signature sgr = new EdDSAEngine(MessageDigest.getInstance(spec.getHashAlgorithm()));
EdDSAPublicKeySpec pubKey = new EdDSAPublicKeySpec(TEST_PK, spec);
PublicKey vKey = new EdDSAPublicKey(pubKey);
sgr.initVerify(vKey);
// First usage
sgr.update(new byte[] {0});
sgr.verify(TEST_MSG_SIG);
// Second usage
sgr.update(TEST_MSG);
assertThat("Second verify failed", sgr.verify(TEST_MSG_SIG), is(true));
}
@Test
public void testSignOneShotMode() throws Exception {
EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
Signature sgr = new EdDSAEngine(MessageDigest.getInstance(spec.getHashAlgorithm()));
EdDSAPrivateKeySpec privKey = new EdDSAPrivateKeySpec(TEST_SEED, spec);
PrivateKey sKey = new EdDSAPrivateKey(privKey);
sgr.initSign(sKey);
sgr.setParameter(EdDSAEngine.ONE_SHOT_MODE);
sgr.update(TEST_MSG);
assertThat("One-shot mode sign failed", sgr.sign(), is(equalTo(TEST_MSG_SIG)));
}
@Test
public void testVerifyOneShotMode() throws Exception {
EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
Signature sgr = new EdDSAEngine(MessageDigest.getInstance(spec.getHashAlgorithm()));
EdDSAPublicKeySpec pubKey = new EdDSAPublicKeySpec(TEST_PK, spec);
PublicKey vKey = new EdDSAPublicKey(pubKey);
sgr.initVerify(vKey);
sgr.setParameter(EdDSAEngine.ONE_SHOT_MODE);
sgr.update(TEST_MSG);
assertThat("One-shot mode verify failed", sgr.verify(TEST_MSG_SIG), is(true));
}
@Test
public void testSignOneShotModeMultipleUpdates() throws Exception {
EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
Signature sgr = new EdDSAEngine(MessageDigest.getInstance(spec.getHashAlgorithm()));
EdDSAPrivateKeySpec privKey = new EdDSAPrivateKeySpec(TEST_SEED, spec);
PrivateKey sKey = new EdDSAPrivateKey(privKey);
sgr.initSign(sKey);
sgr.setParameter(EdDSAEngine.ONE_SHOT_MODE);
sgr.update(TEST_MSG);
exception.expect(SignatureException.class);
exception.expectMessage("update() already called");
sgr.update(TEST_MSG);
}
@Test
public void testVerifyOneShotModeMultipleUpdates() throws Exception {
EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
EdDSAPublicKeySpec pubKey = new EdDSAPublicKeySpec(TEST_PK, spec);
Signature sgr = new EdDSAEngine(MessageDigest.getInstance(spec.getHashAlgorithm()));
PublicKey vKey = new EdDSAPublicKey(pubKey);
sgr.initVerify(vKey);
sgr.setParameter(EdDSAEngine.ONE_SHOT_MODE);
sgr.update(TEST_MSG);
exception.expect(SignatureException.class);
exception.expectMessage("update() already called");
sgr.update(TEST_MSG);
}
@Test
public void testSignOneShot() throws Exception {
EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
EdDSAPrivateKeySpec privKey = new EdDSAPrivateKeySpec(TEST_SEED, spec);
EdDSAEngine sgr = new EdDSAEngine(MessageDigest.getInstance(spec.getHashAlgorithm()));
PrivateKey sKey = new EdDSAPrivateKey(privKey);
sgr.initSign(sKey);
assertThat("signOneShot() failed", sgr.signOneShot(TEST_MSG), is(equalTo(TEST_MSG_SIG)));
}
@Test
public void testVerifyOneShot() throws Exception {
EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("Ed25519");
EdDSAPublicKeySpec pubKey = new EdDSAPublicKeySpec(TEST_PK, spec);
EdDSAEngine sgr = new EdDSAEngine(MessageDigest.getInstance(spec.getHashAlgorithm()));
PublicKey vKey = new EdDSAPublicKey(pubKey);
sgr.initVerify(vKey);
assertThat("verifyOneShot() failed", sgr.verifyOneShot(TEST_MSG, TEST_MSG_SIG), is(true));
}
}

@ -0,0 +1,81 @@
package org.xbib.io.sshd.eddsa;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.security.spec.PKCS8EncodedKeySpec;
import org.xbib.io.sshd.eddsa.spec.EdDSAPrivateKeySpec;
import org.junit.Test;
/**
*
*/
public class EdDSAPrivateKeyTest {
/**
* The example private key MC4CAQAwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC
* from https://tools.ietf.org/html/draft-ietf-curdle-pkix-04#section-10.3
*/
static final byte[] TEST_PRIVKEY = Utils.hexToBytes("302e020100300506032b657004220420d4ee72dbf913584ad5b6d8f1f769f8ad3afe7c28cbf1d4fbe097a88f44755842");
static final byte[] TEST_PRIVKEY_NULL_PARAMS = Utils.hexToBytes("3030020100300706032b6570050004220420d4ee72dbf913584ad5b6d8f1f769f8ad3afe7c28cbf1d4fbe097a88f44755842");
static final byte[] TEST_PRIVKEY_OLD = Utils.hexToBytes("302f020100300806032b65640a01010420d4ee72dbf913584ad5b6d8f1f769f8ad3afe7c28cbf1d4fbe097a88f44755842");
@Test
public void testDecodeAndEncode() throws Exception {
// Decode
PKCS8EncodedKeySpec encoded = new PKCS8EncodedKeySpec(TEST_PRIVKEY);
EdDSAPrivateKey keyIn = new EdDSAPrivateKey(encoded);
// Encode
EdDSAPrivateKeySpec decoded = new EdDSAPrivateKeySpec(
keyIn.getSeed(),
keyIn.getH(),
keyIn.geta(),
keyIn.getA(),
keyIn.getParams());
EdDSAPrivateKey keyOut = new EdDSAPrivateKey(decoded);
// Check
assertThat(keyOut.getEncoded(), is(equalTo(TEST_PRIVKEY)));
}
@Test
public void testDecodeWithNullAndEncode() throws Exception {
// Decode
PKCS8EncodedKeySpec encoded = new PKCS8EncodedKeySpec(TEST_PRIVKEY_NULL_PARAMS);
EdDSAPrivateKey keyIn = new EdDSAPrivateKey(encoded);
// Encode
EdDSAPrivateKeySpec decoded = new EdDSAPrivateKeySpec(
keyIn.getSeed(),
keyIn.getH(),
keyIn.geta(),
keyIn.getA(),
keyIn.getParams());
EdDSAPrivateKey keyOut = new EdDSAPrivateKey(decoded);
// Check
assertThat(keyOut.getEncoded(), is(equalTo(TEST_PRIVKEY)));
}
@Test
public void testReEncodeOldEncoding() throws Exception {
// Decode
PKCS8EncodedKeySpec encoded = new PKCS8EncodedKeySpec(TEST_PRIVKEY_OLD);
EdDSAPrivateKey keyIn = new EdDSAPrivateKey(encoded);
// Encode
EdDSAPrivateKeySpec decoded = new EdDSAPrivateKeySpec(
keyIn.getSeed(),
keyIn.getH(),
keyIn.geta(),
keyIn.getA(),
keyIn.getParams());
EdDSAPrivateKey keyOut = new EdDSAPrivateKey(decoded);
// Check
assertThat(keyOut.getEncoded(), is(equalTo(TEST_PRIVKEY)));
}
}

@ -0,0 +1,72 @@
package org.xbib.io.sshd.eddsa;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.security.spec.X509EncodedKeySpec;
import org.xbib.io.sshd.eddsa.spec.EdDSAPublicKeySpec;
import org.junit.Test;
/**
*
*/
public class EdDSAPublicKeyTest {
/**
* The example public key MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=
* from https://tools.ietf.org/html/draft-ietf-curdle-pkix-04#section-10.1
*/
static final byte[] TEST_PUBKEY = Utils.hexToBytes("302a300506032b657003210019bf44096984cdfe8541bac167dc3b96c85086aa30b6b6cb0c5c38ad703166e1");
static final byte[] TEST_PUBKEY_NULL_PARAMS = Utils.hexToBytes("302c300706032b6570050003210019bf44096984cdfe8541bac167dc3b96c85086aa30b6b6cb0c5c38ad703166e1");
static final byte[] TEST_PUBKEY_OLD = Utils.hexToBytes("302d300806032b65640a010103210019bf44096984cdfe8541bac167dc3b96c85086aa30b6b6cb0c5c38ad703166e1");
@Test
public void testDecodeAndEncode() throws Exception {
// Decode
X509EncodedKeySpec encoded = new X509EncodedKeySpec(TEST_PUBKEY);
EdDSAPublicKey keyIn = new EdDSAPublicKey(encoded);
// Encode
EdDSAPublicKeySpec decoded = new EdDSAPublicKeySpec(
keyIn.getA(),
keyIn.getParams());
EdDSAPublicKey keyOut = new EdDSAPublicKey(decoded);
// Check
assertThat(keyOut.getEncoded(), is(equalTo(TEST_PUBKEY)));
}
@Test
public void testDecodeWithNullAndEncode() throws Exception {
// Decode
X509EncodedKeySpec encoded = new X509EncodedKeySpec(TEST_PUBKEY_NULL_PARAMS);
EdDSAPublicKey keyIn = new EdDSAPublicKey(encoded);
// Encode
EdDSAPublicKeySpec decoded = new EdDSAPublicKeySpec(
keyIn.getA(),
keyIn.getParams());
EdDSAPublicKey keyOut = new EdDSAPublicKey(decoded);
// Check
assertThat(keyOut.getEncoded(), is(equalTo(TEST_PUBKEY)));
}
@Test
public void testReEncodeOldEncoding() throws Exception {
// Decode
X509EncodedKeySpec encoded = new X509EncodedKeySpec(TEST_PUBKEY_OLD);
EdDSAPublicKey keyIn = new EdDSAPublicKey(encoded);
// Encode
EdDSAPublicKeySpec decoded = new EdDSAPublicKeySpec(
keyIn.getA(),
keyIn.getParams());
EdDSAPublicKey keyOut = new EdDSAPublicKey(decoded);
// Check
assertThat(keyOut.getEncoded(), is(equalTo(TEST_PUBKEY)));
}
}

@ -0,0 +1,37 @@
package org.xbib.io.sshd.eddsa;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.security.Signature;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
/**
*
*/
public class EdDSASecurityProviderTest {
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void canGetInstancesWhenProviderIsPresent() throws Exception {
Security.addProvider(new EdDSASecurityProvider());
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EdDSA", "EdDSA");
KeyFactory keyFac = KeyFactory.getInstance("EdDSA", "EdDSA");
Signature sgr = Signature.getInstance("NONEwithEdDSA", "EdDSA");
Security.removeProvider("EdDSA");
}
@Test
public void cannotGetInstancesWhenProviderIsNotPresent() throws Exception {
exception.expect(NoSuchProviderException.class);
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EdDSA", "EdDSA");
}
}

@ -0,0 +1,121 @@
package org.xbib.io.sshd.eddsa;
import org.hamcrest.core.IsEqual;
import org.junit.Assert;
import org.junit.Test;
import java.security.SecureRandom;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
/**
* additional test by the NEM project team.
*
*/
public class UtilsTest {
private static final String hex1 = "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29";
private static final String hex2 = "47a3f5b71494bcd961f3a4e859a238d6eaf8e648746d2f56a89b5e236f98d45f";
private static final String hex3 = "5fd396e4a2b5dc9078f57e3ab5a87c28fd128e5f78cc4a97f4122dc45f6e4bb9";
private static final byte[] bytes1 = { 59, 106, 39, -68, -50, -74, -92, 45, 98, -93, -88, -48, 42, 111, 13, 115,
101, 50, 21, 119, 29, -30, 67, -90, 58, -64, 72, -95, -117, 89, -38, 41 };
private static final byte[] bytes2 = { 71, -93, -11, -73, 20, -108, -68, -39, 97, -13, -92, -24, 89, -94, 56, -42,
-22, -8, -26, 72, 116, 109, 47, 86, -88, -101, 94, 35, 111, -104, -44, 95 };
private static final byte[] bytes3 = { 95, -45, -106, -28, -94, -75, -36, -112, 120, -11, 126, 58, -75, -88, 124, 40,
-3, 18, -114, 95, 120, -52, 74, -105, -12, 18, 45, -60, 95, 110, 75, -71 };
/**
* Test method for {@link Utils#equal(int, int)}.
*/
@Test
public void testIntEqual() {
assertThat(Utils.equal(0, 0), is(1));
assertThat(Utils.equal(1, 1), is(1));
assertThat(Utils.equal(1, 0), is(0));
assertThat(Utils.equal(1, 127), is(0));
assertThat(Utils.equal(-127, 127), is(0));
assertThat(Utils.equal(-42, -42), is(1));
assertThat(Utils.equal(255, 255), is(1));
assertThat(Utils.equal(-255, -256), is(0));
}
@Test
public void equalsReturnsOneForEqualByteArrays() {
final SecureRandom random = new SecureRandom();
final byte[] bytes1 = new byte[32];
final byte[] bytes2 = new byte[32];
for (int i=0; i<100; i++) {
random.nextBytes(bytes1);
System.arraycopy(bytes1, 0, bytes2, 0, 32);
Assert.assertThat(Utils.equal(bytes1, bytes2), IsEqual.equalTo(1));
}
}
@Test
public void equalsReturnsZeroForUnequalByteArrays() {
final SecureRandom random = new SecureRandom();
final byte[] bytes1 = new byte[32];
final byte[] bytes2 = new byte[32];
random.nextBytes(bytes1);
for (int i=0; i<32; i++) {
System.arraycopy(bytes1, 0, bytes2, 0, 32);
bytes2[i] = (byte)(bytes2[i] ^ 0xff);
Assert.assertThat(Utils.equal(bytes1, bytes2), IsEqual.equalTo(0));
}
}
/**
* Test method for {@link Utils#equal(byte[], byte[])}.
*/
@Test
public void testByteArrayEqual() {
byte[] zero = new byte[32];
byte[] one = new byte[32];
one[0] = 1;
assertThat(Utils.equal(zero, zero), is(1));
assertThat(Utils.equal(one, one), is(1));
assertThat(Utils.equal(one, zero), is(0));
assertThat(Utils.equal(zero, one), is(0));
}
/**
* Test method for {@link Utils#negative(int)}.
*/
@Test
public void testNegative() {
assertThat(Utils.negative(0), is(0));
assertThat(Utils.negative(1), is(0));
assertThat(Utils.negative(-1), is(1));
assertThat(Utils.negative(32), is(0));
assertThat(Utils.negative(-100), is(1));
assertThat(Utils.negative(127), is(0));
assertThat(Utils.negative(-255), is(1));
}
/**
* Test method for {@link Utils#bit(byte[], int)}.
*/
@Test
public void testBit() {
assertThat(Utils.bit(new byte[] {0}, 0), is(0));
assertThat(Utils.bit(new byte[] {8}, 3), is(1));
assertThat(Utils.bit(new byte[] {1, 2, 3}, 9), is(1));
assertThat(Utils.bit(new byte[] {1, 2, 3}, 15), is(0));
assertThat(Utils.bit(new byte[] {1, 2, 3}, 16), is(1));
}
@Test
public void hexToBytesReturnsCorrectByteArray() {
Assert.assertThat(Utils.hexToBytes(hex1), IsEqual.equalTo(bytes1));
Assert.assertThat(Utils.hexToBytes(hex2), IsEqual.equalTo(bytes2));
Assert.assertThat(Utils.hexToBytes(hex3), IsEqual.equalTo(bytes3));
}
@Test
public void bytesToHexReturnsCorrectHexString() {
Assert.assertThat(Utils.bytesToHex(bytes1), IsEqual.equalTo(hex1));
Assert.assertThat(Utils.bytesToHex(bytes2), IsEqual.equalTo(hex2));
Assert.assertThat(Utils.bytesToHex(bytes3), IsEqual.equalTo(hex3));
}
}

@ -0,0 +1,183 @@
package org.xbib.io.sshd.eddsa.math;
import org.hamcrest.core.IsEqual;
import org.hamcrest.core.IsNot;
import org.junit.Assert;
import org.junit.Test;
import java.math.BigInteger;
/**
* Tests rely on the BigInteger class.
*/
public abstract class AbstractFieldElementTest {
protected abstract FieldElement getRandomFieldElement();
protected abstract BigInteger toBigInteger(FieldElement f);
protected abstract BigInteger getQ();
protected abstract Field getField();
protected abstract FieldElement getZeroFieldElement();
protected abstract FieldElement getNonZeroFieldElement();
@Test
public void isNonZeroReturnsFalseIfFieldElementIsZero() {
final FieldElement f = getZeroFieldElement();
Assert.assertThat(f.isNonZero(), IsEqual.equalTo(false));
}
@Test
public void isNonZeroReturnsTrueIfFieldElementIsNonZero() {
final FieldElement f = getNonZeroFieldElement();
Assert.assertThat(f.isNonZero(), IsEqual.equalTo(true));
}
@Test
public void addReturnsCorrectResult() {
for (int i=0; i<1000; i++) {
final FieldElement f1 = getRandomFieldElement();
final FieldElement f2 = getRandomFieldElement();
final BigInteger b1 = toBigInteger(f1);
final BigInteger b2 = toBigInteger(f2);
final FieldElement f3 = f1.add(f2);
final BigInteger b3 = toBigInteger(f3).mod(getQ());
Assert.assertThat(b3, IsEqual.equalTo(b1.add(b2).mod(getQ())));
}
}
@Test
public void subtractReturnsCorrectResult() {
for (int i=0; i<1000; i++) {
final FieldElement f1 = getRandomFieldElement();
final FieldElement f2 = getRandomFieldElement();
final BigInteger b1 = toBigInteger(f1);
final BigInteger b2 = toBigInteger(f2);
final FieldElement f3 = f1.subtract(f2);
final BigInteger b3 = toBigInteger(f3).mod(getQ());
Assert.assertThat(b3, IsEqual.equalTo(b1.subtract(b2).mod(getQ())));
}
}
@Test
public void negateReturnsCorrectResult() {
for (int i=0; i<1000; i++) {
final FieldElement f1 = getRandomFieldElement();
final BigInteger b1 = toBigInteger(f1);
final FieldElement f2 = f1.negate();
final BigInteger b2 = toBigInteger(f2).mod(getQ());
Assert.assertThat(b2, IsEqual.equalTo(b1.negate().mod(getQ())));
}
}
@Test
public void multiplyReturnsCorrectResult() {
for (int i=0; i<1000; i++) {
final FieldElement f1 = getRandomFieldElement();
final FieldElement f2 = getRandomFieldElement();
final BigInteger b1 = toBigInteger(f1);
final BigInteger b2 = toBigInteger(f2);
final FieldElement f3 = f1.multiply(f2);
final BigInteger b3 = toBigInteger(f3).mod(getQ());
Assert.assertThat(b3, IsEqual.equalTo(b1.multiply(b2).mod(getQ())));
}
}
@Test
public void squareReturnsCorrectResult() {
for (int i=0; i<1000; i++) {
final FieldElement f1 = getRandomFieldElement();
final BigInteger b1 = toBigInteger(f1);
final FieldElement f2 = f1.square();
final BigInteger b2 = toBigInteger(f2).mod(getQ());
Assert.assertThat(b2, IsEqual.equalTo(b1.multiply(b1).mod(getQ())));
}
}
@Test
public void squareAndDoubleReturnsCorrectResult() {
for (int i=0; i<1000; i++) {
final FieldElement f1 = getRandomFieldElement();
final BigInteger b1 = toBigInteger(f1);
final FieldElement f2 = f1.squareAndDouble();
final BigInteger b2 = toBigInteger(f2).mod(getQ());
Assert.assertThat(b2, IsEqual.equalTo(b1.multiply(b1).multiply(new BigInteger("2")).mod(getQ())));
}
}
@Test
public void invertReturnsCorrectResult() {
for (int i=0; i<1000; i++) {
final FieldElement f1 = getRandomFieldElement();
final BigInteger b1 = toBigInteger(f1);
final FieldElement f2 = f1.invert();
final BigInteger b2 = toBigInteger(f2).mod(getQ());
Assert.assertThat(b2, IsEqual.equalTo(b1.modInverse(getQ())));
}
}
@Test
public void pow22523ReturnsCorrectResult() {
for (int i=0; i<1000; i++) {
final FieldElement f1 = getRandomFieldElement();
final BigInteger b1 = toBigInteger(f1);
final FieldElement f2 = f1.pow22523();
final BigInteger b2 = toBigInteger(f2).mod(getQ());
Assert.assertThat(b2, IsEqual.equalTo(b1.modPow(BigInteger.ONE.shiftLeft(252).subtract(new BigInteger("3")), getQ())));
}
}
@Test
public void cmovReturnsCorrectResult() {
final FieldElement zero = getZeroFieldElement();
final FieldElement nz = getNonZeroFieldElement();
final FieldElement f = getRandomFieldElement();
Assert.assertThat(zero.cmov(nz, 0), IsEqual.equalTo(zero));
Assert.assertThat(zero.cmov(nz, 1), IsEqual.equalTo(nz));
Assert.assertThat(f.cmov(nz, 0), IsEqual.equalTo(f));
Assert.assertThat(f.cmov(nz, 1), IsEqual.equalTo(nz));
}
@Test
public void equalsOnlyReturnsTrueForEquivalentObjects() {
final FieldElement f1 = getRandomFieldElement();
final FieldElement f2 = getField().getEncoding().decode(f1.toByteArray());
final FieldElement f3 = getRandomFieldElement();
final FieldElement f4 = getRandomFieldElement();
Assert.assertThat(f1, IsEqual.equalTo(f2));
Assert.assertThat(f1, IsNot.not(IsEqual.equalTo(f3)));
Assert.assertThat(f1, IsNot.not(IsEqual.equalTo(f4)));
Assert.assertThat(f3, IsNot.not(IsEqual.equalTo(f4)));
}
@Test
public void hashCodesAreEqualForEquivalentObjects() {
final FieldElement f1 = getRandomFieldElement();
final FieldElement f2 = getField().getEncoding().decode(f1.toByteArray());
final FieldElement f3 = getRandomFieldElement();
final FieldElement f4 = getRandomFieldElement();
Assert.assertThat(f1.hashCode(), IsEqual.equalTo(f2.hashCode()));
Assert.assertThat(f1.hashCode(), IsNot.not(IsEqual.equalTo(f3.hashCode())));
Assert.assertThat(f1.hashCode(), IsNot.not(IsEqual.equalTo(f4.hashCode())));
Assert.assertThat(f3.hashCode(), IsNot.not(IsEqual.equalTo(f4.hashCode())));
}
}

@ -0,0 +1,78 @@
package org.xbib.io.sshd.eddsa.math;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveSpec;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveTable;
import org.junit.Test;
/**
* Based on the tests in checkparams.py from the Python Ed25519 implementation.
*
*/
public class ConstantsTest {
static final EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
static final Curve curve = ed25519.getCurve();
static final FieldElement ZERO = curve.getField().ZERO;
static final FieldElement ONE = curve.getField().ONE;
static final FieldElement TWO = curve.getField().TWO;
static final GroupElement P3_ZERO = GroupElement.p3(curve, ZERO, ONE, ONE, ZERO);
@Test
public void testb() {
int b = curve.getField().getb();
assertThat(b, is(greaterThanOrEqualTo(10)));
try {
MessageDigest h = MessageDigest.getInstance(ed25519.getHashAlgorithm());
assertThat(8 * h.getDigestLength(), is(equalTo(2 * b)));
} catch (NoSuchAlgorithmException e) {
fail(e.getMessage());
}
}
/*@Test
public void testq() {
FieldElement q = curve.getField().getQ();
assertThat(TWO.modPow(q.subtractOne(), q), is(equalTo(ONE)));
assertThat(q.mod(curve.getField().FOUR), is(equalTo(ONE)));
}
@Test
public void testl() {
int b = curve.getField().getb();
BigInteger l = ed25519.getL();
assertThat(TWO.modPow(l.subtract(BigInteger.ONE), l), is(equalTo(ONE)));
assertThat(l, is(greaterThanOrEqualTo(BigInteger.valueOf(2).pow(b-4))));
assertThat(l, is(lessThanOrEqualTo(BigInteger.valueOf(2).pow(b-3))));
}
@Test
public void testd() {
FieldElement q = curve.getField().getQ();
FieldElement qm1 = q.subtractOne();
assertThat(curve.getD().modPow(qm1.divide(curve.getField().TWO), q), is(equalTo(qm1)));
}
@Test
public void testI() {
FieldElement q = curve.getField().getQ();
assertThat(curve.getI().modPow(curve.getField().TWO, q), is(equalTo(q.subtractOne())));
}*/
@Test
public void testB() {
GroupElement B = ed25519.getB();
assertThat(B.isOnCurve(curve), is(true));
//assertThat(B.scalarMultiply(new BigIntegerLittleEndianEncoding().encode(ed25519.getL(), curve.getField().getb()/8)), is(equalTo(P3_ZERO)));
}
}

@ -0,0 +1,865 @@
package org.xbib.io.sshd.eddsa.math;
import org.xbib.io.sshd.eddsa.Ed25519TestVectors;
import org.xbib.io.sshd.eddsa.Utils;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveSpec;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveTable;
import org.hamcrest.core.IsEqual;
import org.hamcrest.core.IsNot;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.math.BigInteger;
import java.util.Arrays;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
/**
* Additional tests by NEM project team.
*
*/
public class GroupElementTest {
static final byte[] BYTES_ZEROZERO = Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
static final byte[] BYTES_ONEONE = Utils.hexToBytes("0100000000000000000000000000000000000000000000000000000000000080");
static final byte[] BYTES_TENZERO = Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
static final byte[] BYTES_ONETEN = Utils.hexToBytes("0a00000000000000000000000000000000000000000000000000000000000080");
static final EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
static final Curve curve = ed25519.getCurve();
static final FieldElement ZERO = curve.getField().ZERO;
static final FieldElement ONE = curve.getField().ONE;
static final FieldElement TWO = curve.getField().TWO;
static final FieldElement TEN = curve.getField().fromByteArray(Utils.hexToBytes("0a00000000000000000000000000000000000000000000000000000000000000"));
static final GroupElement P2_ZERO = GroupElement.p2(curve, ZERO, ONE, ONE);
static final FieldElement[] PKR = new FieldElement[] {
curve.getField().fromByteArray(Utils.hexToBytes("5849722e338aced7b50c7f0e9328f9a10c847b08e40af5c5b0577b0fd8984f15")),
curve.getField().fromByteArray(Utils.hexToBytes("3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29"))
};
static final byte[] BYTES_PKR = Utils.hexToBytes("3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29");
@Rule
public ExpectedException exception = ExpectedException.none();
/**
* Test method for {@link GroupElement#p2(Curve, FieldElement, FieldElement, FieldElement)}.
*/
@Test
public void testP2() {
final GroupElement t = GroupElement.p2(curve, ZERO, ONE, ONE);
assertThat(t.curve, is(equalTo(curve)));
assertThat(t.repr, is(GroupElement.Representation.P2));
assertThat(t.X, is(ZERO));
assertThat(t.Y, is(ONE));
assertThat(t.Z, is(ONE));
assertThat(t.T, is((FieldElement) null));
}
/**
* Test method for {@link GroupElement#p3(Curve, FieldElement, FieldElement, FieldElement, FieldElement)}.
*/
@Test
public void testP3() {
final GroupElement t = GroupElement.p3(curve, ZERO, ONE, ONE, ZERO);
assertThat(t.curve, is(equalTo(curve)));
assertThat(t.repr, is(GroupElement.Representation.P3));
assertThat(t.X, is(ZERO));
assertThat(t.Y, is(ONE));
assertThat(t.Z, is(ONE));
assertThat(t.T, is(ZERO));
}
/**
* Test method for {@link GroupElement#p1p1(Curve, FieldElement, FieldElement, FieldElement, FieldElement)}.
*/
@Test
public void testP1p1() {
final GroupElement t = GroupElement.p1p1(curve, ZERO, ONE, ONE, ONE);
assertThat(t.curve, is(equalTo(curve)));
assertThat(t.repr, is(GroupElement.Representation.P1P1));
assertThat(t.X, is(ZERO));
assertThat(t.Y, is(ONE));
assertThat(t.Z, is(ONE));
assertThat(t.T, is(ONE));
}
/**
* Test method for {@link GroupElement#precomp(Curve, FieldElement, FieldElement, FieldElement)}.
*/
@Test
public void testPrecomp() {
final GroupElement t = GroupElement.precomp(curve, ONE, ONE, ZERO);
assertThat(t.curve, is(equalTo(curve)));
assertThat(t.repr, is(GroupElement.Representation.PRECOMP));
assertThat(t.X, is(ONE));
assertThat(t.Y, is(ONE));
assertThat(t.Z, is(ZERO));
assertThat(t.T, is((FieldElement) null));
}
/**
* Test method for {@link GroupElement#cached(Curve, FieldElement, FieldElement, FieldElement, FieldElement)}.
*/
@Test
public void testCached() {
final GroupElement t = GroupElement.cached(curve, ONE, ONE, ONE, ZERO);
assertThat(t.curve, is(equalTo(curve)));
assertThat(t.repr, is(GroupElement.Representation.CACHED));
assertThat(t.X, is(ONE));
assertThat(t.Y, is(ONE));
assertThat(t.Z, is(ONE));
assertThat(t.T, is(ZERO));
}
/**
* Test method for {@link GroupElement#GroupElement(Curve, GroupElement.Representation, FieldElement, FieldElement, FieldElement, FieldElement)}.
*/
@Test
public void testGroupElementCurveRepresentationFieldElementFieldElementFieldElementFieldElement() {
final GroupElement t = new GroupElement(curve, GroupElement.Representation.P3, ZERO, ONE, ONE, ZERO);
assertThat(t.curve, is(equalTo(curve)));
assertThat(t.repr, is(GroupElement.Representation.P3));
assertThat(t.X, is(ZERO));
assertThat(t.Y, is(ONE));
assertThat(t.Z, is(ONE));
assertThat(t.T, is(ZERO));
}
/**
* Tests {@link GroupElement#GroupElement(Curve, byte[])} and
* {@link GroupElement#toByteArray()} against valid public keys.
*/
@Test
public void testToAndFromByteArray() {
GroupElement t;
for (Ed25519TestVectors.TestTuple testCase : Ed25519TestVectors.testCases) {
t = new GroupElement(curve, testCase.pk);
assertThat("Test case " + testCase.caseNum + " failed",
t.toByteArray(), is(equalTo(testCase.pk)));
}
}
/**
* Test method for {@link GroupElement#GroupElement(Curve, byte[])}.
*/
@Test
public void testGroupElementByteArray() {
final GroupElement t = new GroupElement(curve, BYTES_PKR);
final GroupElement s = GroupElement.p3(curve, PKR[0], PKR[1], ONE, PKR[0].multiply(PKR[1]));
assertThat(t, is(equalTo(s)));
}
@Test
public void constructorUsingByteArrayReturnsExpectedResult() {
for (int i=0; i<100; i++) {
// Arrange:
final GroupElement g = MathUtils.getRandomGroupElement();
final byte[] bytes = g.toByteArray();
// Act:
final GroupElement h1 = new GroupElement(curve, bytes);
final GroupElement h2 = MathUtils.toGroupElement(bytes);
// Assert:
Assert.assertThat(h1, IsEqual.equalTo(h2));
}
}
/**
* Test method for {@link GroupElement#toByteArray()}.
* <p>
* TODO 20141001 BR: why test with points which are not on the curve?
*/
@Test
public void testToByteArray() {
byte[] zerozero = GroupElement.p2(curve, ZERO, ZERO, ONE).toByteArray();
assertThat(zerozero.length, is(equalTo(BYTES_ZEROZERO.length)));
assertThat(zerozero, is(equalTo(BYTES_ZEROZERO)));
byte[] oneone = GroupElement.p2(curve, ONE, ONE, ONE).toByteArray();
assertThat(oneone.length, is(equalTo(BYTES_ONEONE.length)));
assertThat(oneone, is(equalTo(BYTES_ONEONE)));
byte[] tenzero = GroupElement.p2(curve, TEN, ZERO, ONE).toByteArray();
assertThat(tenzero.length, is(equalTo(BYTES_TENZERO.length)));
assertThat(tenzero, is(equalTo(BYTES_TENZERO)));
byte[] oneten = GroupElement.p2(curve, ONE, TEN, ONE).toByteArray();
assertThat(oneten.length, is(equalTo(BYTES_ONETEN.length)));
assertThat(oneten, is(equalTo(BYTES_ONETEN)));
byte[] pkr = GroupElement.p2(curve, PKR[0], PKR[1], ONE).toByteArray();
assertThat(pkr.length, is(equalTo(BYTES_PKR.length)));
assertThat(pkr, is(equalTo(BYTES_PKR)));
}
@Test
public void toByteArrayReturnsExpectedResult() {
for (int i=0; i<100; i++) {
// Arrange:
final GroupElement g = MathUtils.getRandomGroupElement();
// Act:
final byte[] gBytes = g.toByteArray();
final byte[] bytes = MathUtils.toByteArray(MathUtils.toBigInteger(g.getY()));
if (MathUtils.toBigInteger(g.getX()).mod(new BigInteger("2")).equals(BigInteger.ONE)) {
bytes[31] |= 0x80;
}
// Assert:
Assert.assertThat(Arrays.equals(gBytes, bytes), IsEqual.equalTo(true));
}
}
// region toX where X is the representation
/**
* Test method for {@link GroupElement#toP2()}.
*/
@Test
public void testToP2() {
GroupElement p3zero = curve.getZero(GroupElement.Representation.P3);
GroupElement t = p3zero.toP2();
assertThat(t.repr, is(GroupElement.Representation.P2));
assertThat(t.X, is(p3zero.X));
assertThat(t.Y, is(p3zero.Y));
assertThat(t.Z, is(p3zero.Z));
assertThat(t.T, is((FieldElement) null));
GroupElement B = ed25519.getB();
t = B.toP2();
assertThat(t.repr, is(GroupElement.Representation.P2));
assertThat(t.X, is(B.X));
assertThat(t.Y, is(B.Y));
assertThat(t.Z, is(B.Z));
assertThat(t.T, is((FieldElement) null));
}
@Test (expected = IllegalArgumentException.class)
public void toP2ThrowsIfGroupElementHasPrecompRepresentation() {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.PRECOMP);
// Assert:
g.toP2();
}
@Test (expected = IllegalArgumentException.class)
public void toP2ThrowsIfGroupElementHasCachedRepresentation() {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.CACHED);
// Assert:
g.toP2();
}
@Test
public void toP2ReturnsExpectedResultIfGroupElementHasP2Representation() {
for (int i=0; i<10; i++) {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.P2);
// Act:
final GroupElement h = g.toP2();
// Assert:
Assert.assertThat(h, IsEqual.equalTo(g));
Assert.assertThat(h.getRepresentation(), IsEqual.equalTo(GroupElement.Representation.P2));
Assert.assertThat(h.getX(), IsEqual.equalTo(g.getX()));
Assert.assertThat(h.getY(), IsEqual.equalTo(g.getY()));
Assert.assertThat(h.getZ(), IsEqual.equalTo(g.getZ()));
Assert.assertThat(h.getT(), IsEqual.equalTo(null));
}
}
@Test
public void toP2ReturnsExpectedResultIfGroupElementHasP3Representation() {
for (int i=0; i<10; i++) {
// Arrange:
final GroupElement g = MathUtils.getRandomGroupElement();
// Act:
final GroupElement h1 = g.toP2();
final GroupElement h2 = MathUtils.toRepresentation(g, GroupElement.Representation.P2);
// Assert:
Assert.assertThat(h1, IsEqual.equalTo(h2));
Assert.assertThat(h1.getRepresentation(), IsEqual.equalTo(GroupElement.Representation.P2));
Assert.assertThat(h1.getX(), IsEqual.equalTo(g.getX()));
Assert.assertThat(h1.getY(), IsEqual.equalTo(g.getY()));
Assert.assertThat(h1.getZ(), IsEqual.equalTo(g.getZ()));
Assert.assertThat(h1.getT(), IsEqual.equalTo(null));
}
}
@Test
public void toP2ReturnsExpectedResultIfGroupElementHasP1P1Representation() {
for (int i=0; i<10; i++) {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.P1P1);
// Act:
final GroupElement h1 = g.toP2();
final GroupElement h2 = MathUtils.toRepresentation(g, GroupElement.Representation.P2);
// Assert:
Assert.assertThat(h1, IsEqual.equalTo(h2));
Assert.assertThat(h1.getRepresentation(), IsEqual.equalTo(GroupElement.Representation.P2));
Assert.assertThat(h1.getX(), IsEqual.equalTo(g.getX().multiply(g.getT())));
Assert.assertThat(h1.getY(), IsEqual.equalTo(g.getY().multiply(g.getZ())));
Assert.assertThat(h1.getZ(), IsEqual.equalTo(g.getZ().multiply(g.getT())));
Assert.assertThat(h1.getT(), IsEqual.equalTo(null));
}
}
@Test (expected = IllegalArgumentException.class)
public void toP3ThrowsIfGroupElementHasP2Representation() {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.P2);
// Assert:
g.toP3();
}
@Test (expected = IllegalArgumentException.class)
public void toP3ThrowsIfGroupElementHasPrecompRepresentation() {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.PRECOMP);
// Assert:
g.toP3();
}
@Test (expected = IllegalArgumentException.class)
public void toP3ThrowsIfGroupElementHasCachedRepresentation() {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.CACHED);
// Assert:
g.toP3();
}
@Test
public void toP3ReturnsExpectedResultIfGroupElementHasP1P1Representation() {
for (int i=0; i<10; i++) {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.P1P1);
// Act:
final GroupElement h1 = g.toP3();
final GroupElement h2 = MathUtils.toRepresentation(g, GroupElement.Representation.P3);
// Assert:
Assert.assertThat(h1, IsEqual.equalTo(h2));
Assert.assertThat(h1.getRepresentation(), IsEqual.equalTo(GroupElement.Representation.P3));
Assert.assertThat(h1.getX(), IsEqual.equalTo(g.getX().multiply(g.getT())));
Assert.assertThat(h1.getY(), IsEqual.equalTo(g.getY().multiply(g.getZ())));
Assert.assertThat(h1.getZ(), IsEqual.equalTo(g.getZ().multiply(g.getT())));
Assert.assertThat(h1.getT(), IsEqual.equalTo(g.getX().multiply(g.getY())));
}
}
@Test
public void toP3ReturnsExpectedResultIfGroupElementHasP3Representation() {
for (int i=0; i<10; i++) {
// Arrange:
final GroupElement g = MathUtils.getRandomGroupElement();
// Act:
final GroupElement h = g.toP3();
// Assert:
Assert.assertThat(h, IsEqual.equalTo(g));
Assert.assertThat(h.getRepresentation(), IsEqual.equalTo(GroupElement.Representation.P3));
Assert.assertThat(h, IsEqual.equalTo(g));
Assert.assertThat(h.getX(), IsEqual.equalTo(g.getX()));
Assert.assertThat(h.getY(), IsEqual.equalTo(g.getY()));
Assert.assertThat(h.getZ(), IsEqual.equalTo(g.getZ()));
Assert.assertThat(h.getT(), IsEqual.equalTo(g.getT()));
}
}
@Test (expected = IllegalArgumentException.class)
public void toCachedThrowsIfGroupElementHasP2Representation() {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.P2);
// Assert:
g.toCached();
}
@Test (expected = IllegalArgumentException.class)
public void toCachedThrowsIfGroupElementHasPrecompRepresentation() {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.PRECOMP);
// Assert:
g.toCached();
}
@Test (expected = IllegalArgumentException.class)
public void toCachedThrowsIfGroupElementHasP1P1Representation() {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.P1P1);
// Assert:
g.toCached();
}
@Test
public void toCachedReturnsExpectedResultIfGroupElementHasCachedRepresentation() {
for (int i=0; i<10; i++) {
// Arrange:
final GroupElement g = MathUtils.toRepresentation(MathUtils.getRandomGroupElement(), GroupElement.Representation.CACHED);
// Act:
final GroupElement h = g.toCached();
// Assert:
Assert.assertThat(h, IsEqual.equalTo(g));
Assert.assertThat(h.getRepresentation(), IsEqual.equalTo(GroupElement.Representation.CACHED));
Assert.assertThat(h, IsEqual.equalTo(g));
Assert.assertThat(h.getX(), IsEqual.equalTo(g.getX()));
Assert.assertThat(h.getY(), IsEqual.equalTo(g.getY()));
Assert.assertThat(h.getZ(), IsEqual.equalTo(g.getZ()));
Assert.assertThat(h.getT(), IsEqual.equalTo(g.getT()));
}
}
@Test
public void toCachedReturnsExpectedResultIfGroupElementHasP3Representation() {
for (int i=0; i<10; i++) {
// Arrange:
final GroupElement g = MathUtils.getRandomGroupElement();
// Act:
final GroupElement h1 = g.toCached();
final GroupElement h2 = MathUtils.toRepresentation(g, GroupElement.Representation.CACHED);
// Assert:
Assert.assertThat(h1, IsEqual.equalTo(h2));
Assert.assertThat(h1.getRepresentation(), IsEqual.equalTo(GroupElement.Representation.CACHED));
Assert.assertThat(h1, IsEqual.equalTo(g));
Assert.assertThat(h1.getX(), IsEqual.equalTo(g.getY().add(g.getX())));
Assert.assertThat(h1.getY(), IsEqual.equalTo(g.getY().subtract(g.getX())));
Assert.assertThat(h1.getZ(), IsEqual.equalTo(g.getZ()));
Assert.assertThat(h1.getT(), IsEqual.equalTo(g.getT().multiply(curve.get2D())));
}
}
// endregion
/**
* Test method for {@link GroupElement#precompute(boolean)}.
*/
@Test
public void testPrecompute() {
GroupElement B = ed25519.getB();
assertThat(B.precmp, is(equalTo(PrecomputationTestVectors.testPrecmp)));
assertThat(B.dblPrecmp, is(equalTo(PrecomputationTestVectors.testDblPrecmp)));
}
@Test
public void precomputedTableContainsExpectedGroupElements() {
// Arrange:
GroupElement g = ed25519.getB();
// Act + Assert:
for (int i = 0; i < 32; i++) {
GroupElement h = g;
for (int j = 0; j < 8; j++) {
Assert.assertThat(MathUtils.toRepresentation(h, GroupElement.Representation.PRECOMP), IsEqual.equalTo(ed25519.getB().precmp[i][j]));
h = MathUtils.addGroupElements(h, g);
}
for (int k = 0; k < 8; k++) {
g = MathUtils.addGroupElements(g, g);
}
}
}
@Test
public void dblPrecomputedTableContainsExpectedGroupElements() {
// Arrange:
GroupElement g = ed25519.getB();
GroupElement h = MathUtils.addGroupElements(g, g);
// Act + Assert:
for (int i=0; i<8; i++) {
Assert.assertThat(MathUtils.toRepresentation(g, GroupElement.Representation.PRECOMP), IsEqual.equalTo(ed25519.getB().dblPrecmp[i]));
g = MathUtils.addGroupElements(g, h);
}
}
/**
* Test method for {@link GroupElement#dbl()}.
*/
@Test
public void testDbl() {
GroupElement B = ed25519.getB();
// 2 * B = B + B
assertThat(B.dbl(), is(equalTo(B.add(B.toCached()))));
}
@Test
public void dblReturnsExpectedResult() {
for (int i=0; i<1000; i++) {
// Arrange:
final GroupElement g = MathUtils.getRandomGroupElement();
// Act:
final GroupElement h1 = g.dbl();
final GroupElement h2 = MathUtils.doubleGroupElement(g);
// Assert:
Assert.assertThat(h2, IsEqual.equalTo(h1));
}
}
@Test
public void addingNeutralGroupElementDoesNotChangeGroupElement() {
final GroupElement neutral = GroupElement.p3(curve, curve.getField().ZERO, curve.getField().ONE, curve.getField().ONE, curve.getField().ZERO);
for (int i=0; i<1000; i++) {
// Arrange:
final GroupElement g = MathUtils.getRandomGroupElement();
// Act:
final GroupElement h1 = g.add(neutral.toCached());
final GroupElement h2 = neutral.add(g.toCached());
// Assert:
Assert.assertThat(g, IsEqual.equalTo(h1));
Assert.assertThat(g, IsEqual.equalTo(h2));
}
}
@Test
public void addReturnsExpectedResult() {
for (int i=0; i<1000; i++) {
// Arrange:
final GroupElement g1 = MathUtils.getRandomGroupElement();
final GroupElement g2 = MathUtils.getRandomGroupElement();
// Act:
final GroupElement h1 = g1.add(g2.toCached());
final GroupElement h2 = MathUtils.addGroupElements(g1, g2);
// Assert:
Assert.assertThat(h2, IsEqual.equalTo(h1));
}
}
@Test
public void subReturnsExpectedResult() {
for (int i=0; i<1000; i++) {
// Arrange:
final GroupElement g1 = MathUtils.getRandomGroupElement();
final GroupElement g2 = MathUtils.getRandomGroupElement();
// Act:
final GroupElement h1 = g1.sub(g2.toCached());
final GroupElement h2 = MathUtils.addGroupElements(g1, MathUtils.negateGroupElement(g2));
// Assert:
Assert.assertThat(h2, IsEqual.equalTo(h1));
}
}
// region hashCode / equals
/**
* Test method for {@link GroupElement#equals(Object)}.
*/
@Test
public void testEqualsObject() {
assertThat(GroupElement.p2(curve, ZERO, ONE, ONE),
is(equalTo(P2_ZERO)));
}
@Test
public void equalsOnlyReturnsTrueForEquivalentObjects() {
// Arrange:
final GroupElement g1 = MathUtils.getRandomGroupElement();
final GroupElement g2 = MathUtils.toRepresentation(g1, GroupElement.Representation.P2);
final GroupElement g3 = MathUtils.toRepresentation(g1, GroupElement.Representation.CACHED);
final GroupElement g4 = MathUtils.toRepresentation(g1, GroupElement.Representation.P1P1);
final GroupElement g5 = MathUtils.getRandomGroupElement();
// Assert
Assert.assertThat(g2, IsEqual.equalTo(g1));
Assert.assertThat(g3, IsEqual.equalTo(g1));
Assert.assertThat(g1, IsEqual.equalTo(g4));
Assert.assertThat(g1, IsNot.not(IsEqual.equalTo(g5)));
Assert.assertThat(g2, IsNot.not(IsEqual.equalTo(g5)));
Assert.assertThat(g3, IsNot.not(IsEqual.equalTo(g5)));
Assert.assertThat(g5, IsNot.not(IsEqual.equalTo(g4)));
}
@Test
public void hashCodesAreEqualForEquivalentObjects() {
// Arrange:
final GroupElement g1 = MathUtils.getRandomGroupElement();
final GroupElement g2 = MathUtils.toRepresentation(g1, GroupElement.Representation.P2);
final GroupElement g3 = MathUtils.toRepresentation(g1, GroupElement.Representation.P1P1);
final GroupElement g4 = MathUtils.getRandomGroupElement();
// Assert
Assert.assertThat(g2.hashCode(), IsEqual.equalTo(g1.hashCode()));
Assert.assertThat(g3.hashCode(), IsEqual.equalTo(g1.hashCode()));
Assert.assertThat(g1.hashCode(), IsNot.not(IsEqual.equalTo(g4.hashCode())));
Assert.assertThat(g2.hashCode(), IsNot.not(IsEqual.equalTo(g4.hashCode())));
Assert.assertThat(g3.hashCode(), IsNot.not(IsEqual.equalTo(g4.hashCode())));
}
// endregion
static final byte[] BYTES_ZERO = Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
static final byte[] BYTES_ONE = Utils.hexToBytes("0100000000000000000000000000000000000000000000000000000000000000");
static final byte[] BYTES_42 = Utils.hexToBytes("2A00000000000000000000000000000000000000000000000000000000000000");
static final byte[] BYTES_1234567890 = Utils.hexToBytes("D202964900000000000000000000000000000000000000000000000000000000");
static final byte[] RADIX16_ZERO = Utils.hexToBytes("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
static final byte[] RADIX16_ONE = Utils.hexToBytes("01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
static final byte[] RADIX16_42 = Utils.hexToBytes("FA030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
/**
* Test method for {@link GroupElement#toRadix16(byte[])}.
*/
@Test
public void testToRadix16() {
assertThat(GroupElement.toRadix16(BYTES_ZERO), is(RADIX16_ZERO));
assertThat(GroupElement.toRadix16(BYTES_ONE), is(RADIX16_ONE));
assertThat(GroupElement.toRadix16(BYTES_42), is(RADIX16_42));
byte[] from1234567890 = GroupElement.toRadix16(BYTES_1234567890);
int total = 0;
for (int i = 0; i < from1234567890.length; i++) {
assertThat(from1234567890[i], is(greaterThanOrEqualTo((byte)-8)));
assertThat(from1234567890[i], is(lessThanOrEqualTo((byte)8)));
total += from1234567890[i] * Math.pow(16, i);
}
assertThat(total, is(1234567890));
byte[] pkrR16 = GroupElement.toRadix16(BYTES_PKR);
for (int i = 0; i < pkrR16.length; i++) {
assertThat(pkrR16[i], is(greaterThanOrEqualTo((byte)-8)));
assertThat(pkrR16[i], is(lessThanOrEqualTo((byte)8)));
}
}
/**
* Test method for {@link GroupElement#cmov(GroupElement, int)}.
*/
@Test
public void testCmov() {
GroupElement a = curve.getZero(GroupElement.Representation.PRECOMP);
GroupElement b = GroupElement.precomp(curve, TWO, ZERO, TEN);
assertThat(a.cmov(b, 0), is(equalTo(a)));
assertThat(a.cmov(b, 1), is(equalTo(b)));
}
/**
* Test method for {@link GroupElement#select(int, int)}.
*/
@Test
public void testSelect() {
GroupElement B = ed25519.getB();
for (int i = 0; i < 32; i++) {
// 16^i 0 B
assertThat(i + ",0", B.select(i, 0),
is(equalTo(GroupElement.precomp(curve, ONE, ONE, ZERO))));
for (int j = 1; j < 8; j++) {
// 16^i r_i B
GroupElement t = B.select(i, j);
assertThat(i + "," + j,
t, is(equalTo(B.precmp[i][j-1])));
// -16^i r_i B
t = B.select(i, -j);
GroupElement neg = GroupElement.precomp(curve,
B.precmp[i][j-1].Y,
B.precmp[i][j-1].X,
B.precmp[i][j-1].Z.negate());
assertThat(i + "," + -j,
t, is(equalTo(neg)));
}
}
}
// region scalar multiplication
/**
* Test method for {@link GroupElement#scalarMultiply(byte[])}.
* Test values generated with Python Ed25519 implementation.
*/
@Test
public void testScalarMultiplyByteArray() {
// Little-endian
byte[] zero = Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
byte[] one = Utils.hexToBytes("0100000000000000000000000000000000000000000000000000000000000000");
byte[] two = Utils.hexToBytes("0200000000000000000000000000000000000000000000000000000000000000");
byte[] a = Utils.hexToBytes("d072f8dd9c07fa7bc8d22a4b325d26301ee9202f6db89aa7c3731529e37e437c");
GroupElement A = new GroupElement(curve, Utils.hexToBytes("d4cf8595571830644bd14af416954d09ab7159751ad9e0f7a6cbd92379e71a66"));
assertThat("scalarMultiply(0) failed",
ed25519.getB().scalarMultiply(zero), is(equalTo(curve.getZero(GroupElement.Representation.P3))));
assertThat("scalarMultiply(1) failed",
ed25519.getB().scalarMultiply(one), is(equalTo(ed25519.getB())));
assertThat("scalarMultiply(2) failed",
ed25519.getB().scalarMultiply(two), is(equalTo(ed25519.getB().dbl())));
assertThat("scalarMultiply(a) failed",
ed25519.getB().scalarMultiply(a), is(equalTo(A)));
}
@Test
public void scalarMultiplyBasePointWithZeroReturnsNeutralElement() {
// Arrange:
final GroupElement basePoint = ed25519.getB();
// Act:
final GroupElement g = basePoint.scalarMultiply(curve.getField().ZERO.toByteArray());
// Assert:
Assert.assertThat(curve.getZero(GroupElement.Representation.P3), IsEqual.equalTo(g));
}
@Test
public void scalarMultiplyBasePointWithOneReturnsBasePoint() {
// Arrange:
final GroupElement basePoint = ed25519.getB();
// Act:
final GroupElement g = basePoint.scalarMultiply(curve.getField().ONE.toByteArray());
// Assert:
Assert.assertThat(basePoint, IsEqual.equalTo(g));
}
// This test is slow (~6s) due to math utils using an inferior algorithm to calculate the result.
@Test
public void scalarMultiplyBasePointReturnsExpectedResult() {
for (int i=0; i<10; i++) {
// Arrange:
final GroupElement basePoint = ed25519.getB();
final FieldElement f = MathUtils.getRandomFieldElement();
// Act:
final GroupElement g = basePoint.scalarMultiply(f.toByteArray());
final GroupElement h = MathUtils.scalarMultiplyGroupElement(basePoint, f);
// Assert:
Assert.assertThat(g, IsEqual.equalTo(h));
}
}
@Test
public void testDoubleScalarMultiplyVariableTime() {
// Little-endian
byte[] zero = Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
byte[] one = Utils.hexToBytes("0100000000000000000000000000000000000000000000000000000000000000");
byte[] two = Utils.hexToBytes("0200000000000000000000000000000000000000000000000000000000000000");
byte[] a = Utils.hexToBytes("d072f8dd9c07fa7bc8d22a4b325d26301ee9202f6db89aa7c3731529e37e437c");
GroupElement A = new GroupElement(curve, Utils.hexToBytes("d4cf8595571830644bd14af416954d09ab7159751ad9e0f7a6cbd92379e71a66"));
GroupElement B = ed25519.getB();
GroupElement geZero = curve.getZero(GroupElement.Representation.P3);
geZero.precompute(false);
// 0 * GE(0) + 0 * GE(0) = GE(0)
assertThat(geZero.doubleScalarMultiplyVariableTime(geZero, zero, zero),
is(equalTo(geZero)));
// 0 * GE(0) + 0 * B = GE(0)
assertThat(B.doubleScalarMultiplyVariableTime(geZero, zero, zero),
is(equalTo(geZero)));
// 1 * GE(0) + 0 * B = GE(0)
assertThat(B.doubleScalarMultiplyVariableTime(geZero, one, zero),
is(equalTo(geZero)));
// 1 * GE(0) + 1 * B = B
assertThat(B.doubleScalarMultiplyVariableTime(geZero, one, one),
is(equalTo(B)));
// 1 * B + 1 * B = 2 * B
assertThat(B.doubleScalarMultiplyVariableTime(B, one, one),
is(equalTo(B.dbl())));
// 1 * B + 2 * B = 3 * B
assertThat(B.doubleScalarMultiplyVariableTime(B, one, two),
is(equalTo(B.dbl().toP3().add(B.toCached()))));
// 2 * B + 2 * B = 4 * B
assertThat(B.doubleScalarMultiplyVariableTime(B, two, two),
is(equalTo(B.dbl().toP3().dbl())));
// 0 * B + a * B = A
assertThat(B.doubleScalarMultiplyVariableTime(B, zero, a),
is(equalTo(A)));
// a * B + 0 * B = A
assertThat(B.doubleScalarMultiplyVariableTime(B, a, zero),
is(equalTo(A)));
// a * B + a * B = 2 * A
assertThat(B.doubleScalarMultiplyVariableTime(B, a, a),
is(equalTo(A.dbl())));
}
// This test is slow (~6s) due to math utils using an inferior algorithm to calculate the result.
@Test
public void doubleScalarMultiplyVariableTimeReturnsExpectedResult() {
for (int i=0; i<10; i++) {
// Arrange:
final GroupElement basePoint = ed25519.getB();
final GroupElement g = MathUtils.getRandomGroupElement();
g.precompute(false);
final FieldElement f1 = MathUtils.getRandomFieldElement();
final FieldElement f2 = MathUtils.getRandomFieldElement();
// Act:
final GroupElement h1 = basePoint.doubleScalarMultiplyVariableTime(g, f2.toByteArray(), f1.toByteArray());
final GroupElement h2 = MathUtils.doubleScalarMultiplyGroupElements(basePoint, f1, g, f2);
// Assert:
Assert.assertThat(h1, IsEqual.equalTo(h2));
}
}
// endregion
/**
* Test method for {@link GroupElement#isOnCurve(Curve)}.
*/
@Test
public void testIsOnCurve() {
assertThat(P2_ZERO.isOnCurve(curve),
is(true));
assertThat(GroupElement.p2(curve, ZERO, ZERO, ONE).isOnCurve(curve),
is(false));
assertThat(GroupElement.p2(curve, ONE, ONE, ONE).isOnCurve(curve),
is(false));
assertThat(GroupElement.p2(curve, TEN, ZERO, ONE).isOnCurve(curve),
is(false));
assertThat(GroupElement.p2(curve, ONE, TEN, ONE).isOnCurve(curve),
is(false));
assertThat(GroupElement.p2(curve, PKR[0], PKR[1], ONE).isOnCurve(curve),
is(true));
}
@Test
public void isOnCurveReturnsTrueForPointsOnTheCurve() {
for (int i=0; i<100; i++) {
// Arrange:
final GroupElement g = MathUtils.getRandomGroupElement();
// Assert:
Assert.assertThat(g.isOnCurve(), IsEqual.equalTo(true));
}
}
@Test
public void isOnCurveReturnsFalseForPointsNotOnTheCurve() {
for (int i=0; i<100; i++) {
// Arrange:
final GroupElement g = MathUtils.getRandomGroupElement();
final GroupElement h = GroupElement.p2(curve, g.getX(), g.getY(), g.getZ().multiply(curve.getField().TWO));
// Assert (can only fail for 5*Z^2=1):
Assert.assertThat(h.isOnCurve(), IsEqual.equalTo(false));
}
}
}

@ -0,0 +1,472 @@
package org.xbib.io.sshd.eddsa.math;
import org.xbib.io.sshd.eddsa.Utils;
import org.xbib.io.sshd.eddsa.math.ed25519.Ed25519FieldElement;
import org.xbib.io.sshd.eddsa.math.ed25519.Ed25519LittleEndianEncoding;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveSpec;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveTable;
import org.hamcrest.core.IsEqual;
import org.junit.Assert;
import org.junit.Test;
import java.math.BigInteger;
import java.security.SecureRandom;
/**
* Utility class to help with calculations.
*/
public class MathUtils {
private static final int[] exponents = {0, 26, 26 + 25, 2*26 + 25, 2*26 + 2*25, 3*26 + 2*25, 3*26 + 3*25, 4*26 + 3*25, 4*26 + 4*25, 5*26 + 4*25};
private static final SecureRandom random = new SecureRandom();
private static final EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
private static final Curve curve = ed25519.getCurve();
private static final BigInteger d = new BigInteger("-121665").multiply(new BigInteger("121666").modInverse(getQ()));
private static final BigInteger groupOrder = BigInteger.ONE.shiftLeft(252).add(new BigInteger("27742317777372353535851937790883648493"));
/**
* Gets q = 2^255 - 19 as BigInteger.
*/
public static BigInteger getQ() {
return new BigInteger("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed", 16);
}
/**
* Gets group order = 2^252 + 27742317777372353535851937790883648493 as BigInteger.
*/
public static BigInteger getGroupOrder() {
return groupOrder;
}
/**
* Gets the underlying finite field with q=2^255 - 19 elements.
*
* @return The finite field.
*/
public static Field getField() {
return new Field(
256, // b
Utils.hexToBytes("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), // q
new Ed25519LittleEndianEncoding());
}
// region field element
/**
* Converts a 2^25.5 bit representation to a BigInteger.
* <p>
* Value: 2^exponents[0] * t[0] + 2^exponents[1] * t[1] + ... + 2^exponents[9] * t[9]
*
* @param t The 2^25.5 bit representation.
* @return The BigInteger.
*/
public static BigInteger toBigInteger(final int[] t) {
BigInteger b = BigInteger.ZERO;
for (int i=0; i<10; i++) {
b = b.add(BigInteger.ONE.multiply(BigInteger.valueOf(t[i])).shiftLeft(exponents[i]));
}
return b;
}
/**
* Converts a 2^8 bit representation to a BigInteger.
* <p>
* Value: bytes[0] + 2^8 * bytes[1] + ...
*
* @param bytes The 2^8 bit representation.
* @return The BigInteger.
*/
public static BigInteger toBigInteger(final byte[] bytes) {
BigInteger b = BigInteger.ZERO;
for (int i=0; i<bytes.length; i++) {
b = b.add(BigInteger.ONE.multiply(BigInteger.valueOf(bytes[i] & 0xff)).shiftLeft(i * 8));
}
return b;
}
/**
* Converts a field element to a BigInteger.
*
* @param f The field element.
* @return The BigInteger.
*/
public static BigInteger toBigInteger(final FieldElement f) {
return toBigInteger(f.toByteArray());
}
/**
* Converts a BigInteger to a field element.
*
* @param b The BigInteger.
* @return The field element.
*/
public static FieldElement toFieldElement(final BigInteger b) {
return getField().getEncoding().decode(toByteArray(b));
}
/**
* Converts a BigInteger to a little endian 32 byte representation.
*
* @param b The BigInteger.
* @return The 32 byte representation.
*/
public static byte[] toByteArray(final BigInteger b) {
if (b.compareTo(BigInteger.ONE.shiftLeft(256)) >= 0) {
throw new RuntimeException("only numbers < 2^256 are allowed");
}
final byte[] bytes = new byte[32];
final byte[] original = b.toByteArray();
// Although b < 2^256, original can have length > 32 with some bytes set to 0.
final int offset = original.length > 32? original.length - 32 : 0;
for (int i=0; i<original.length - offset; i++) {
bytes[original.length - i - offset - 1] = original[i + offset];
}
return bytes;
}
/**
* Reduces an integer in 2^8 bit representation modulo the group order and returns the result.
*
* @param bytes The integer in 2^8 bit representation.
* @return The mod group order reduced integer.
*/
public static byte[] reduceModGroupOrder(final byte[] bytes) {
final BigInteger b = toBigInteger(bytes).mod(groupOrder);
return toByteArray(b);
}
/**
* Calculates (a * b + c) mod group order and returns the result.
* <p>
* a, b and c are given in 2^8 bit representation.
*
* @param a The first integer.
* @param b The second integer.
* @param c The third integer.
* @return The mod group order reduced result.
*/
public static byte[] multiplyAndAddModGroupOrder(final byte[] a, final byte[] b, final byte[] c) {
final BigInteger result = toBigInteger(a).multiply(toBigInteger(b)).add(toBigInteger(c)).mod(groupOrder);
return toByteArray(result);
}
public static byte[] getRandomByteArray(final int length) {
final byte[] bytes = new byte[length];
random.nextBytes(bytes);
return bytes;
}
/**
* Gets a random field element where |t[i]| <= 2^24 for 0 <= i <= 9.
*
* @return The field element.
*/
public static FieldElement getRandomFieldElement() {
final int[] t = new int[10];
for (int j=0; j<10; j++) {
t[j] = random.nextInt(1 << 25) - (1 << 24);
}
return new Ed25519FieldElement(getField(), t);
}
// endregion
// region group element
/**
* Gets a random group element in P3 representation.
*
* @return The group element.
*/
public static GroupElement getRandomGroupElement() {
final byte[] bytes = new byte[32];
while (true) {
try {
random.nextBytes(bytes);
return new GroupElement(curve, bytes);
} catch (IllegalArgumentException e) {
// Will fail in about 87.5%, so try again.
}
}
}
/**
* Creates a group element from a byte array.
* <p>
* Bit 0 to 254 are the affine y-coordinate, bit 255 is the sign of the affine x-coordinate.
*
* @param bytes the byte array.
* @return The group element.
*/
public static GroupElement toGroupElement(final byte[] bytes) {
final boolean shouldBeNegative = (bytes[31] >> 7) != 0;
bytes[31] &= 0x7f;
final BigInteger y = MathUtils.toBigInteger(bytes);
// x = sign(x) * sqrt((y^2 - 1) / (d * y^2 + 1))
final BigInteger u = y.multiply(y).subtract(BigInteger.ONE).mod(getQ());
final BigInteger v = d.multiply(y).multiply(y).add(BigInteger.ONE).mod(getQ());
final BigInteger tmp = u.multiply(v.pow(7)).modPow(BigInteger.ONE.shiftLeft(252).subtract(new BigInteger("3")), getQ()).mod(getQ());
BigInteger x = tmp.multiply(u).multiply(v.pow(3)).mod(getQ());
if (!v.multiply(x).multiply(x).subtract(u).mod(getQ()).equals(BigInteger.ZERO)) {
if (!v.multiply(x).multiply(x).add(u).mod(getQ()).equals(BigInteger.ZERO)) {
throw new IllegalArgumentException("not a valid GroupElement");
}
x = x.multiply(toBigInteger(curve.getI())).mod(getQ());
}
final boolean isNegative = x.mod(new BigInteger("2")).equals(BigInteger.ONE);
if ((shouldBeNegative && !isNegative) || (!shouldBeNegative && isNegative)) {
x = x.negate().mod(getQ());
}
return GroupElement.p3(curve, toFieldElement(x), toFieldElement(y), getField().ONE, toFieldElement(x.multiply(y).mod(getQ())));
}
/**
* Converts a group element from one representation to another.
* This method is a helper used to test various methods in GroupElement.
*
* @param g The group element.
* @param repr The desired representation.
* @return The same group element in the new representation.
*/
public static GroupElement toRepresentation(final GroupElement g, final GroupElement.Representation repr) {
BigInteger x;
BigInteger y;
final BigInteger gX = toBigInteger(g.getX().toByteArray());
final BigInteger gY = toBigInteger(g.getY().toByteArray());
final BigInteger gZ = toBigInteger(g.getZ().toByteArray());
final BigInteger gT = null == g.getT()? null : toBigInteger(g.getT().toByteArray());
// Switch to affine coordinates.
switch (g.getRepresentation()) {
case P2:
case P3:
x = gX.multiply(gZ.modInverse(getQ())).mod(getQ());
y = gY.multiply(gZ.modInverse(getQ())).mod(getQ());
break;
case P1P1:
x = gX.multiply(gZ.modInverse(getQ())).mod(getQ());
y = gY.multiply(gT.modInverse(getQ())).mod(getQ());
break;
case CACHED:
x = gX.subtract(gY).multiply(gZ.multiply(new BigInteger("2")).modInverse(getQ())).mod(getQ());
y = gX.add(gY).multiply(gZ.multiply(new BigInteger("2")).modInverse(getQ())).mod(getQ());
break;
case PRECOMP:
x = gX.subtract(gY).multiply(new BigInteger("2").modInverse(getQ())).mod(getQ());
y = gX.add(gY).multiply(new BigInteger("2").modInverse(getQ())).mod(getQ());
break;
default:
throw new UnsupportedOperationException();
}
// Now back to the desired representation.
switch (repr) {
case P2:
return GroupElement.p2(
curve,
toFieldElement(x),
toFieldElement(y),
getField().ONE);
case P3:
return GroupElement.p3(
curve,
toFieldElement(x),
toFieldElement(y),
getField().ONE,
toFieldElement(x.multiply(y).mod(getQ())));
case P1P1:
return GroupElement.p1p1(
curve,
toFieldElement(x),
toFieldElement(y),
getField().ONE,
getField().ONE);
case CACHED:
return GroupElement.cached(
curve,
toFieldElement(y.add(x).mod(getQ())),
toFieldElement(y.subtract(x).mod(getQ())),
getField().ONE,
toFieldElement(d.multiply(new BigInteger("2")).multiply(x).multiply(y).mod(getQ())));
case PRECOMP:
return GroupElement.precomp(
curve,
toFieldElement(y.add(x).mod(getQ())),
toFieldElement(y.subtract(x).mod(getQ())),
toFieldElement(d.multiply(new BigInteger("2")).multiply(x).multiply(y).mod(getQ())));
default:
throw new UnsupportedOperationException();
}
}
/**
* Adds two group elements and returns the result in P3 representation.
* It uses BigInteger arithmetic and the affine representation.
* This method is a helper used to test the projective group addition formulas in GroupElement.
*
* @param g1 The first group element.
* @param g2 The second group element.
* @return The result of the addition.
*/
public static GroupElement addGroupElements(final GroupElement g1, final GroupElement g2) {
// Relying on a special representation of the group elements.
if ((g1.getRepresentation() != GroupElement.Representation.P2 && g1.getRepresentation() != GroupElement.Representation.P3) ||
(g2.getRepresentation() != GroupElement.Representation.P2 && g2.getRepresentation() != GroupElement.Representation.P3)) {
throw new IllegalArgumentException("g1 and g2 must have representation P2 or P3");
}
// Projective coordinates
final BigInteger g1X = toBigInteger(g1.getX().toByteArray());
final BigInteger g1Y = toBigInteger(g1.getY().toByteArray());
final BigInteger g1Z = toBigInteger(g1.getZ().toByteArray());
final BigInteger g2X = toBigInteger(g2.getX().toByteArray());
final BigInteger g2Y = toBigInteger(g2.getY().toByteArray());
final BigInteger g2Z = toBigInteger(g2.getZ().toByteArray());
// Affine coordinates
final BigInteger g1x = g1X.multiply(g1Z.modInverse(getQ())).mod(getQ());
final BigInteger g1y = g1Y.multiply(g1Z.modInverse(getQ())).mod(getQ());
final BigInteger g2x = g2X.multiply(g2Z.modInverse(getQ())).mod(getQ());
final BigInteger g2y = g2Y.multiply(g2Z.modInverse(getQ())).mod(getQ());
// Addition formula for affine coordinates. The formula is complete in our case.
//
// (x3, y3) = (x1, y1) + (x2, y2) where
//
// x3 = (x1 * y2 + x2 * y1) / (1 + d * x1 * x2 * y1 * y2) and
// y3 = (x1 * x2 + y1 * y2) / (1 - d * x1 * x2 * y1 * y2) and
// d = -121665/121666
BigInteger dx1x2y1y2 = d.multiply(g1x).multiply(g2x).multiply(g1y).multiply(g2y).mod(getQ());
BigInteger x3 = g1x.multiply(g2y).add(g2x.multiply(g1y))
.multiply(BigInteger.ONE.add(dx1x2y1y2).modInverse(getQ())).mod(getQ());
BigInteger y3 = g1x.multiply(g2x).add(g1y.multiply(g2y))
.multiply(BigInteger.ONE.subtract(dx1x2y1y2).modInverse(getQ())).mod(getQ());
BigInteger t3 = x3.multiply(y3).mod(getQ());
return GroupElement.p3(g1.getCurve(), toFieldElement(x3), toFieldElement(y3), getField().ONE, toFieldElement(t3));
}
/**
* Doubles a group element and returns the result in P3 representation.
* It uses BigInteger arithmetic and the affine representation.
* This method is a helper used to test the projective group doubling formula in GroupElement.
*
* @param g The group element.
* @return g+g.
*/
public static GroupElement doubleGroupElement(final GroupElement g) {
return addGroupElements(g, g);
}
/**
* Scalar multiply the group element by the field element.
*
* @param g The group element.
* @param f The field element.
* @return The resulting group element.
*/
public static GroupElement scalarMultiplyGroupElement(final GroupElement g, final FieldElement f) {
final byte[] bytes = f.toByteArray();
GroupElement h = curve.getZero(GroupElement.Representation.P3);
for (int i=254; i>=0; i--) {
h = doubleGroupElement(h);
if (Utils.bit(bytes, i) == 1) {
h = addGroupElements(h, g);
}
}
return h;
}
/**
* Calculates f1 * g1 + f2 * g2.
*
* @param g1 The first group element.
* @param f1 The first multiplier.
* @param g2 The second group element.
* @param f2 The second multiplier.
* @return The resulting group element.
*/
public static GroupElement doubleScalarMultiplyGroupElements(
final GroupElement g1,
final FieldElement f1,
final GroupElement g2,
final FieldElement f2) {
final GroupElement h1 = scalarMultiplyGroupElement(g1, f1);
final GroupElement h2 = scalarMultiplyGroupElement(g2, f2);
return addGroupElements(h1, h2);
}
/**
* Negates a group element.
*
* @param g The group element.
* @return The negated group element.
*/
public static GroupElement negateGroupElement(final GroupElement g) {
if (g.getRepresentation() != GroupElement.Representation.P3) {
throw new IllegalArgumentException("g must have representation P3");
}
return GroupElement.p3(g.getCurve(), g.getX().negate(), g.getY(), g.getZ(), g.getT().negate());
}
// Start TODO BR: Remove when finished!
@Test
public void mathUtilsWorkAsExpected() {
final GroupElement neutral = GroupElement.p3(curve, curve.getField().ZERO, curve.getField().ONE, curve.getField().ONE, curve.getField().ZERO);
for (int i=0; i<1000; i++) {
final GroupElement g = getRandomGroupElement();
// Act:
final GroupElement h1 = addGroupElements(g, neutral);
final GroupElement h2 = addGroupElements(neutral, g);
// Assert:
Assert.assertThat(g, IsEqual.equalTo(h1));
Assert.assertThat(g, IsEqual.equalTo(h2));
}
for (int i=0; i<1000; i++) {
GroupElement g = getRandomGroupElement();
// P3 -> P2.
GroupElement h = toRepresentation(g, GroupElement.Representation.P2);
Assert.assertThat(h, IsEqual.equalTo(g));
// P3 -> P1P1.
h = toRepresentation(g, GroupElement.Representation.P1P1);
Assert.assertThat(g, IsEqual.equalTo(h));
// P3 -> CACHED.
h = toRepresentation(g, GroupElement.Representation.CACHED);
Assert.assertThat(h, IsEqual.equalTo(g));
// P3 -> P2 -> P3.
g = toRepresentation(g, GroupElement.Representation.P2);
h = toRepresentation(g, GroupElement.Representation.P3);
Assert.assertThat(g, IsEqual.equalTo(h));
// P3 -> P2 -> P1P1.
g = toRepresentation(g, GroupElement.Representation.P2);
h = toRepresentation(g, GroupElement.Representation.P1P1);
Assert.assertThat(g, IsEqual.equalTo(h));
}
for (int i=0; i<10; i++) {
// Arrange:
final GroupElement g = MathUtils.getRandomGroupElement();
// Act:
final GroupElement h = MathUtils.scalarMultiplyGroupElement(g, curve.getField().ZERO);
// Assert:
Assert.assertThat(curve.getZero(GroupElement.Representation.P3), IsEqual.equalTo(h));
}
}
// End TODO BR: Remove when finished!
}

@ -0,0 +1,103 @@
package org.xbib.io.sshd.eddsa.math;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.xbib.io.sshd.eddsa.Utils;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveSpec;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveTable;
/**
*
*/
public class PrecomputationTestVectors {
// Test files were generated using base.py and base2.py from ref10
// (by printing hex(x%q) instead of the radix-255 representation).
static GroupElement[][] testPrecmp = getPrecomputation("basePrecmp");
static GroupElement[] testDblPrecmp = getDoublePrecomputation("baseDblPrecmp");
public static GroupElement[][] getPrecomputation(String fileName) {
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
Curve curve = ed25519.getCurve();
Field field = curve.getField();
GroupElement[][] precmp = new GroupElement[32][8];
BufferedReader file = null;
int row = 0, col = 0;
try {
InputStream is = PrecomputationTestVectors.class.getResourceAsStream(fileName);
if (is == null)
throw new IOException("Resource not found: " + fileName);
file = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = file.readLine()) != null) {
if (line.equals(" },"))
col += 1;
else if (line.equals("},")) {
col = 0;
row += 1;
} else if (line.startsWith(" { ")) {
String ypxStr = line.substring(4, line.lastIndexOf(' '));
FieldElement ypx = field.fromByteArray(
Utils.hexToBytes(ypxStr));
line = file.readLine();
String ymxStr = line.substring(4, line.lastIndexOf(' '));
FieldElement ymx = field.fromByteArray(
Utils.hexToBytes(ymxStr));
line = file.readLine();
String xy2dStr = line.substring(4, line.lastIndexOf(' '));
FieldElement xy2d = field.fromByteArray(
Utils.hexToBytes(xy2dStr));
precmp[row][col] = GroupElement.precomp(curve,
ypx, ymx, xy2d);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (file != null) try { file.close(); } catch (IOException e) {}
}
return precmp;
}
public static GroupElement[] getDoublePrecomputation(String fileName) {
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
Curve curve = ed25519.getCurve();
Field field = curve.getField();
GroupElement[] dblPrecmp = new GroupElement[8];
BufferedReader file = null;
int row = 0;
try {
InputStream is = PrecomputationTestVectors.class.getResourceAsStream(fileName);
if (is == null)
throw new IOException("Resource not found: " + fileName);
file = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = file.readLine()) != null) {
if (line.equals(" },")) {
row += 1;
} else if (line.startsWith(" { ")) {
String ypxStr = line.substring(4, line.lastIndexOf(' '));
FieldElement ypx = field.fromByteArray(
Utils.hexToBytes(ypxStr));
line = file.readLine();
String ymxStr = line.substring(4, line.lastIndexOf(' '));
FieldElement ymx = field.fromByteArray(
Utils.hexToBytes(ymxStr));
line = file.readLine();
String xy2dStr = line.substring(4, line.lastIndexOf(' '));
FieldElement xy2d = field.fromByteArray(
Utils.hexToBytes(xy2dStr));
dblPrecmp[row] = GroupElement.precomp(curve,
ypx, ymx, xy2d);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (file != null) try { file.close(); } catch (IOException e) {}
}
return dblPrecmp;
}
}

@ -0,0 +1,104 @@
package org.xbib.io.sshd.eddsa.math.bigint;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.math.BigInteger;
import java.util.Random;
import org.xbib.io.sshd.eddsa.Utils;
import org.xbib.io.sshd.eddsa.math.Field;
import org.xbib.io.sshd.eddsa.math.FieldElement;
import org.xbib.io.sshd.eddsa.math.MathUtils;
import org.xbib.io.sshd.eddsa.math.AbstractFieldElementTest;
import org.junit.Test;
/**
*
*/
public class BigIntegerFieldElementTest extends AbstractFieldElementTest {
static final byte[] BYTES_ZERO = Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
static final byte[] BYTES_ONE = Utils.hexToBytes("0100000000000000000000000000000000000000000000000000000000000000");
static final byte[] BYTES_TEN = Utils.hexToBytes("0a00000000000000000000000000000000000000000000000000000000000000");
static final Field ed25519Field = new Field(
256, // b
Utils.hexToBytes("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), // q
new BigIntegerLittleEndianEncoding());
static final FieldElement ZERO = new BigIntegerFieldElement(ed25519Field, BigInteger.ZERO);
static final FieldElement ONE = new BigIntegerFieldElement(ed25519Field, BigInteger.ONE);
static final FieldElement TWO = new BigIntegerFieldElement(ed25519Field, BigInteger.valueOf(2));
protected FieldElement getRandomFieldElement() {
BigInteger r;
Random rnd = new Random();
do {
r = new BigInteger(255, rnd);
} while (r.compareTo(getQ()) >= 0);
return new BigIntegerFieldElement(ed25519Field, r);
}
protected BigInteger toBigInteger(FieldElement f) {
return ((BigIntegerFieldElement)f).bi;
}
protected BigInteger getQ() {
return MathUtils.getQ();
}
protected Field getField() {
return ed25519Field;
}
/**
* Test method for {@link BigIntegerFieldElement#BigIntegerFieldElement(Field, BigInteger)}.
*/
@Test
public void testFieldElementBigInteger() {
assertThat(new BigIntegerFieldElement(ed25519Field, BigInteger.ZERO).bi, is(BigInteger.ZERO));
assertThat(new BigIntegerFieldElement(ed25519Field, BigInteger.ONE).bi, is(BigInteger.ONE));
assertThat(new BigIntegerFieldElement(ed25519Field, BigInteger.valueOf(2)).bi, is(BigInteger.valueOf(2)));
}
/**
* Test method for {@link FieldElement#toByteArray()}.
*/
@Test
public void testToByteArray() {
byte[] zero = ZERO.toByteArray();
assertThat(zero.length, is(equalTo(BYTES_ZERO.length)));
assertThat(zero, is(equalTo(BYTES_ZERO)));
byte[] one = ONE.toByteArray();
assertThat(one.length, is(equalTo(BYTES_ONE.length)));
assertThat(one, is(equalTo(BYTES_ONE)));
byte[] ten = new BigIntegerFieldElement(ed25519Field, BigInteger.TEN).toByteArray();
assertThat(ten.length, is(equalTo(BYTES_TEN.length)));
assertThat(ten, is(equalTo(BYTES_TEN)));
}
// region isNonZero
protected FieldElement getZeroFieldElement() {
return ZERO;
}
protected FieldElement getNonZeroFieldElement() {
return TWO;
}
// endregion
/**
* Test method for {@link FieldElement#equals(Object)}.
*/
@Test
public void testEqualsObject() {
assertThat(new BigIntegerFieldElement(ed25519Field, BigInteger.ZERO), is(equalTo(ZERO)));
assertThat(new BigIntegerFieldElement(ed25519Field, BigInteger.valueOf(1000)), is(equalTo(new BigIntegerFieldElement(ed25519Field, BigInteger.valueOf(1000)))));
assertThat(ONE, is(not(equalTo(TWO))));
}
}

@ -0,0 +1,61 @@
package org.xbib.io.sshd.eddsa.math.bigint;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.math.BigInteger;
import org.xbib.io.sshd.eddsa.Utils;
import org.xbib.io.sshd.eddsa.math.Field;
import org.xbib.io.sshd.eddsa.math.ScalarOps;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveSpec;
import org.xbib.io.sshd.eddsa.spec.EdDSANamedCurveTable;
import org.junit.Test;
/**
*
*/
public class BigIntegerScalarOpsTest {
static final EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
static final Field ed25519Field = ed25519.getCurve().getField();
/**
* Test method for {@link BigIntegerScalarOps#reduce(byte[])}.
*/
@Test
public void testReduce() {
ScalarOps sc = new BigIntegerScalarOps(ed25519Field,
new BigInteger("5"));
assertThat(sc.reduce(new byte[] {7}),
is(equalTo(Utils.hexToBytes("0200000000000000000000000000000000000000000000000000000000000000"))));
ScalarOps sc2 = new BigIntegerScalarOps(ed25519Field,
new BigInteger("7237005577332262213973186563042994240857116359379907606001950938285454250989"));
// Example from test case 1
byte[] r = Utils.hexToBytes("b6b19cd8e0426f5983fa112d89a143aa97dab8bc5deb8d5b6253c928b65272f4044098c2a990039cde5b6a4818df0bfb6e40dc5dee54248032962323e701352d");
assertThat(sc2.reduce(r), is(equalTo(Utils.hexToBytes("f38907308c893deaf244787db4af53682249107418afc2edc58f75ac58a07404"))));
}
/**
* Test method for {@link BigIntegerScalarOps#multiplyAndAdd(byte[], byte[], byte[])}.
*/
@Test
public void testMultiplyAndAdd() {
ScalarOps sc = new BigIntegerScalarOps(ed25519Field,
new BigInteger("5"));
assertThat(sc.multiplyAndAdd(new byte[] {7}, new byte[] {2}, new byte[] {5}),
is(equalTo(Utils.hexToBytes("0400000000000000000000000000000000000000000000000000000000000000"))));
ScalarOps sc2 = new BigIntegerScalarOps(ed25519Field,
new BigInteger("7237005577332262213973186563042994240857116359379907606001950938285454250989"));
// Example from test case 1
byte[] h = Utils.hexToBytes("86eabc8e4c96193d290504e7c600df6cf8d8256131ec2c138a3e7e162e525404");
byte[] a = Utils.hexToBytes("307c83864f2833cb427a2ef1c00a013cfdff2768d980c0a3a520f006904de94f");
byte[] r = Utils.hexToBytes("f38907308c893deaf244787db4af53682249107418afc2edc58f75ac58a07404");
byte[] S = Utils.hexToBytes("5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b");
assertThat(sc2.multiplyAndAdd(h, a, r), is(equalTo(S)));
}
}

@ -0,0 +1,78 @@
package org.xbib.io.sshd.eddsa.math.ed25519;
import org.xbib.io.sshd.eddsa.math.AbstractFieldElementTest;
import org.xbib.io.sshd.eddsa.math.Field;
import org.xbib.io.sshd.eddsa.math.FieldElement;
import org.xbib.io.sshd.eddsa.math.MathUtils;
import org.hamcrest.core.IsEqual;
import org.junit.Assert;
import org.junit.Test;
import java.math.BigInteger;
/**
* Tests rely on the BigInteger class.
*/
public class Ed25519FieldElementTest extends AbstractFieldElementTest {
protected FieldElement getRandomFieldElement() {
return MathUtils.getRandomFieldElement();
}
protected BigInteger toBigInteger(FieldElement f) {
return MathUtils.toBigInteger(f);
}
protected BigInteger getQ() {
return MathUtils.getQ();
}
protected Field getField() {
return MathUtils.getField();
}
@Test
public void canConstructFieldElementFromArrayWithCorrectLength() {
new Ed25519FieldElement(MathUtils.getField(), new int[10]);
}
@Test (expected = IllegalArgumentException.class)
public void cannotConstructFieldElementFromArrayWithIncorrectLength() {
new Ed25519FieldElement(MathUtils.getField(), new int[9]);
}
@Test (expected = IllegalArgumentException.class)
public void cannotConstructFieldElementWithoutField() {
new Ed25519FieldElement(null, new int[9]);
}
protected FieldElement getZeroFieldElement() {
return new Ed25519FieldElement(MathUtils.getField(), new int[10]);
}
protected FieldElement getNonZeroFieldElement() {
final int[] t = new int[10];
t[0] = 5;
return new Ed25519FieldElement(MathUtils.getField(), t);
}
@Test
public void toStringReturnsCorrectRepresentation() {
final byte[] bytes = new byte[32];
for (int i=0; i<32; i++) {
bytes[i] = (byte)(i+1);
}
final FieldElement f = MathUtils.getField().getEncoding().decode(bytes);
final String fAsString = f.toString();
final StringBuilder builder = new StringBuilder();
builder.append("[Ed25519FieldElement val=");
for (byte b : bytes) {
builder.append(String.format("%02x", b));
}
builder.append("]");
Assert.assertThat(fAsString, IsEqual.equalTo(builder.toString()));
}
}

@ -0,0 +1,107 @@
package org.xbib.io.sshd.eddsa.math.ed25519;
import org.xbib.io.sshd.eddsa.math.FieldElement;
import org.xbib.io.sshd.eddsa.math.MathUtils;
import org.hamcrest.core.IsEqual;
import org.junit.Assert;
import org.junit.Test;
import java.math.BigInteger;
import java.security.SecureRandom;
/**
* Tests rely on the BigInteger class.
*/
public class Ed25519LittleEndianEncodingTest {
private static final SecureRandom random = new SecureRandom();
@Test
public void encodeReturnsCorrectByteArrayForSimpleFieldElements() {
// Arrange:
final int[] t1 = new int[10];
final int[] t2 = new int[10];
t2[0] = 1;
final FieldElement fieldElement1 = new Ed25519FieldElement(MathUtils.getField(), t1);
final FieldElement fieldElement2 = new Ed25519FieldElement(MathUtils.getField(), t2);
// Act:
final byte[] bytes1 = MathUtils.getField().getEncoding().encode(fieldElement1);
final byte[] bytes2 = MathUtils.getField().getEncoding().encode(fieldElement2);
// Assert:
Assert.assertThat(bytes1, IsEqual.equalTo(MathUtils.toByteArray(BigInteger.ZERO)));
Assert.assertThat(bytes2, IsEqual.equalTo(MathUtils.toByteArray(BigInteger.ONE)));
}
@Test
public void encodeReturnsCorrectByteArray() {
for (int i=0; i<10000; i++){
// Arrange:
final int[] t = new int[10];
for (int j=0; j<10; j++) {
t[j] = random.nextInt(1 << 28) - (1 << 27);
}
final FieldElement fieldElement1 = new Ed25519FieldElement(MathUtils.getField(), t);
final BigInteger b = MathUtils.toBigInteger(t);
// Act:
final byte[] bytes = MathUtils.getField().getEncoding().encode(fieldElement1);
// Assert:
Assert.assertThat(bytes, IsEqual.equalTo(MathUtils.toByteArray(b.mod(MathUtils.getQ()))));
}
}
@Test
public void decodeReturnsCorrectFieldElementForSimpleByteArrays() {
// Arrange:
final byte[] bytes1 = new byte[32];
final byte[] bytes2 = new byte[32];
bytes2[0] = 1;
// Act:
final Ed25519FieldElement f1 = (Ed25519FieldElement)MathUtils.getField().getEncoding().decode(bytes1);
final Ed25519FieldElement f2 = (Ed25519FieldElement)MathUtils.getField().getEncoding().decode(bytes2);
final BigInteger b1 = MathUtils.toBigInteger(f1.t);
final BigInteger b2 = MathUtils.toBigInteger(f2.t);
// Assert:
Assert.assertThat(b1, IsEqual.equalTo(BigInteger.ZERO));
Assert.assertThat(b2, IsEqual.equalTo(BigInteger.ONE));
}
@Test
public void decodeReturnsCorrectFieldElement() {
for (int i=0; i<10000; i++) {
// Arrange:
final byte[] bytes = new byte[32];
random.nextBytes(bytes);
bytes[31] = (byte)(bytes[31] & 0x7f);
final BigInteger b1 = MathUtils.toBigInteger(bytes);
// Act:
final Ed25519FieldElement f = (Ed25519FieldElement)MathUtils.getField().getEncoding().decode(bytes);
final BigInteger b2 = MathUtils.toBigInteger(f.t).mod(MathUtils.getQ());
// Assert:
Assert.assertThat(b2, IsEqual.equalTo(b1));
}
}
@Test
public void isNegativeReturnsCorrectResult() {
for (int i=0; i<10000; i++) {
// Arrange:
final int[] t = new int[10];
for (int j=0; j<10; j++) {
t[j] = random.nextInt(1 << 28) - (1 << 27);
}
final boolean isNegative = MathUtils.toBigInteger(t).mod(MathUtils.getQ()).mod(new BigInteger("2")).equals(BigInteger.ONE);
final FieldElement f = new Ed25519FieldElement(MathUtils.getField(), t);
// Assert:
Assert.assertThat(MathUtils.getField().getEncoding().isNegative(f), IsEqual.equalTo(isNegative));
}
}
}

@ -0,0 +1,72 @@
package org.xbib.io.sshd.eddsa.math.ed25519;
import org.xbib.io.sshd.eddsa.Utils;
import org.xbib.io.sshd.eddsa.math.MathUtils;
import org.hamcrest.core.IsEqual;
import org.junit.Assert;
import org.junit.Test;
import java.math.BigInteger;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
/**
* Additional tests by the NEM project team.
*
*/
public class Ed25519ScalarOpsTest {
private static final Ed25519ScalarOps scalarOps = new Ed25519ScalarOps();
/**
* Test method for {@link org.xbib.io.sshd.eddsa.math.bigint.BigIntegerScalarOps#reduce(byte[])}.
*/
@Test
public void testReduce() {
// Example from test case 1
byte[] r = Utils.hexToBytes("b6b19cd8e0426f5983fa112d89a143aa97dab8bc5deb8d5b6253c928b65272f4044098c2a990039cde5b6a4818df0bfb6e40dc5dee54248032962323e701352d");
assertThat(scalarOps.reduce(r), is(equalTo(Utils.hexToBytes("f38907308c893deaf244787db4af53682249107418afc2edc58f75ac58a07404"))));
}
@Test
public void reduceReturnsExpectedResult() {
for (int i=0; i<1000; i++) {
final byte[] bytes = MathUtils.getRandomByteArray(64);
final byte[] reduced1 = scalarOps.reduce(bytes);
final byte[] reduced2 = MathUtils.reduceModGroupOrder(bytes);
Assert.assertThat(MathUtils.toBigInteger(reduced1).compareTo(MathUtils.getGroupOrder()), IsEqual.equalTo(-1));
Assert.assertThat(MathUtils.toBigInteger(reduced1).compareTo(new BigInteger("-1")), IsEqual.equalTo(1));
Assert.assertThat(reduced1, IsEqual.equalTo(reduced2));
}
}
/**
* Test method for {@link org.xbib.io.sshd.eddsa.math.bigint.BigIntegerScalarOps#multiplyAndAdd(byte[], byte[], byte[])}.
*/
@Test
public void testMultiplyAndAdd() {
byte[] h = Utils.hexToBytes("86eabc8e4c96193d290504e7c600df6cf8d8256131ec2c138a3e7e162e525404");
byte[] a = Utils.hexToBytes("307c83864f2833cb427a2ef1c00a013cfdff2768d980c0a3a520f006904de94f");
byte[] r = Utils.hexToBytes("f38907308c893deaf244787db4af53682249107418afc2edc58f75ac58a07404");
byte[] S = Utils.hexToBytes("5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b");
assertThat(scalarOps.multiplyAndAdd(h, a, r), is(equalTo(S)));
}
@Test
public void multiplyAndAddReturnsExpectedResult() {
for (int i=0; i<1000; i++) {
final byte[] bytes1 = MathUtils.getRandomByteArray(32);
final byte[] bytes2 = MathUtils.getRandomByteArray(32);
final byte[] bytes3 = MathUtils.getRandomByteArray(32);
final byte[] result1 = scalarOps.multiplyAndAdd(bytes1, bytes2, bytes3);
final byte[] result2 = MathUtils.multiplyAndAddModGroupOrder(bytes1, bytes2, bytes3);
Assert.assertThat(MathUtils.toBigInteger(result1).compareTo(MathUtils.getGroupOrder()), IsEqual.equalTo(-1));
Assert.assertThat(MathUtils.toBigInteger(result1).compareTo(new BigInteger("-1")), IsEqual.equalTo(1));
Assert.assertThat(result1, IsEqual.equalTo(result2));
}
}
}

@ -0,0 +1,24 @@
package org.xbib.io.sshd.eddsa.spec;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
/**
*
*/
public class EdDSANamedCurveTableTest {
/**
* Ensure curve names are case-inspecific
*/
@Test
public void curveNamesAreCaseInspecific() {
EdDSANamedCurveSpec mixed = EdDSANamedCurveTable.getByName("Ed25519");
EdDSANamedCurveSpec lower = EdDSANamedCurveTable.getByName("ed25519");
EdDSANamedCurveSpec upper = EdDSANamedCurveTable.getByName("ED25519");
assertThat(lower, is(equalTo(mixed)));
assertThat(upper, is(equalTo(mixed)));
}
}

@ -0,0 +1,59 @@
package org.xbib.io.sshd.eddsa.spec;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.xbib.io.sshd.eddsa.Utils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
/**
*
*/
public class EdDSAPrivateKeySpecTest {
static final byte[] ZERO_SEED = Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
static final byte[] ZERO_H = Utils.hexToBytes("5046adc1dba838867b2bbbfdd0c3423e58b57970b5267a90f57960924a87f1960a6a85eaa642dac835424b5d7c8d637c00408c7a73da672b7f498521420b6dd3");
static final byte[] ZERO_PK = Utils.hexToBytes("3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29");
static final EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
@Rule
public ExpectedException exception = ExpectedException.none();
/**
* Test method for {@link EdDSAPrivateKeySpec#EdDSAPrivateKeySpec(byte[], EdDSAParameterSpec)}.
*/
@Test
public void testEdDSAPrivateKeySpecFromSeed() {
EdDSAPrivateKeySpec key = new EdDSAPrivateKeySpec(ZERO_SEED, ed25519);
assertThat(key.getSeed(), is(equalTo(ZERO_SEED)));
assertThat(key.getH(), is(equalTo(ZERO_H)));
assertThat(key.getA().toByteArray(), is(equalTo(ZERO_PK)));
}
@Test
public void incorrectSeedLengthThrows() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("seed length is wrong");
EdDSAPrivateKeySpec key = new EdDSAPrivateKeySpec(new byte[2], ed25519);
}
/**
* Test method for {@link EdDSAPrivateKeySpec#EdDSAPrivateKeySpec(EdDSAParameterSpec, byte[])}.
*/
@Test
public void testEdDSAPrivateKeySpecFromH() {
EdDSAPrivateKeySpec key = new EdDSAPrivateKeySpec(ed25519, ZERO_H);
assertThat(key.getSeed(), is(nullValue()));
assertThat(key.getH(), is(equalTo(ZERO_H)));
assertThat(key.getA().toByteArray(), is(equalTo(ZERO_PK)));
}
@Test
public void incorrectHashLengthThrows() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("hash length is wrong");
EdDSAPrivateKeySpec key = new EdDSAPrivateKeySpec(ed25519, new byte[2]);
}
}

@ -0,0 +1,40 @@
{
{ 853b8cf5c693bc2f190e8cfbc62d93cfc2423d6498480b2765bad4333a9dcf07 },
{ 3e9140d70539109db3be40d1059f39fd098a8f683484c1a56712f898922ffd44 },
{ 68aa7a870512c9ab9ec4aacc23e8d9268c5943ddcb7d1b5aa8650c9f687b116f },
},
{
{ 3097ee4ca8b025af8a4b86e830845a023267019f02501bc1f4f8809a1b4e167a },
{ 65d2fca4e81f61567dbac1e5fd53d33bbdd64b211af3318162da5b558715b92a },
{ 89d8d00d3f93ae1462da351c222394584cdbf28c45e570d1c6b4b912af26285a },
},
{
{ 33bba50844bc12a202ed5ec7c348508d44ecbf5a0ceb1bddeb06e246f1cc4529 },
{ bad647a4c382917fb729274bd11400d587a064b81cf13ce3f3551beb737e4a15 },
{ 85822a81f1dbbbbcfcd1bdd007080e272da7bd1b0b671bb49ab63b6b69beaa43 },
},
{
{ bfa34e94d05c1a6bd2c09db33a357074492e54288252b2717e923c2869ea1b46 },
{ b12132aa9a2c6fbaa723ba3b5321a06c3a2c19924f76ea9de017532e5ddd6e1d },
{ a2b3b801c86d83f19aa43e05475f03b3f3ad7758ba419c52a7900f6a1cbb9f7a },
},
{
{ 2f63a8a68a672e9bc546bc516f9e50a6b5f586c6c933b2ce597fdd8a33edb934 },
{ 64809d037e216ef39b4120f5b681a09844b05ee708c6cb968f9cdcfa515ac049 },
{ 1baf4590bfe8b4062fd219a7e883ffe216cfd49329fcf6aa068b001b0272c173 },
},
{
{ de2a808a8400bf2f272e3002cffed9e50634701771843e11af8f6d54e2aa7542 },
{ 48438649025b5f318183087769b3d63e95eb8d6a5575a0a37fc7d5298059ab18 },
{ e98960fdc52c2bd8a4e48232a1b41e0322861ab59911314448f93db52255c63d },
},
{
{ 6d7f00a222c270bfdbdebcb59ab384bf07ba07fb120e7a5341f246c3eed74f23 },
{ 93bf7f323b016f506b6f779bc9ebfcae6859adaa32b2129da72460172d886702 },
{ 78a32e7319a1605371d48ddfb1e6372433e5a791f837efa2637809aafda67b49 },
},
{
{ a0eacf1303ccce246d249c188dc24886d0d4f2c1fabdbd2d2be72df11729e261 },
{ 0bcf8c4686cd0b04d610992aa49b82d39251b20708300875bf5ed01842cdb543 },
{ 16b5d09b2f769a5deede3f374eaf38eb7042d6937d5a2e0342d8e40a21611d51 },
},

@ -0,0 +1,6 @@
The ftp-fs is derived from
https://github.com/robtimus/ftp-fs/
Copyright by Rob Spoor
Licensed under the Apache Software License 2.0

@ -0,0 +1,7 @@
dependencies {
api project(':files-ftp')
testImplementation "org.mockftpserver:MockFtpServer:${project.property('mockftpserver.version')}"
testImplementation "org.junit.jupiter:junit-jupiter-params:${project.property('junit.version')}"
testImplementation "org.mockito:mockito-core:${project.property('mockito.version')}"
testImplementation "org.mockito:mockito-junit-jupiter:${project.property('mockito.version')}"
}

@ -0,0 +1,5 @@
module org.xbib.files.ftp.fs {
requires org.xbib.files.ftp;
provides java.nio.file.spi.FileSystemProvider
with org.xbib.io.ftp.fs.FTPFileSystemProvider;
}

@ -0,0 +1,132 @@
package org.xbib.io.ftp.fs;
import java.io.IOException;
import java.nio.file.DirectoryIteratorException;
import java.nio.file.DirectoryStream;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* This class provides a skeletal implementation of the {@link DirectoryStream} interface to minimize the effort
* required to implement this interface.
* It will take care of ending iteration when the stream is closed, and making sure that {@link #iterator()}
* is only called once.
* Subclasses often only need to implement {@link #getNext()}. Optionally, if they need perform setup steps before
* iteration, they should override
* {@link #setupIteration()} as well.
*
* @param <T> The type of element returned by the iterator.
*/
public abstract class AbstractDirectoryStream<T> implements DirectoryStream<T> {
private final Filter<? super T> filter;
private boolean open = true;
private Iterator<T> iterator = null;
/**
* Creates a new {@code DirectoryStream}.
*
* @param filter The optional filter to use.
*/
public AbstractDirectoryStream(Filter<? super T> filter) {
this.filter = filter;
}
@Override
public synchronized void close() throws IOException {
open = false;
}
private synchronized boolean isOpen() {
return open;
}
@Override
public synchronized Iterator<T> iterator() {
if (!open) {
throw Messages.directoryStream().closed();
}
if (iterator != null) {
throw Messages.directoryStream().iteratorAlreadyReturned();
}
iterator = new Iterator<T>() {
private T next = null;
private State state = State.UNSPECIFIED;
@Override
public boolean hasNext() {
if (state == State.UNSPECIFIED) {
next = getNextElement();
state = next != null ? State.ACTIVE : State.ENDED;
}
return state == State.ACTIVE;
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
T result = next;
next = null;
state = State.UNSPECIFIED;
return result;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
setupIteration();
return iterator;
}
private T getNextElement() {
while (isOpen()) {
try {
T next = getNext();
if (next == null) {
return null;
}
if (filter == null || filter.accept(next)) {
return next;
}
} catch (IOException e) {
throw new DirectoryIteratorException(e);
}
}
return null;
}
/**
* Performs the necessary steps to setup iteration. The default implementation does nothing.
*/
protected void setupIteration() {
// does nothing
}
/**
* Returns the next element in iteration.
*
* @return The next element in iteration, or {@code null} if there is no more next element.
* @throws IOException If the next element could not be retrieved.
*/
protected abstract T getNext() throws IOException;
private enum State {
/**
* Indicates a lookahead iterator is still active (i.e. there is a next element).
*/
ACTIVE,
/**
* Indicates a lookahead iterator has ended (i.e. there is no next element).
*/
ENDED,
/**
* Indicates it's not known whether or not a lookahead iterator has a next element or not.
*/
UNSPECIFIED,
}
}

@ -0,0 +1,151 @@
package org.xbib.io.ftp.fs;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;
/**
* This class provides a skeletal implementation of the {@link Path} interface to minimize the effort
* required to implement this interface.
*/
public abstract class AbstractPath implements Path {
private static final WatchEvent.Modifier[] NO_MODIFIERS = {};
/**
* Returns the name of the file or directory denoted by this path as a {@code Path} object.
* <p>
* This implementation returns {@link #getName(int) getName(i)}, where {@code i} is equal to {@link #getNameCount()}{@code - 1}.
* If {@code getNameCount()} returns {@code 0} this method returns {@code null}.
*/
@Override
public Path getFileName() {
int nameCount = getNameCount();
return nameCount == 0 ? null : getName(nameCount - 1);
}
/**
* Returns a name element of this path as a {@code Path} object.
* <p>
* This implementation calls {@link #subpath(int, int) subpath(index, index + 1)}.
*/
@Override
public Path getName(int index) {
return subpath(index, index + 1);
}
/**
* Tests if this path starts with a {@code Path}, constructed by converting the given path string.
* <p>
* This implementation uses this path's {@link #getFileSystem() FileSystem} to {@link FileSystem#getPath(String, String...) convert} the given
* string into a {@code Path}, then calls {@link #startsWith(Path)}.
*/
@Override
public boolean startsWith(String other) {
return startsWith(getFileSystem().getPath(other));
}
/**
* Tests if this path ends with a {@code Path}, constructed by converting the given path string.
* <p>
* This implementation uses this path's {@link #getFileSystem() FileSystem} to {@link FileSystem#getPath(String, String...) convert} the given
* string into a {@code Path}, then calls {@link #endsWith(Path)}.
*/
@Override
public boolean endsWith(String other) {
return endsWith(getFileSystem().getPath(other));
}
/**
* Converts a given path string to a {@code Path} and resolves it against this {@code Path}.
* <p>
* This implementation uses this path's {@link #getFileSystem() FileSystem} to {@link FileSystem#getPath(String, String...) convert} the given
* string into a {@code Path}, then calls {@link #resolve(Path)}.
*/
@Override
public Path resolve(String other) {
return resolve(getFileSystem().getPath(other));
}
/**
* Converts a given path string to a {@code Path} and resolves it against this path's {@link #getParent parent} path.
* <p>
* This implementation uses this path's {@link #getFileSystem() FileSystem} to {@link FileSystem#getPath(String, String...) convert} the given
* string into a {@code Path}, then calls {@link #resolveSibling(Path)}.
*/
@Override
public Path resolveSibling(String other) {
return resolveSibling(getFileSystem().getPath(other));
}
/**
* Resolves the given path against this path's {@link #getParent parent} path.
* <p>
* This implementation returns {@code getParent().}{@link Path#resolve(Path) resolve(other)}, or {@code other} if this path has no parent.
*/
@Override
public Path resolveSibling(Path other) {
Objects.requireNonNull(other);
Path parent = getParent();
return parent == null ? other : parent.resolve(other);
}
/**
* Returns a {@link File} object representing this path.
* <p>
* This implementation will always throw an {@link UnsupportedOperationException} as per the contract of {@link Path#toFile()}.
*/
@Override
public File toFile() {
throw Messages.unsupportedOperation(Path.class, "toFile");
}
/**
* Registers the file located by this path with a watch service.
*/
@Override
public WatchKey register(WatchService watcher, Kind<?>... events) throws IOException {
return register(watcher, events, NO_MODIFIERS);
}
/**
* Returns an iterator over the name elements of this path.
* <p>
* This implementation returns an iterator that uses {@link #getNameCount()} to determine whether or not there are more elements,
* and {@link #getName(int)} to return the elements.
*/
@Override
public Iterator<Path> iterator() {
return new Iterator<Path>() {
private int index = 0;
@Override
public boolean hasNext() {
return index < getNameCount();
}
@Override
public Path next() {
if (hasNext()) {
Path result = getName(index);
index++;
return result;
}
throw new NoSuchElementException();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}

@ -0,0 +1,29 @@
package org.xbib.io.ftp.fs;
import org.xbib.io.ftp.client.FTPClient;
/**
* The possible FTP connection modes. Note that server-to-server is not supported.
*/
public enum ConnectionMode {
/**
* Indicates that FTP servers should connect to clients' data ports to initiate data transfers.
*/
ACTIVE {
@Override
void apply(FTPClient client) {
client.enterLocalActiveMode();
}
},
/**
* Indicates that FTP servers are in passive mode, requiring clients to connect to the servers' data ports to initiate transfers.
*/
PASSIVE {
@Override
void apply(FTPClient client) {
client.enterLocalPassiveMode();
}
},;
abstract void apply(FTPClient client);
}

@ -0,0 +1,101 @@
package org.xbib.io.ftp.fs;
import java.nio.file.CopyOption;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* A representation of possible copy options.
*/
final class CopyOptions extends TransferOptions {
public final boolean replaceExisting;
public final Collection<? extends CopyOption> options;
private CopyOptions(boolean replaceExisting,
FileType fileType, FileStructure fileStructure, FileTransferMode fileTransferMode,
Collection<? extends CopyOption> options) {
super(fileType, fileStructure, fileTransferMode);
this.replaceExisting = replaceExisting;
this.options = options;
}
static CopyOptions forCopy(CopyOption... options) {
boolean replaceExisting = false;
FileType fileType = null;
FileStructure fileStructure = null;
FileTransferMode fileTransferMode = null;
for (CopyOption option : options) {
if (option == StandardCopyOption.REPLACE_EXISTING) {
replaceExisting = true;
} else if (option instanceof FileType) {
fileType = setOnce((FileType) option, fileType, options);
} else if (option instanceof FileStructure) {
fileStructure = setOnce((FileStructure) option, fileStructure, options);
} else if (option instanceof FileTransferMode) {
fileTransferMode = setOnce((FileTransferMode) option, fileTransferMode, options);
} else if (!isIgnoredCopyOption(option)) {
throw Messages.fileSystemProvider().unsupportedCopyOption(option);
}
}
return new CopyOptions(replaceExisting, fileType, fileStructure, fileTransferMode, Arrays.asList(options));
}
static CopyOptions forMove(boolean sameFileSystem, CopyOption... options) {
boolean replaceExisting = false;
FileType fileType = null;
FileStructure fileStructure = null;
FileTransferMode fileTransferMode = null;
for (CopyOption option : options) {
if (option == StandardCopyOption.REPLACE_EXISTING) {
replaceExisting = true;
} else if (option instanceof FileType) {
fileType = setOnce((FileType) option, fileType, options);
} else if (option instanceof FileStructure) {
fileStructure = setOnce((FileStructure) option, fileStructure, options);
} else if (option instanceof FileTransferMode) {
fileTransferMode = setOnce((FileTransferMode) option, fileTransferMode, options);
} else if (!(option == StandardCopyOption.ATOMIC_MOVE && sameFileSystem) && !isIgnoredCopyOption(option)) {
throw Messages.fileSystemProvider().unsupportedCopyOption(option);
}
}
return new CopyOptions(replaceExisting, fileType, fileStructure, fileTransferMode, Arrays.asList(options));
}
private static <T> T setOnce(T newValue, T existing, CopyOption... options) {
if (existing != null && !existing.equals(newValue)) {
throw Messages.fileSystemProvider().illegalCopyOptionCombination(options);
}
return newValue;
}
private static boolean isIgnoredCopyOption(CopyOption option) {
return option == LinkOption.NOFOLLOW_LINKS;
}
public Collection<OpenOption> toOpenOptions(OpenOption... additional) {
List<OpenOption> openOptions = new ArrayList<>(options.size() + additional.length);
for (CopyOption option : options) {
if (option instanceof OpenOption) {
openOptions.add((OpenOption) option);
}
}
Collections.addAll(openOptions, additional);
return openOptions;
}
}

@ -0,0 +1,57 @@
package org.xbib.io.ftp.fs;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystemException;
import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.util.Collection;
/**
* A default {@link FileSystemExceptionFactory} that always returns an {@link FTPFileSystemException}
* unless specified otherwise.
*/
public class DefaultFileSystemExceptionFactory implements FileSystemExceptionFactory {
static final DefaultFileSystemExceptionFactory INSTANCE = new DefaultFileSystemExceptionFactory();
@Override
public FileSystemException createGetFileException(String file, int replyCode, String replyString) {
return new NoSuchFileException(file);
}
@Override
public FileSystemException createChangeWorkingDirectoryException(String directory, int replyCode, String replyString) {
return new FTPFileSystemException(directory, replyCode, replyString);
}
@Override
public FileAlreadyExistsException createCreateDirectoryException(String directory, int replyCode, String replyString) {
return new FileAlreadyExistsException(directory);
}
@Override
public FileSystemException createDeleteException(String file, int replyCode, String replyString, boolean isDirectory) {
return new FTPFileSystemException(file, replyCode, replyString);
}
@Override
public FileSystemException createNewInputStreamException(String file, int replyCode, String replyString) {
return new FTPFileSystemException(file, replyCode, replyString);
}
@Override
public FileSystemException createNewOutputStreamException(String file, int replyCode, String replyString,
Collection<? extends OpenOption> options) {
return new FTPFileSystemException(file, replyCode, replyString);
}
@Override
public FileSystemException createCopyException(String file, String other, int replyCode, String replyString) {
return new FTPFileSystemException(file, other, replyCode, replyString);
}
@Override
public FileSystemException createMoveException(String file, String other, int replyCode, String replyString) {
return new FTPFileSystemException(file, other, replyCode, replyString);
}
}

@ -0,0 +1,446 @@
package org.xbib.io.ftp.fs;
import org.xbib.io.ftp.client.FTPClient;
import org.xbib.io.ftp.client.FTPFile;
import org.xbib.io.ftp.client.FTPFileFilter;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.nio.file.OpenOption;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* A pool of FTP clients, allowing multiple commands to be executed concurrently.
*/
final class FTPClientPool {
private final String hostname;
private final int port;
private final FTPEnvironment env;
private final FileSystemExceptionFactory exceptionFactory;
private final BlockingQueue<Client> pool;
FTPClientPool(String hostname, int port, FTPEnvironment env) throws IOException {
this.hostname = hostname;
this.port = port;
this.env = env.clone();
this.exceptionFactory = env.getExceptionFactory();
final int poolSize = env.getClientConnectionCount();
this.pool = new ArrayBlockingQueue<>(poolSize);
try {
for (int i = 0; i < poolSize; i++) {
pool.add(new Client(true));
}
} catch (IOException e) {
// creating the pool failed, disconnect all clients
for (Client client : pool) {
try {
client.disconnect();
} catch (IOException e2) {
e.addSuppressed(e2);
}
}
throw e;
}
}
Client get() throws IOException {
try {
Client client = pool.take();
try {
if (!client.isConnected()) {
client = new Client(true);
}
} catch (final Exception e) {
// could not create a new client; re-add the broken client to the pool to prevent pool starvation
pool.add(client);
throw e;
}
client.increaseRefCount();
return client;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
InterruptedIOException iioe = new InterruptedIOException(e.getMessage());
iioe.initCause(e);
throw iioe;
}
}
Client getOrCreate() throws IOException {
Client client = pool.poll();
if (client == null) {
// nothing was taken from the pool, so no risk of pool starvation if creating the client fails
return new Client(false);
}
try {
if (!client.isConnected()) {
client = new Client(true);
}
} catch (final Exception e) {
// could not create a new client; re-add the broken client to the pool to prevent pool starvation
pool.add(client);
throw e;
}
client.increaseRefCount();
return client;
}
void keepAlive() throws IOException {
List<Client> clients = new ArrayList<>();
pool.drainTo(clients);
IOException exception = null;
for (Client client : clients) {
try {
client.keepAlive();
} catch (IOException e) {
exception = add(exception, e);
} finally {
returnToPool(client);
}
}
if (exception != null) {
throw exception;
}
}
void close() throws IOException {
List<Client> clients = new ArrayList<>();
pool.drainTo(clients);
IOException exception = null;
for (Client client : clients) {
try {
client.disconnect();
} catch (IOException e) {
exception = add(exception, e);
}
}
if (exception != null) {
throw exception;
}
}
private IOException add(IOException existing, IOException e) {
if (existing == null) {
return e;
}
existing.addSuppressed(e);
return existing;
}
private void returnToPool(Client client) {
assert client.refCount == 0;
pool.add(client);
}
final class Client implements Closeable {
private final FTPClient client;
private final boolean pooled;
private FileType fileType;
private FileStructure fileStructure;
private FileTransferMode fileTransferMode;
private int refCount = 0;
private Client(boolean pooled) throws IOException {
this.client = env.createClient(hostname, port);
this.pooled = pooled;
this.fileType = env.getDefaultFileType();
this.fileStructure = env.getDefaultFileStructure();
this.fileTransferMode = env.getDefaultFileTransferMode();
}
private void increaseRefCount() {
refCount++;
}
private int decreaseRefCount() {
if (refCount > 0) {
refCount--;
}
return refCount;
}
private void keepAlive() throws IOException {
client.sendNoOp();
}
private boolean isConnected() {
if (client.isConnected()) {
try {
keepAlive();
return true;
} catch (IOException e) {
// the keep alive failed - treat as not connected, and actually disconnect quietly
disconnectQuietly();
}
}
return false;
}
private void disconnect() throws IOException {
client.disconnect();
}
private void disconnectQuietly() {
try {
client.disconnect();
} catch (IOException e) {
// ignore
}
}
@Override
public void close() throws IOException {
if (decreaseRefCount() == 0) {
if (pooled) {
returnToPool(this);
} else {
disconnect();
}
}
}
String pwd() throws IOException {
String pwd = client.printWorkingDirectory();
if (pwd == null) {
throw new FTPFileSystemException(client.getReplyCode(), client.getReplyString());
}
return pwd;
}
private void applyTransferOptions(TransferOptions options) throws IOException {
if (options.fileType != null && options.fileType != fileType) {
options.fileType.apply(client);
fileType = options.fileType;
}
if (options.fileStructure != null && options.fileStructure != fileStructure) {
options.fileStructure.apply(client);
fileStructure = options.fileStructure;
}
if (options.fileTransferMode != null && options.fileTransferMode != fileTransferMode) {
options.fileTransferMode.apply(client);
fileTransferMode = options.fileTransferMode;
}
}
InputStream newInputStream(String path, OpenOptions options) throws IOException {
assert options.read;
applyTransferOptions(options);
InputStream in = client.retrieveFileStream(path);
if (in == null) {
throw exceptionFactory.createNewInputStreamException(path, client.getReplyCode(), client.getReplyString());
}
refCount++;
return new FTPInputStream(path, in, options.deleteOnClose);
}
OutputStream newOutputStream(String path, OpenOptions options) throws IOException {
assert options.write;
applyTransferOptions(options);
OutputStream out = options.append ? client.appendFileStream(path) : client.storeFileStream(path);
if (out == null) {
throw exceptionFactory.createNewOutputStreamException(path, client.getReplyCode(), client.getReplyString(), options.options);
}
refCount++;
return new FTPOutputStream(path, out, options.deleteOnClose);
}
private void finalizeStream() throws IOException {
assert refCount > 0;
if (!client.completePendingCommand()) {
throw new FTPFileSystemException(client.getReplyCode(), client.getReplyString());
}
if (decreaseRefCount() == 0) {
if (pooled) {
returnToPool(Client.this);
} else {
disconnect();
}
}
}
void storeFile(String path, InputStream local, TransferOptions options, Collection<? extends OpenOption> openOptions) throws IOException {
applyTransferOptions(options);
if (!client.storeFile(path, local)) {
throw exceptionFactory.createNewOutputStreamException(path, client.getReplyCode(), client.getReplyString(), openOptions);
}
}
FTPFile[] listFiles(String path) throws IOException {
return client.listFiles(path);
}
FTPFile[] listFiles(String path, FTPFileFilter filter) throws IOException {
return client.listFiles(path, filter);
}
void throwIfEmpty(String path, FTPFile[] ftpFiles) throws IOException {
if (ftpFiles.length == 0) {
throw exceptionFactory.createGetFileException(path, client.getReplyCode(), client.getReplyString());
}
}
void mkdir(String path) throws IOException {
if (!client.makeDirectory(path)) {
throw exceptionFactory.createCreateDirectoryException(path, client.getReplyCode(), client.getReplyString());
}
}
void delete(String path, boolean isDirectory) throws IOException {
boolean success = isDirectory ? client.removeDirectory(path) : client.deleteFile(path);
if (!success) {
throw exceptionFactory.createDeleteException(path, client.getReplyCode(), client.getReplyString(), isDirectory);
}
}
void rename(String source, String target) throws IOException {
if (!client.rename(source, target)) {
throw exceptionFactory.createMoveException(source, target, client.getReplyCode(), client.getReplyString());
}
}
ZonedDateTime mdtm(String path) throws IOException {
FTPFile file = client.mdtmFile(path);
return file == null ? null : file.getTimestamp();
}
private final class FTPInputStream extends InputStream {
private final String path;
private final InputStream in;
private final boolean deleteOnClose;
private boolean open = true;
private FTPInputStream(String path, InputStream in, boolean deleteOnClose) {
this.path = path;
this.in = in;
this.deleteOnClose = deleteOnClose;
}
@Override
public int read() throws IOException {
return in.read();
}
@Override
public int read(byte[] b) throws IOException {
return in.read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return in.read(b, off, len);
}
@Override
public long skip(long n) throws IOException {
return in.skip(n);
}
@Override
public int available() throws IOException {
return in.available();
}
@Override
public void close() throws IOException {
if (open) {
in.close();
open = false;
finalizeStream();
if (deleteOnClose) {
delete(path, false);
}
}
}
@Override
public synchronized void mark(int readlimit) {
in.mark(readlimit);
}
@Override
public synchronized void reset() throws IOException {
in.reset();
}
@Override
public boolean markSupported() {
return in.markSupported();
}
}
private final class FTPOutputStream extends OutputStream {
private final String path;
private final OutputStream out;
private final boolean deleteOnClose;
private boolean open = true;
private FTPOutputStream(String path, OutputStream out, boolean deleteOnClose) {
this.path = path;
this.out = out;
this.deleteOnClose = deleteOnClose;
}
@Override
public void write(int b) throws IOException {
out.write(b);
}
@Override
public void write(byte[] b) throws IOException {
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
}
@Override
public void flush() throws IOException {
out.flush();
}
@Override
public void close() throws IOException {
if (open) {
out.close();
open = false;
finalizeStream();
if (deleteOnClose) {
delete(path, false);
}
}
}
}
}
}

@ -0,0 +1,877 @@
package org.xbib.io.ftp.fs;
import org.xbib.io.ftp.client.FTP;
import org.xbib.io.ftp.client.FTPClient;
import org.xbib.io.ftp.client.FTPClientConfig;
import org.xbib.io.ftp.client.FTPFileEntryParser;
import org.xbib.io.ftp.client.parser.FTPFileEntryParserFactory;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.spi.FileSystemProvider;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* A utility class to set up environments that can be used in the {@link FileSystemProvider#newFileSystem(URI, Map)}
* and {@link FileSystemProvider#newFileSystem(Path, Map)} methods of {@link FTPFileSystemProvider}.
*/
public class FTPEnvironment implements Map<String, Object>, Cloneable {
// connect support
private static final String LOCAL_ADDR = "localAddr";
private static final String LOCAL_PORT = "localPort";
// login support
private static final String USERNAME = "username";
private static final String PASSWORD = "password";
private static final String ACCOUNT = "account";
// SocketClient
private static final String SO_TIMEOUT = "soTimeout";
private static final String SEND_BUFFER_SIZE = "sendBufferSize";
private static final String RECEIVE_BUFFER_SIZE = "receiveBufferSize";
private static final String TCP_NO_DELAY = "tcpNoDelay";
private static final String KEEP_ALIVE = "keepAlive";
private static final String SO_LINGER_ON = "soLinger.on";
private static final String SO_LINGER_VALUE = "soLinger.val";
private static final String SOCKET_FACTORY = "socketFactory";
private static final String SERVER_SOCKET_FACTORY = "serverSocketFactory";
private static final String CONNECT_TIMEOUT = "connectTimeout";
private static final String PROXY = "proxy";
private static final String CHARSET = "charset";
// FTP
private static final String CONTROL_ENCODING = "controlEncoding";
private static final String STRICT_MULTILINE_PARSING = "strictMultilineParsing";
// FTPClient
private static final String DATA_TIMEOUT = "dataTimeout";
private static final String PARSER_FACTORY = "parserFactory";
private static final String REMOTE_VERIFICATION_ENABLED = "remoteVerificationEnabled";
private static final String DEFAULT_DIR = "defaultDir";
private static final String CONNECTION_MODE = "connectionMode";
private static final String ACTIVE_PORT_RANGE_MIN = "activePortRange.min";
private static final String ACTIVE_PORT_RANGE_MAX = "activePortRange.max";
private static final String ACTIVE_EXTERNAL_IP_ADDRESS = "activeExternalIPAddress";
private static final String PASSIVE = "passive";
private static final String PASSIVE_LOCAL_IP_ADDRESS = "passiveLocalIPAddress";
private static final String REPORT_ACTIVE_EXTERNAL_IP_ADDRESS = "reportActiveExternalIPAddress";
private static final String BUFFER_SIZE = "bufferSize";
private static final String SEND_DATA_SOCKET_BUFFER_SIZE = "sendDataSocketBufferSize";
private static final String RECEIVE_DATA_SOCKET_BUFFER_SIZE = "receiveDataSocketBufferSize";
private static final String CLIENT_CONFIG = "clientConfig";
private static final String USE_EPSV_WITH_IPV4 = "useEPSVwithIPv4";
private static final String CONTROL_KEEP_ALIVE_TIMEOUT = "controlKeepAliveTimeout";
private static final String CONTROL_KEEP_ALIVE_REPLY_TIMEOUT = "controlKeepAliveReplyTimeout";
private static final String PASSIVE_NAT_WORKAROUND_STRATEGY = "passiveNatWorkaroundStrategy";
private static final String AUTODETECT_ENCODING = "autodetectEncoding";
// FTP file system support
private static final int DEFAULT_CLIENT_CONNECTION_COUNT = 1;
private static final String CLIENT_CONNECTION_COUNT = "clientConnectionCount";
private static final String FILE_SYSTEM_EXCEPTION_FACTORY = "fileSystemExceptionFactory";
private Map<String, Object> map;
/**
* Creates a new FTP environment.
*/
public FTPEnvironment() {
map = new HashMap<>();
}
/**
* Creates a new FTP environment.
*
* @param map The map to wrap.
*/
public FTPEnvironment(Map<String, Object> map) {
this.map = Objects.requireNonNull(map);
}
@SuppressWarnings("unchecked")
static FTPEnvironment wrap(Map<String, ?> map) {
if (map instanceof FTPEnvironment) {
return (FTPEnvironment) map;
}
return new FTPEnvironment((Map<String, Object>) map);
}
/**
* Stores the local address to use.
*
* @param localAddr The local address to use.
* @param localPort The local port to use.
* @return This object.
* @see Socket#bind(SocketAddress)
* @see InetSocketAddress#InetSocketAddress(InetAddress, int)
*/
public FTPEnvironment withLocalAddress(InetAddress localAddr, int localPort) {
put(LOCAL_ADDR, localAddr);
put(LOCAL_PORT, localPort);
return this;
}
// login support
/**
* Stores the credentials to use.
*
* @param username The username to use.
* @param password The password to use.
* @return This object.
*/
public FTPEnvironment withCredentials(String username, char[] password) {
put(USERNAME, username);
put(PASSWORD, password);
return this;
}
/**
* Stores the credentials to use.
*
* @param username The username to use.
* @param password The password to use.
* @param account The account to use.
* @return This object.
*/
public FTPEnvironment withCredentials(String username, char[] password, String account) {
put(USERNAME, username);
put(PASSWORD, password);
put(ACCOUNT, account);
return this;
}
// SocketClient
/**
* Stores the socket timeout.
*
* @param timeout The socket timeout in milliseconds.
* @return This object.
* @see Socket#setSoTimeout(int)
*/
public FTPEnvironment withSoTimeout(int timeout) {
put(SO_TIMEOUT, timeout);
return this;
}
/**
* Stores the socket send buffer size to use.
*
* @param size The size of the buffer in bytes.
* @return This object.
* @see Socket#setSendBufferSize(int)
*/
public FTPEnvironment withSendBufferSize(int size) {
put(SEND_BUFFER_SIZE, size);
return this;
}
/**
* Stores the socket receive buffer size to use.
*
* @param size The size of the buffer in bytes.
* @return This object.
* @see Socket#setReceiveBufferSize(int)
*/
public FTPEnvironment withReceiveBufferSize(int size) {
put(RECEIVE_BUFFER_SIZE, size);
return this;
}
/**
* Stores whether or not the Nagle's algorithm ({@code TCP_NODELAY}) should be enabled.
*
* @param on {@code true} if Nagle's algorithm should be enabled, or {@code false} otherwise.
* @return This object.
* @see Socket#setTcpNoDelay(boolean)
*/
public FTPEnvironment withTcpNoDelay(boolean on) {
put(TCP_NO_DELAY, on);
return this;
}
/**
* Stores whether or not {@code SO_KEEPALIVE} should be enabled.
*
* @param keepAlive {@code true} if keep-alive should be enabled, or {@code false} otherwise.
* @return This object.
* @see Socket#setKeepAlive(boolean)
*/
public FTPEnvironment withKeepAlive(boolean keepAlive) {
put(KEEP_ALIVE, keepAlive);
return this;
}
/**
* Stores whether or not {@code SO_LINGER} should be enabled, and if so, the linger time.
*
* @param on {@code true} if {@code SO_LINGER} should be enabled, or {@code false} otherwise.
* @param linger The linger time in seconds, if {@code on} is {@code true}.
* @return This object.
* @see Socket#setSoLinger(boolean, int)
*/
public FTPEnvironment withSoLinger(boolean on, int linger) {
put(SO_LINGER_ON, on);
put(SO_LINGER_VALUE, linger);
return this;
}
/**
* Stores the socket factory to use.
*
* @param factory The socket factory to use.
* @return This object.
*/
public FTPEnvironment withSocketFactory(SocketFactory factory) {
put(SOCKET_FACTORY, factory);
return this;
}
/**
* Stores the server socket factory to use.
*
* @param factory The server socket factory to use.
* @return This object.
*/
public FTPEnvironment withServerSocketFactory(ServerSocketFactory factory) {
put(SERVER_SOCKET_FACTORY, factory);
return this;
}
/**
* Stores the connection timeout to use.
*
* @param timeout The connection timeout in milliseconds.
* @return This object.
* @see Socket#connect(SocketAddress, int)
*/
public FTPEnvironment withConnectTimeout(int timeout) {
put(CONNECT_TIMEOUT, timeout);
return this;
}
/**
* Stores the proxy to use.
*
* @param proxy The proxy to use.
* @return This object.
*/
public FTPEnvironment withProxy(Proxy proxy) {
put(PROXY, proxy);
return this;
}
/**
* Stores the charset to use.
*
* @param charset The charset to use.
* @return This object.
*/
public FTPEnvironment withCharset(Charset charset) {
put(CHARSET, charset);
return this;
}
// FTP
/**
* Stores the character encoding to be used by the FTP control connection.
* Some FTP servers require that commands be issued in a non-ASCII encoding like UTF-8 so that filenames with multi-byte character
* representations (e.g, Big 8) can be specified.
*
* @param encoding The character encoding to use.
* @return This object.
*/
public FTPEnvironment withControlEncoding(String encoding) {
put(CONTROL_ENCODING, encoding);
return this;
}
/**
* Stores whether or not strict multiline parsing should be enabled, as per RFC 959, section 4.2.
*
* @param strictMultilineParsing {@code true} to enable strict multiline parsing, or {@code false} to disable it.
* @return This object.
*/
public FTPEnvironment withStrictlyMultilineParsing(boolean strictMultilineParsing) {
put(STRICT_MULTILINE_PARSING, strictMultilineParsing);
return this;
}
// FTPClient
/**
* Stores the timeout in milliseconds to use when reading from data connections.
*
* @param timeout The timeout in milliseconds that is used when opening data connection sockets.
* @return This object.
*/
public FTPEnvironment withDataTimeout(int timeout) {
put(DATA_TIMEOUT, timeout);
return this;
}
/**
* Stores the factory used for parser creation.
*
* @param parserFactory The factory object used to create {@link FTPFileEntryParser}s
* @return This object.
*/
public FTPEnvironment withParserFactory(FTPFileEntryParserFactory parserFactory) {
put(PARSER_FACTORY, parserFactory);
return this;
}
/**
* Stores whether or not verification that the remote host taking part of a data connection is the same as the host to which the control
* connection is attached should be enabled.
*
* @param enabled {@code true} to enable verification, or {@code false} to disable verification.
* @return This object.
*/
public FTPEnvironment withRemoteVerificationEnabled(boolean enabled) {
put(REMOTE_VERIFICATION_ENABLED, enabled);
return this;
}
/**
* Stores the default directory to use.
* If it exists, this will be the directory that relative paths are resolved to.
*
* @param pathname The default directory to use.
* @return This object.
*/
public FTPEnvironment withDefaultDirectory(String pathname) {
put(DEFAULT_DIR, pathname);
return this;
}
/**
* Stores the connection mode to use.
* If the connection mode is not set, it will default to {@link ConnectionMode#ACTIVE}.
*
* @param connectionMode The connection mode to use.
* @return This object.
*/
public FTPEnvironment withConnectionMode(ConnectionMode connectionMode) {
put(CONNECTION_MODE, connectionMode);
return this;
}
/**
* Stores the client side port range in active mode.
*
* @param minPort The lowest available port (inclusive).
* @param maxPort The highest available port (inclusive).
* @return This object.
*/
public FTPEnvironment withActivePortRange(int minPort, int maxPort) {
put(ACTIVE_PORT_RANGE_MIN, minPort);
put(ACTIVE_PORT_RANGE_MAX, maxPort);
return this;
}
/**
* Stores the external IP address in active mode. Useful when there are multiple network cards.
*
* @param ipAddress The external IP address of this machine.
* @return This object.
*/
public FTPEnvironment withActiveExternalIPAddress(String ipAddress) {
put(ACTIVE_EXTERNAL_IP_ADDRESS, ipAddress);
return this;
}
/**
* Stores the local IP address to use in passive mode. Useful when there are multiple network cards.
*
* @param ipAddress The local IP address of this machine.
* @return This object.
*/
public FTPEnvironment withPassiveLocalIPAddress(String ipAddress) {
put(PASSIVE_LOCAL_IP_ADDRESS, ipAddress);
return this;
}
/**
* Stores the external IP address to report in EPRT/PORT commands in active mode. Useful when there are multiple network cards.
*
* @param ipAddress The external IP address of this machine.
* @return This object.
*/
public FTPEnvironment withReportActiveExternalIPAddress(String ipAddress) {
put(REPORT_ACTIVE_EXTERNAL_IP_ADDRESS, ipAddress);
return this;
}
/**
* Stores the buffer size to use.
*
* @param bufferSize The buffer size to use.
* @return This object.
*/
public FTPEnvironment withBufferSize(int bufferSize) {
put(BUFFER_SIZE, bufferSize);
return this;
}
/**
* Stores the value to use for the data socket {@code SO_SNDBUF} option.
*
* @param bufferSizr The size of the buffer.
* @return This object.
*/
public FTPEnvironment withSendDataSocketBufferSize(int bufferSizr) {
put(SEND_DATA_SOCKET_BUFFER_SIZE, bufferSizr);
return this;
}
/**
* Stores the value to use for the data socket {@code SO_RCVBUF} option.
*
* @param bufferSize The size of the buffer.
* @return This object.
*/
public FTPEnvironment withReceiveDataSocketBufferSize(int bufferSize) {
put(RECEIVE_DATA_SOCKET_BUFFER_SIZE, bufferSize);
return this;
}
/**
* Stores the FTP client config to use.
*
* @param clientConfig The client config to use.
* @return This object.
*/
public FTPEnvironment withClientConfig(FTPClientConfig clientConfig) {
put(CLIENT_CONFIG, clientConfig);
return this;
}
/**
* Stores whether or not to use EPSV with IPv4. Might be worth enabling in some circumstances.
* For example, when using IPv4 with NAT it may work with some rare configurations.
* E.g. if FTP server has a static PASV address (external network) and the client is coming from another internal network.
* In that case the data connection after PASV command would fail, while EPSV would make the client succeed by taking just the port.
*
* @param selected The flag to use.
* @return This object.
*/
public FTPEnvironment withUseEPSVwithIPv4(boolean selected) {
put(USE_EPSV_WITH_IPV4, selected);
return this;
}
/**
* Stores the time to wait between sending control connection keep-alive messages when processing file upload or download.
*
* @param timeout The keep-alive timeout to use, in milliseconds.
* @return This object.
*/
public FTPEnvironment withControlKeepAliveTimeout(long timeout) {
put(CONTROL_KEEP_ALIVE_TIMEOUT, timeout);
return this;
}
/**
* Stores how long to wait for control keep-alive message replies.
*
* @param timeout The keep-alive reply timeout to use, in milliseconds.
* @return This object.
*/
public FTPEnvironment withControlKeepAliveReplyTimeout(int timeout) {
put(CONTROL_KEEP_ALIVE_REPLY_TIMEOUT, timeout);
return this;
}
/**
* Stores the workaround strategy to replace the PASV mode reply addresses.
* This gets around the problem that some NAT boxes may change the reply.
* The default implementation is {@link FTPClient.NatServerResolverImpl}, i.e. site-local replies are replaced.
*
* @param resolver The workaround strategy to replace internal IP's in passive mode, or {@code null} to disable the workaround
* (i.e. use PASV mode reply address.)
* @return This object.
*/
public FTPEnvironment withPassiveNatWorkaroundStrategy(FTPClient.HostnameResolver resolver) {
put(PASSIVE_NAT_WORKAROUND_STRATEGY, resolver);
return this;
}
/**
* Stores whether or not automatic server encoding detection should be enabled.
* Note that only UTF-8 is supported.
*
* @param autodetect {@code true} to enable automatic server encoding detection, or {@code false} to disable it.
* @return This object.
*/
public FTPEnvironment withAutodetectEncoding(boolean autodetect) {
put(AUTODETECT_ENCODING, autodetect);
return this;
}
// FTP file system support
/**
* Stores the number of client connections to use. This value influences the number of concurrent threads that can access an FTP file system.
*
* @param count The number of client connection to use.
* @return This object.
*/
public FTPEnvironment withClientConnectionCount(int count) {
put(CLIENT_CONNECTION_COUNT, count);
return this;
}
/**
* Stores the file system exception factory to use.
*
* @param factory The file system exception factory to use.
* @return This object.
*/
public FTPEnvironment withFileSystemExceptionFactory(FileSystemExceptionFactory factory) {
put(FILE_SYSTEM_EXCEPTION_FACTORY, factory);
return this;
}
String getUsername() {
return FileSystemProviderSupport.getValue(this, USERNAME, String.class, null);
}
FileType getDefaultFileType() {
// explicitly set in initializePostConnect
return FileType.binary();
}
FileStructure getDefaultFileStructure() {
// as specified by FTPClient
return FileStructure.FILE;
}
FileTransferMode getDefaultFileTransferMode() {
// as specified by FTPClient
return FileTransferMode.STREAM;
}
int getClientConnectionCount() {
int count = FileSystemProviderSupport.getIntValue(this, CLIENT_CONNECTION_COUNT, DEFAULT_CLIENT_CONNECTION_COUNT);
return Math.max(1, count);
}
FileSystemExceptionFactory getExceptionFactory() {
return FileSystemProviderSupport.getValue(this, FILE_SYSTEM_EXCEPTION_FACTORY, FileSystemExceptionFactory.class,
DefaultFileSystemExceptionFactory.INSTANCE);
}
FTPClient createClient(String hostname, int port) throws IOException {
FTPClient client = new FTPClient();
initializePreConnect(client);
connect(client, hostname, port);
initializePostConnect(client);
verifyConnection(client);
return client;
}
void initializePreConnect(FTPClient client) throws IOException {
client.setListHiddenFiles(true);
if (containsKey(SEND_BUFFER_SIZE)) {
int size = FileSystemProviderSupport.getIntValue(this, SEND_BUFFER_SIZE);
client.setSendBufferSize(size);
}
if (containsKey(RECEIVE_BUFFER_SIZE)) {
int size = FileSystemProviderSupport.getIntValue(this, RECEIVE_BUFFER_SIZE);
client.setReceiveBufferSize(size);
}
if (containsKey(SOCKET_FACTORY)) {
SocketFactory factory = FileSystemProviderSupport.getValue(this, SOCKET_FACTORY, SocketFactory.class, null);
client.setSocketFactory(factory);
}
if (containsKey(SERVER_SOCKET_FACTORY)) {
ServerSocketFactory factory = FileSystemProviderSupport.getValue(this, SERVER_SOCKET_FACTORY, ServerSocketFactory.class, null);
client.setServerSocketFactory(factory);
}
if (containsKey(CONNECT_TIMEOUT)) {
int connectTimeout = FileSystemProviderSupport.getIntValue(this, CONNECT_TIMEOUT);
client.setConnectTimeout(connectTimeout);
}
if (containsKey(PROXY)) {
Proxy proxy = FileSystemProviderSupport.getValue(this, PROXY, Proxy.class, null);
client.setProxy(proxy);
}
if (containsKey(CHARSET)) {
Charset charset = FileSystemProviderSupport.getValue(this, CHARSET, Charset.class, null);
client.setCharset(charset);
}
if (containsKey(CONTROL_ENCODING)) {
String controlEncoding = FileSystemProviderSupport.getValue(this, CONTROL_ENCODING, String.class, null);
client.setControlEncoding(controlEncoding);
}
if (containsKey(STRICT_MULTILINE_PARSING)) {
boolean strictMultilineParsing = FileSystemProviderSupport.getBooleanValue(this, STRICT_MULTILINE_PARSING);
client.setStrictMultilineParsing(strictMultilineParsing);
}
if (containsKey(DATA_TIMEOUT)) {
int timeout = FileSystemProviderSupport.getIntValue(this, DATA_TIMEOUT);
client.setDataTimeout(timeout);
}
if (containsKey(PARSER_FACTORY)) {
FTPFileEntryParserFactory parserFactory = FileSystemProviderSupport.getValue(this, PARSER_FACTORY, FTPFileEntryParserFactory.class, null);
client.setParserFactory(parserFactory);
}
if (containsKey(REMOTE_VERIFICATION_ENABLED)) {
boolean enable = FileSystemProviderSupport.getBooleanValue(this, REMOTE_VERIFICATION_ENABLED);
client.setRemoteVerificationEnabled(enable);
}
FileSystemProviderSupport.getValue(this, CONNECTION_MODE, ConnectionMode.class, ConnectionMode.ACTIVE).apply(client);
if (containsKey(ACTIVE_PORT_RANGE_MIN) && containsKey(ACTIVE_PORT_RANGE_MAX)) {
int minPort = FileSystemProviderSupport.getIntValue(this, ACTIVE_PORT_RANGE_MIN);
int maxPort = FileSystemProviderSupport.getIntValue(this, ACTIVE_PORT_RANGE_MAX);
client.setActivePortRange(minPort, maxPort);
}
if (containsKey(ACTIVE_EXTERNAL_IP_ADDRESS)) {
String ipAddress = FileSystemProviderSupport.getValue(this, ACTIVE_EXTERNAL_IP_ADDRESS, String.class, null);
client.setActiveExternalIPAddress(ipAddress);
}
if (containsKey(PASSIVE_LOCAL_IP_ADDRESS)) {
String ipAddress = FileSystemProviderSupport.getValue(this, PASSIVE_LOCAL_IP_ADDRESS, String.class, null);
client.setPassiveLocalIPAddress(ipAddress);
}
if (containsKey(REPORT_ACTIVE_EXTERNAL_IP_ADDRESS)) {
String ipAddress = FileSystemProviderSupport.getValue(this, REPORT_ACTIVE_EXTERNAL_IP_ADDRESS, String.class, null);
client.setReportActiveExternalIPAddress(ipAddress);
}
if (containsKey(BUFFER_SIZE)) {
int bufSize = FileSystemProviderSupport.getIntValue(this, BUFFER_SIZE);
client.setBufferSize(bufSize);
}
if (containsKey(SEND_DATA_SOCKET_BUFFER_SIZE)) {
int bufSize = FileSystemProviderSupport.getIntValue(this, SEND_DATA_SOCKET_BUFFER_SIZE);
client.setSendDataSocketBufferSize(bufSize);
}
if (containsKey(RECEIVE_DATA_SOCKET_BUFFER_SIZE)) {
int bufSize = FileSystemProviderSupport.getIntValue(this, RECEIVE_DATA_SOCKET_BUFFER_SIZE);
client.setReceieveDataSocketBufferSize(bufSize);
}
if (containsKey(CLIENT_CONFIG)) {
FTPClientConfig clientConfig = FileSystemProviderSupport.getValue(this, CLIENT_CONFIG, FTPClientConfig.class, null);
if (clientConfig != null) {
clientConfig = new FTPClientConfig(clientConfig);
}
client.configure(clientConfig);
}
if (containsKey(PASSIVE_NAT_WORKAROUND_STRATEGY)) {
FTPClient.HostnameResolver resolver = FileSystemProviderSupport.getValue(this, PASSIVE_NAT_WORKAROUND_STRATEGY, FTPClient.HostnameResolver.class, null);
client.setPassiveNatWorkaroundStrategy(resolver);
}
if (containsKey(USE_EPSV_WITH_IPV4)) {
boolean selected = FileSystemProviderSupport.getBooleanValue(this, USE_EPSV_WITH_IPV4);
client.setUseEPSVwithIPv4(selected);
}
if (containsKey(CONTROL_KEEP_ALIVE_TIMEOUT)) {
long controlIdle = FileSystemProviderSupport.getLongValue(this, CONTROL_KEEP_ALIVE_TIMEOUT);
// the value is stored as ms, but the method expects seconds
controlIdle = TimeUnit.MILLISECONDS.toSeconds(controlIdle);
client.setControlKeepAliveTimeout(controlIdle);
}
if (containsKey(CONTROL_KEEP_ALIVE_REPLY_TIMEOUT)) {
int timeout = FileSystemProviderSupport.getIntValue(this, CONTROL_KEEP_ALIVE_REPLY_TIMEOUT);
client.setControlKeepAliveReplyTimeout(timeout);
}
if (containsKey(AUTODETECT_ENCODING)) {
boolean autodetect = FileSystemProviderSupport.getBooleanValue(this, AUTODETECT_ENCODING);
client.setAutodetectUTF8(autodetect);
}
}
void connect(FTPClient client, String hostname, int port) throws IOException {
if (port == -1) {
port = client.getDefaultPort();
}
InetAddress localAddr = FileSystemProviderSupport.getValue(this, LOCAL_ADDR, InetAddress.class, null);
if (localAddr != null) {
int localPort = FileSystemProviderSupport.getIntValue(this, LOCAL_PORT);
client.connect(hostname, port, localAddr, localPort);
} else {
client.connect(hostname, port);
}
String username = getUsername();
char[] passwordChars = FileSystemProviderSupport.getValue(this, PASSWORD, char[].class, null);
String password = passwordChars != null ? new String(passwordChars) : null;
String account = FileSystemProviderSupport.getValue(this, ACCOUNT, String.class, null);
if (account != null) {
if (!client.login(username, password, account)) {
throw new FTPFileSystemException(client.getReplyCode(), client.getReplyString());
}
} else if (username != null || password != null) {
if (!client.login(username, password)) {
throw new FTPFileSystemException(client.getReplyCode(), client.getReplyString());
}
}
// else no account or username/password - don't log in
}
void initializePostConnect(FTPClient client) throws IOException {
if (containsKey(SO_TIMEOUT)) {
int timeout = FileSystemProviderSupport.getIntValue(this, SO_TIMEOUT);
client.setSoTimeout(timeout);
}
if (containsKey(TCP_NO_DELAY)) {
boolean on = FileSystemProviderSupport.getBooleanValue(this, TCP_NO_DELAY);
client.setTcpNoDelay(on);
}
if (containsKey(KEEP_ALIVE)) {
boolean keepAlive = FileSystemProviderSupport.getBooleanValue(this, KEEP_ALIVE);
client.setKeepAlive(keepAlive);
}
if (containsKey(SO_LINGER_ON) && containsKey(SO_LINGER_VALUE)) {
boolean on = FileSystemProviderSupport.getBooleanValue(this, SO_LINGER_ON);
int val = FileSystemProviderSupport.getIntValue(this, SO_LINGER_VALUE);
client.setSoLinger(on, val);
}
if (containsKey(PASSIVE)) {
client.enterRemotePassiveMode();
}
// default to binary
client.setFileType(FTP.BINARY_FILE_TYPE);
String defaultDir = FileSystemProviderSupport.getValue(this, DEFAULT_DIR, String.class, null);
if (defaultDir != null && !client.changeWorkingDirectory(defaultDir)) {
throw getExceptionFactory().createChangeWorkingDirectoryException(defaultDir, client.getReplyCode(), client.getReplyString());
}
}
void verifyConnection(FTPClient client) throws IOException {
if (client.printWorkingDirectory() == null) {
throw new FTPFileSystemException(client.getReplyCode(), client.getReplyString());
}
}
// Map / Object
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return map.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return map.containsValue(value);
}
@Override
public Object get(Object key) {
return map.get(key);
}
@Override
public Object put(String key, Object value) {
return map.put(key, value);
}
@Override
public Object remove(Object key) {
return map.remove(key);
}
@Override
public void putAll(Map<? extends String, ? extends Object> m) {
map.putAll(m);
}
@Override
public void clear() {
map.clear();
}
@Override
public Set<String> keySet() {
return map.keySet();
}
@Override
public Collection<Object> values() {
return map.values();
}
@Override
public Set<Entry<String, Object>> entrySet() {
return map.entrySet();
}
@Override
public boolean equals(Object o) {
return map.equals(o);
}
@Override
public int hashCode() {
return map.hashCode();
}
@Override
public String toString() {
return map.toString();
}
@Override
public FTPEnvironment clone() {
try {
FTPEnvironment clone = (FTPEnvironment) super.clone();
clone.map = new HashMap<>(map);
return clone;
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
}
}

@ -0,0 +1,48 @@
package org.xbib.io.ftp.fs;
import java.nio.file.FileAlreadyExistsException;
/**
* An exception that is thrown if an FTP command does not execute successfully because a file already exists.
*/
public class FTPFileAlreadyExistsException extends FileAlreadyExistsException implements FTPResponse {
private static final long serialVersionUID = 671724890729526141L;
private final int replyCode;
/**
* Creates a new {@code FTPFileAlreadyExistsException}.
*
* @param file A string identifying the file, or {@code null} if not known.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this exception.
* @param replyString The entire text from the last FTP response that triggered this exception. It will be used as the exception's reason.
*/
public FTPFileAlreadyExistsException(String file, int replyCode, String replyString) {
super(file, null, replyString);
this.replyCode = replyCode;
}
/**
* Creates a new {@code FTPFileAlreadyExistsException}.
*
* @param file A string identifying the file, or {@code null} if not known.
* @param other A string identifying the other file, or {@code null} if not known.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this exception.
* @param replyString The entire text from the last FTP response that triggered this exception. It will be used as the exception's reason.
*/
public FTPFileAlreadyExistsException(String file, String other, int replyCode, String replyString) {
super(file, other, replyString);
this.replyCode = replyCode;
}
@Override
public int getReplyCode() {
return replyCode;
}
@Override
public String getReplyString() {
return getReason();
}
}

@ -0,0 +1,81 @@
package org.xbib.io.ftp.fs;
import java.io.IOException;
import java.nio.file.FileStore;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.attribute.FileStoreAttributeView;
import java.nio.file.attribute.PosixFileAttributeView;
import java.util.Objects;
/**
* An FTP file system store.
*/
class FTPFileStore extends FileStore {
private final FTPFileSystem fs;
FTPFileStore(FTPFileSystem fs) {
this.fs = Objects.requireNonNull(fs);
}
@Override
public String name() {
return fs.toUri("/").toString();
}
@Override
public String type() {
return "ftp";
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
public long getTotalSpace() throws IOException {
return fs.getTotalSpace();
}
@Override
public long getUsableSpace() throws IOException {
return fs.getUsableSpace();
}
@Override
public long getUnallocatedSpace() throws IOException {
return fs.getUnallocatedSpace();
}
@Override
public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type) {
return type == BasicFileAttributeView.class || type == PosixFileAttributeView.class;
}
@Override
public boolean supportsFileAttributeView(String name) {
return "basic".equals(name) || "owner".equals(name) || "posix".equals(name);
}
@Override
public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type) {
Objects.requireNonNull(type);
return null;
}
@Override
public Object getAttribute(String attribute) throws IOException {
if ("totalSpace".equals(attribute)) {
return getTotalSpace();
}
if ("usableSpace".equals(attribute)) {
return getUsableSpace();
}
if ("unallocatedSpace".equals(attribute)) {
return getUnallocatedSpace();
}
throw Messages.fileStore().unsupportedAttribute(attribute);
}
}

@ -0,0 +1,183 @@
package org.xbib.io.ftp.fs;
import org.xbib.io.ftp.client.FTPFile;
import org.xbib.io.ftp.client.FTPFileFilter;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotDirectoryException;
import java.util.ArrayList;
import java.util.List;
/**
* A strategy for handling FTP files in an FTP server specific way.
* This will help support FTP servers that return the current directory (.) when
* listing directories, and FTP servers that don't.
*/
abstract class FTPFileStrategy {
static FTPFileStrategy getInstance(FTPClientPool.Client client) throws IOException {
FTPFile[] ftpFiles = client.listFiles("/", new FTPFileFilter() {
@Override
public boolean accept(FTPFile ftpFile) {
String fileName = FTPFileSystem.getFileName(ftpFile);
return FTPFileSystem.CURRENT_DIR.equals(fileName);
}
});
return ftpFiles.length == 0 ? NonUnix.INSTANCE : Unix.INSTANCE;
}
abstract List<FTPFile> getChildren(FTPClientPool.Client client, FTPPath path) throws IOException;
abstract FTPFile getFTPFile(FTPClientPool.Client client, FTPPath path) throws IOException;
abstract FTPFile getLink(FTPClientPool.Client client, FTPFile ftpFile, FTPPath path) throws IOException;
private static final class Unix extends FTPFileStrategy {
private static final FTPFileStrategy INSTANCE = new Unix();
@Override
List<FTPFile> getChildren(FTPClientPool.Client client, FTPPath path) throws IOException {
FTPFile[] ftpFiles = client.listFiles(path.path());
if (ftpFiles.length == 0) {
throw new NoSuchFileException(path.path());
}
boolean isDirectory = false;
List<FTPFile> children = new ArrayList<>(ftpFiles.length);
for (FTPFile ftpFile : ftpFiles) {
String fileName = FTPFileSystem.getFileName(ftpFile);
if (FTPFileSystem.CURRENT_DIR.equals(fileName)) {
isDirectory = true;
} else if (!FTPFileSystem.PARENT_DIR.equals(fileName)) {
children.add(ftpFile);
}
}
if (!isDirectory) {
throw new NotDirectoryException(path.path());
}
return children;
}
@Override
FTPFile getFTPFile(FTPClientPool.Client client, FTPPath path) throws IOException {
final String name = path.fileName();
FTPFile[] ftpFiles = client.listFiles(path.path(), new FTPFileFilter() {
@Override
public boolean accept(FTPFile ftpFile) {
String fileName = FTPFileSystem.getFileName(ftpFile);
return FTPFileSystem.CURRENT_DIR.equals(fileName) || (name != null && name.equals(fileName));
}
});
client.throwIfEmpty(path.path(), ftpFiles);
if (ftpFiles.length == 1) {
return ftpFiles[0];
}
for (FTPFile ftpFile : ftpFiles) {
if (FTPFileSystem.CURRENT_DIR.equals(FTPFileSystem.getFileName(ftpFile))) {
return ftpFile;
}
}
throw new IllegalStateException();
}
@Override
FTPFile getLink(FTPClientPool.Client client, FTPFile ftpFile, FTPPath path) throws IOException {
if (ftpFile.getLink() != null) {
return ftpFile;
}
if (ftpFile.isDirectory() && FTPFileSystem.CURRENT_DIR.equals(FTPFileSystem.getFileName(ftpFile))) {
// The file is returned using getFTPFile, which returns the . (current directory) entry for directories.
// List the parent (if any) instead.
final String parentPath = path.toAbsolutePath().parentPath();
final String name = path.fileName();
if (parentPath == null) {
// path is /, there is no link
return null;
}
FTPFile[] ftpFiles = client.listFiles(parentPath, new FTPFileFilter() {
@Override
public boolean accept(FTPFile ftpFile) {
return (ftpFile.isDirectory() || ftpFile.isSymbolicLink()) && name.equals(FTPFileSystem.getFileName(ftpFile));
}
});
client.throwIfEmpty(path.path(), ftpFiles);
return ftpFiles[0].getLink() == null ? null : ftpFiles[0];
}
return null;
}
}
private static final class NonUnix extends FTPFileStrategy {
private static final FTPFileStrategy INSTANCE = new NonUnix();
@Override
List<FTPFile> getChildren(FTPClientPool.Client client, FTPPath path) throws IOException {
FTPFile[] ftpFiles = client.listFiles(path.path());
boolean isDirectory = false;
List<FTPFile> children = new ArrayList<>(ftpFiles.length);
for (FTPFile ftpFile : ftpFiles) {
String fileName = FTPFileSystem.getFileName(ftpFile);
if (FTPFileSystem.CURRENT_DIR.equals(fileName)) {
isDirectory = true;
} else if (!FTPFileSystem.PARENT_DIR.equals(fileName)) {
children.add(ftpFile);
}
}
if (!isDirectory && children.size() <= 1) {
// either zero or one, check the parent to see if the path exists and is a directory
FTPPath currentPath = path;
FTPFile currentFtpFile = getFTPFile(client, currentPath);
while (currentFtpFile.isSymbolicLink()) {
currentPath = path.resolve(currentFtpFile.getLink());
currentFtpFile = getFTPFile(client, currentPath);
}
if (!currentFtpFile.isDirectory()) {
throw new NotDirectoryException(path.path());
}
}
return children;
}
@Override
FTPFile getFTPFile(FTPClientPool.Client client, FTPPath path) throws IOException {
final String parentPath = path.toAbsolutePath().parentPath();
final String name = path.fileName();
if (parentPath == null) {
// path is /, but that cannot be listed
FTPFile rootFtpFile = new FTPFile();
rootFtpFile.setName("/");
rootFtpFile.setType(FTPFile.DIRECTORY_TYPE);
return rootFtpFile;
}
FTPFile[] ftpFiles = client.listFiles(parentPath,
ftpFile -> name != null && name.equals(FTPFileSystem.getFileName(ftpFile)));
if (ftpFiles.length == 0) {
throw new NoSuchFileException(path.path());
}
if (ftpFiles.length == 1) {
return ftpFiles[0];
}
throw new IllegalStateException();
}
@Override
FTPFile getLink(FTPClientPool.Client client, FTPFile ftpFile, FTPPath path) throws IOException {
// getFTPFile always returns the entry in the parent, so there's no need to list the parent here.
return ftpFile.getLink() == null ? null : ftpFile;
}
}
}

@ -0,0 +1,803 @@
package org.xbib.io.ftp.fs;
import org.xbib.io.ftp.client.FTPFile;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.AccessMode;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotLinkException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.StandardOpenOption;
import java.nio.file.WatchService;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.GroupPrincipal;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.UserPrincipal;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
/**
* An FTP file system.
*/
class FTPFileSystem extends FileSystem {
static final String CURRENT_DIR = ".";
static final String PARENT_DIR = "..";
private static final Set<String> SUPPORTED_FILE_ATTRIBUTE_VIEWS = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList("basic", "owner", "posix")));
private static final Set<String> BASIC_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
"basic:lastModifiedTime", "basic:lastAccessTime", "basic:creationTime", "basic:size",
"basic:isRegularFile", "basic:isDirectory", "basic:isSymbolicLink", "basic:isOther", "basic:fileKey")));
private static final Set<String> OWNER_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Collections.singletonList(
"owner:owner")));
private static final Set<String> POSIX_ATTRIBUTES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
"posix:lastModifiedTime", "posix:lastAccessTime", "posix:creationTime", "posix:size",
"posix:isRegularFile", "posix:isDirectory", "posix:isSymbolicLink", "posix:isOther", "posix:fileKey",
"posix:owner", "posix:group", "posix:permissions")));
private final FTPFileSystemProvider provider;
private final Iterable<Path> rootDirectories;
private final FileStore fileStore;
private final Iterable<FileStore> fileStores;
private final FTPClientPool clientPool;
private final URI uri;
private final String defaultDirectory;
private final FTPFileStrategy ftpFileStrategy;
private final AtomicBoolean open = new AtomicBoolean(true);
FTPFileSystem(FTPFileSystemProvider provider, URI uri, FTPEnvironment env) throws IOException {
this.provider = Objects.requireNonNull(provider);
this.rootDirectories = Collections.<Path>singleton(new FTPPath(this, "/"));
this.fileStore = new FTPFileStore(this);
this.fileStores = Collections.<FileStore>singleton(fileStore);
this.clientPool = new FTPClientPool(uri.getHost(), uri.getPort(), env);
this.uri = Objects.requireNonNull(uri);
try (FTPClientPool.Client client = clientPool.get()) {
this.defaultDirectory = client.pwd();
this.ftpFileStrategy = FTPFileStrategy.getInstance(client);
}
}
static String getFileName(FTPFile ftpFile) {
String fileName = ftpFile.getName();
if (fileName == null) {
return null;
}
int index = fileName.lastIndexOf('/');
return index == -1 || index == fileName.length() - 1 ? fileName : fileName.substring(index + 1);
}
@Override
public FTPFileSystemProvider provider() {
return provider;
}
@Override
public void close() throws IOException {
if (open.getAndSet(false)) {
provider.removeFileSystem(uri);
clientPool.close();
}
}
@Override
public boolean isOpen() {
return open.get();
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
public String getSeparator() {
return "/";
}
@Override
public Iterable<Path> getRootDirectories() {
return rootDirectories;
}
@Override
public Iterable<FileStore> getFileStores() {
return fileStores;
}
@Override
public Set<String> supportedFileAttributeViews() {
return SUPPORTED_FILE_ATTRIBUTE_VIEWS;
}
@Override
public Path getPath(String first, String... more) {
StringBuilder sb = new StringBuilder(first);
for (String s : more) {
sb.append("/").append(s);
}
return new FTPPath(this, sb.toString());
}
@Override
public PathMatcher getPathMatcher(String syntaxAndPattern) {
final Pattern pattern = PathMatcherSupport.toPattern(syntaxAndPattern);
return new PathMatcher() {
@Override
public boolean matches(Path path) {
return pattern.matcher(path.toString()).matches();
}
};
}
@Override
public UserPrincipalLookupService getUserPrincipalLookupService() {
throw Messages.unsupportedOperation(FileSystem.class, "getUserPrincipalLookupService");
}
@Override
public WatchService newWatchService() throws IOException {
throw Messages.unsupportedOperation(FileSystem.class, "newWatchService");
}
void keepAlive() throws IOException {
clientPool.keepAlive();
}
URI toUri(FTPPath path) {
FTPPath absPath = toAbsolutePath(path).normalize();
return toUri(absPath.path());
}
URI toUri(String path) {
return URISupport.create(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), path, null, null);
}
FTPPath toAbsolutePath(FTPPath path) {
if (path.isAbsolute()) {
return path;
}
return new FTPPath(this, defaultDirectory + "/" + path.path());
}
FTPPath toRealPath(FTPPath path, LinkOption... options) throws IOException {
boolean followLinks = LinkOptionSupport.followLinks(options);
try (FTPClientPool.Client client = clientPool.get()) {
return toRealPath(client, path, followLinks).ftpPath;
}
}
private FTPPathAndFilePair toRealPath(FTPClientPool.Client client, FTPPath path, boolean followLinks) throws IOException {
FTPPath absPath = toAbsolutePath(path).normalize();
// call getFTPFile to verify the file exists
FTPFile ftpFile = getFTPFile(client, absPath);
if (followLinks && isPossibleSymbolicLink(ftpFile)) {
FTPFile link = getLink(client, ftpFile, absPath);
if (link != null) {
return toRealPath(client, new FTPPath(this, link.getLink()), followLinks);
}
}
return new FTPPathAndFilePair(absPath, ftpFile);
}
private boolean isPossibleSymbolicLink(FTPFile ftpFile) {
return ftpFile.isSymbolicLink() || (ftpFile.isDirectory() && CURRENT_DIR.equals(getFileName(ftpFile)));
}
String toString(FTPPath path) {
return path.path();
}
InputStream newInputStream(FTPPath path, OpenOption... options) throws IOException {
OpenOptions openOptions = OpenOptions.forNewInputStream(options);
try (FTPClientPool.Client client = clientPool.get()) {
return newInputStream(client, path, openOptions);
}
}
private InputStream newInputStream(FTPClientPool.Client client, FTPPath path, OpenOptions options) throws IOException {
assert options.read;
return client.newInputStream(path.path(), options);
}
OutputStream newOutputStream(FTPPath path, OpenOption... options) throws IOException {
OpenOptions openOptions = OpenOptions.forNewOutputStream(options);
try (FTPClientPool.Client client = clientPool.get()) {
return newOutputStream(client, path, false, openOptions).out;
}
}
private FTPFileAndOutputStreamPair newOutputStream(FTPClientPool.Client client, FTPPath path, boolean requireFTPFile, OpenOptions options) throws IOException {
// retrieve the file unless create is true and createNew is false, because then the file can be created
FTPFile ftpFile = null;
if (!options.create || options.createNew) {
ftpFile = findFTPFile(client, path);
if (ftpFile != null && ftpFile.isDirectory()) {
throw Messages.fileSystemProvider().isDirectory(path.path());
}
if (!options.createNew && ftpFile == null) {
throw new NoSuchFileException(path.path());
} else if (options.createNew && ftpFile != null) {
throw new FileAlreadyExistsException(path.path());
}
}
// else the file can be created if necessary
if (ftpFile == null && requireFTPFile) {
ftpFile = findFTPFile(client, path);
}
OutputStream out = client.newOutputStream(path.path(), options);
return new FTPFileAndOutputStreamPair(ftpFile, out);
}
SeekableByteChannel newByteChannel(FTPPath path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
if (attrs.length > 0) {
throw Messages.fileSystemProvider().unsupportedCreateFileAttribute(attrs[0].name());
}
OpenOptions openOptions = OpenOptions.forNewByteChannel(options);
try (FTPClientPool.Client client = clientPool.get()) {
if (openOptions.read) {
// use findFTPFile instead of getFTPFile, to let the opening of the stream provide the correct error message
FTPFile ftpFile = findFTPFile(client, path);
InputStream in = newInputStream(client, path, openOptions);
long size = ftpFile == null ? 0 : ftpFile.getSize();
return FileSystemProviderSupport.createSeekableByteChannel(in, size);
}
// if append then we need the FTP file, to find the initial position of the channel
boolean requireFTPFile = openOptions.append;
FTPFileAndOutputStreamPair outPair = newOutputStream(client, path, requireFTPFile, openOptions);
long initialPosition = outPair.ftpFile == null ? 0 : outPair.ftpFile.getSize();
return FileSystemProviderSupport.createSeekableByteChannel(outPair.out, initialPosition);
}
}
DirectoryStream<Path> newDirectoryStream(final FTPPath path, Filter<? super Path> filter) throws IOException {
List<FTPFile> children;
try (FTPClientPool.Client client = clientPool.get()) {
children = ftpFileStrategy.getChildren(client, path);
}
return new FTPPathDirectoryStream(path, children, filter);
}
void createDirectory(FTPPath path, FileAttribute<?>... attrs) throws IOException {
if (attrs.length > 0) {
throw Messages.fileSystemProvider().unsupportedCreateFileAttribute(attrs[0].name());
}
try (FTPClientPool.Client client = clientPool.get()) {
client.mkdir(path.path());
}
}
void delete(FTPPath path) throws IOException {
try (FTPClientPool.Client client = clientPool.get()) {
FTPFile ftpFile = getFTPFile(client, path);
boolean isDirectory = ftpFile.isDirectory();
client.delete(path.path(), isDirectory);
}
}
FTPPath readSymbolicLink(FTPPath path) throws IOException {
try (FTPClientPool.Client client = clientPool.get()) {
FTPFile ftpFile = getFTPFile(client, path);
FTPFile link = getLink(client, ftpFile, path);
if (link == null) {
throw new NotLinkException(path.path());
}
return path.resolveSibling(link.getLink());
}
}
void copy(FTPPath source, FTPPath target, CopyOption... options) throws IOException {
boolean sameFileSystem = source.getFileSystem() == target.getFileSystem();
CopyOptions copyOptions = CopyOptions.forCopy(options);
try (FTPClientPool.Client client = clientPool.get()) {
// get the FTP file to determine whether a directory needs to be created or a file needs to be copied
// Files.copy specifies that for links, the final target must be copied
FTPPathAndFilePair sourcePair = toRealPath(client, source, true);
if (!sameFileSystem) {
copyAcrossFileSystems(client, source, sourcePair.ftpFile, target, copyOptions);
return;
}
try {
if (sourcePair.ftpPath.path().equals(toRealPath(client, target, true).ftpPath.path())) {
// non-op, don't do a thing as specified by Files.copy
return;
}
} catch (NoSuchFileException e) {
// the target does not exist or either path is an invalid link, ignore the error and continue
}
FTPFile targetFtpFile = findFTPFile(client, target);
if (targetFtpFile != null) {
if (copyOptions.replaceExisting) {
client.delete(target.path(), targetFtpFile.isDirectory());
} else {
throw new FileAlreadyExistsException(target.path());
}
}
if (sourcePair.ftpFile.isDirectory()) {
client.mkdir(target.path());
} else {
try (FTPClientPool.Client client2 = clientPool.getOrCreate()) {
copyFile(client, source, client2, target, copyOptions);
}
}
}
}
private void copyAcrossFileSystems(FTPClientPool.Client sourceClient, FTPPath source, FTPFile sourceFtpFile, FTPPath target, CopyOptions options)
throws IOException {
try (FTPClientPool.Client targetClient = target.getFileSystem().clientPool.getOrCreate()) {
FTPFile targetFtpFile = findFTPFile(targetClient, target);
if (targetFtpFile != null) {
if (options.replaceExisting) {
targetClient.delete(target.path(), targetFtpFile.isDirectory());
} else {
throw new FileAlreadyExistsException(target.path());
}
}
if (sourceFtpFile.isDirectory()) {
sourceClient.mkdir(target.path());
} else {
copyFile(sourceClient, source, targetClient, target, options);
}
}
}
private void copyFile(FTPClientPool.Client sourceClient, FTPPath source, FTPClientPool.Client targetClient, FTPPath target, CopyOptions options) throws IOException {
OpenOptions inOptions = OpenOptions.forNewInputStream(options.toOpenOptions(StandardOpenOption.READ));
OpenOptions outOptions = OpenOptions
.forNewOutputStream(options.toOpenOptions(StandardOpenOption.WRITE, StandardOpenOption.CREATE));
try (InputStream in = sourceClient.newInputStream(source.path(), inOptions)) {
targetClient.storeFile(target.path(), in, outOptions, outOptions.options);
}
}
void move(FTPPath source, FTPPath target, CopyOption... options) throws IOException {
boolean sameFileSystem = source.getFileSystem() == target.getFileSystem();
CopyOptions copyOptions = CopyOptions.forMove(sameFileSystem, options);
try (FTPClientPool.Client client = clientPool.get()) {
if (!sameFileSystem) {
FTPFile ftpFile = getFTPFile(client, source);
if (getLink(client, ftpFile, source) != null) {
throw new IOException(FTPMessages.copyOfSymbolicLinksAcrossFileSystemsNotSupported());
}
copyAcrossFileSystems(client, source, ftpFile, target, copyOptions);
client.delete(source.path(), ftpFile.isDirectory());
return;
}
try {
if (isSameFile(client, source, target)) {
// non-op, don't do a thing as specified by Files.move
return;
}
} catch (NoSuchFileException e) {
// the source or target does not exist or either path is an invalid link
// call getFTPFile to ensure the source file exists
// ignore any error to target or if the source link is invalid
getFTPFile(client, source);
}
if (toAbsolutePath(source).parentPath() == null) {
// cannot move or rename the root
throw new DirectoryNotEmptyException(source.path());
}
FTPFile targetFTPFile = findFTPFile(client, target);
if (copyOptions.replaceExisting && targetFTPFile != null) {
client.delete(target.path(), targetFTPFile.isDirectory());
}
client.rename(source.path(), target.path());
}
}
boolean isSameFile(FTPPath path, FTPPath path2) throws IOException {
if (path.getFileSystem() != path2.getFileSystem()) {
return false;
}
if (path.equals(path2)) {
return true;
}
try (FTPClientPool.Client client = clientPool.get()) {
return isSameFile(client, path, path2);
}
}
private boolean isSameFile(FTPClientPool.Client client, FTPPath path, FTPPath path2) throws IOException {
if (path.equals(path2)) {
return true;
}
return toRealPath(client, path, true).ftpPath.path().equals(toRealPath(client, path2, true).ftpPath.path());
}
boolean isHidden(FTPPath path) throws IOException {
// call getFTPFile to check for existence
try (FTPClientPool.Client client = clientPool.get()) {
getFTPFile(client, path);
}
String fileName = path.fileName();
return !CURRENT_DIR.equals(fileName) && !PARENT_DIR.equals(fileName) && fileName.startsWith(".");
}
FileStore getFileStore(FTPPath path) throws IOException {
// call getFTPFile to check existence of the path
try (FTPClientPool.Client client = clientPool.get()) {
getFTPFile(client, path);
}
return fileStore;
}
void checkAccess(FTPPath path, AccessMode... modes) throws IOException {
try (FTPClientPool.Client client = clientPool.get()) {
FTPFile ftpFile = getFTPFile(client, path);
for (AccessMode mode : modes) {
if (!hasAccess(ftpFile, mode)) {
throw new AccessDeniedException(path.path());
}
}
}
}
private boolean hasAccess(FTPFile ftpFile, AccessMode mode) {
switch (mode) {
case READ:
return ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION);
case WRITE:
return ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION);
case EXECUTE:
return ftpFile.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION);
default:
return false;
}
}
PosixFileAttributes readAttributes(FTPPath path, LinkOption... options) throws IOException {
boolean followLinks = LinkOptionSupport.followLinks(options);
try (FTPClientPool.Client client = clientPool.get()) {
FTPPathAndFilePair pair = toRealPath(client, path, followLinks);
ZonedDateTime lastModified = client.mdtm(pair.ftpPath.path());
FTPFile link = followLinks ? null : getLink(client, pair.ftpFile, path);
FTPFile ftpFile = link == null ? pair.ftpFile : link;
return new FTPPathFileAttributes(ftpFile, lastModified);
}
}
Map<String, Object> readAttributes(FTPPath path, String attributes, LinkOption... options) throws IOException {
String view;
int pos = attributes.indexOf(':');
if (pos == -1) {
view = "basic";
attributes = "basic:" + attributes;
} else {
view = attributes.substring(0, pos);
}
if (!SUPPORTED_FILE_ATTRIBUTE_VIEWS.contains(view)) {
throw Messages.fileSystemProvider().unsupportedFileAttributeView(view);
}
Set<String> allowedAttributes;
if (attributes.startsWith("basic:")) {
allowedAttributes = BASIC_ATTRIBUTES;
} else if (attributes.startsWith("owner:")) {
allowedAttributes = OWNER_ATTRIBUTES;
} else if (attributes.startsWith("posix:")) {
allowedAttributes = POSIX_ATTRIBUTES;
} else {
// should not occur
throw Messages.fileSystemProvider().unsupportedFileAttributeView(attributes.substring(0, attributes.indexOf(':')));
}
Map<String, Object> result = getAttributeMap(attributes, allowedAttributes);
PosixFileAttributes posixAttributes = readAttributes(path, options);
for (Map.Entry<String, Object> entry : result.entrySet()) {
switch (entry.getKey()) {
case "basic:lastModifiedTime":
case "posix:lastModifiedTime":
entry.setValue(posixAttributes.lastModifiedTime());
break;
case "basic:lastAccessTime":
case "posix:lastAccessTime":
entry.setValue(posixAttributes.lastAccessTime());
break;
case "basic:creationTime":
case "posix:creationTime":
entry.setValue(posixAttributes.creationTime());
break;
case "basic:size":
case "posix:size":
entry.setValue(posixAttributes.size());
break;
case "basic:isRegularFile":
case "posix:isRegularFile":
entry.setValue(posixAttributes.isRegularFile());
break;
case "basic:isDirectory":
case "posix:isDirectory":
entry.setValue(posixAttributes.isDirectory());
break;
case "basic:isSymbolicLink":
case "posix:isSymbolicLink":
entry.setValue(posixAttributes.isSymbolicLink());
break;
case "basic:isOther":
case "posix:isOther":
entry.setValue(posixAttributes.isOther());
break;
case "basic:fileKey":
case "posix:fileKey":
entry.setValue(posixAttributes.fileKey());
break;
case "owner:owner":
case "posix:owner":
entry.setValue(posixAttributes.owner());
break;
case "posix:group":
entry.setValue(posixAttributes.group());
break;
case "posix:permissions":
entry.setValue(posixAttributes.permissions());
break;
default:
// should not occur
throw new IllegalStateException("unexpected attribute name: " + entry.getKey());
}
}
return result;
}
private Map<String, Object> getAttributeMap(String attributes, Set<String> allowedAttributes) {
int indexOfColon = attributes.indexOf(':');
String prefix = attributes.substring(0, indexOfColon + 1);
attributes = attributes.substring(indexOfColon + 1);
String[] attributeList = attributes.split(",");
Map<String, Object> result = new HashMap<>(allowedAttributes.size());
for (String attribute : attributeList) {
String prefixedAttribute = prefix + attribute;
if (allowedAttributes.contains(prefixedAttribute)) {
result.put(prefixedAttribute, null);
} else if ("*".equals(attribute)) {
for (String s : allowedAttributes) {
result.put(s, null);
}
} else {
throw Messages.fileSystemProvider().unsupportedFileAttribute(attribute);
}
}
return result;
}
FTPFile getFTPFile(FTPPath path) throws IOException {
try (FTPClientPool.Client client = clientPool.get()) {
return getFTPFile(client, path);
}
}
private FTPFile getFTPFile(FTPClientPool.Client client, FTPPath path) throws IOException {
return ftpFileStrategy.getFTPFile(client, path);
}
private FTPFile findFTPFile(FTPClientPool.Client client, FTPPath path) throws IOException {
try {
return getFTPFile(client, path);
} catch (NoSuchFileException e) {
return null;
}
}
private FTPFile getLink(FTPClientPool.Client client, FTPFile ftpFile, FTPPath path) throws IOException {
return ftpFileStrategy.getLink(client, ftpFile, path);
}
long getTotalSpace() {
// FTPClient does not support retrieving the total space
return Long.MAX_VALUE;
}
long getUsableSpace() {
// FTPClient does not support retrieving the usable space
return Long.MAX_VALUE;
}
long getUnallocatedSpace() {
// FTPClient does not support retrieving the unallocated space
return Long.MAX_VALUE;
}
private static final class FTPPathAndFilePair {
private final FTPPath ftpPath;
private final FTPFile ftpFile;
private FTPPathAndFilePair(FTPPath ftpPath, FTPFile ftpFile) {
this.ftpPath = ftpPath;
this.ftpFile = ftpFile;
}
}
private static final class FTPFileAndOutputStreamPair {
private final FTPFile ftpFile;
private final OutputStream out;
private FTPFileAndOutputStreamPair(FTPFile ftpFile, OutputStream out) {
this.ftpFile = ftpFile;
this.out = out;
}
}
private static final class FTPPathDirectoryStream extends AbstractDirectoryStream<Path> {
private final FTPPath path;
private final List<FTPFile> files;
private Iterator<FTPFile> iterator;
private FTPPathDirectoryStream(FTPPath path, List<FTPFile> files, Filter<? super Path> filter) {
super(filter);
this.path = path;
this.files = files;
}
@Override
protected void setupIteration() {
iterator = files.iterator();
}
@Override
protected Path getNext() throws IOException {
return iterator.hasNext() ? path.resolve(getFileName(iterator.next())) : null;
}
}
private static final class FTPPathFileAttributes implements PosixFileAttributes {
private static final FileTime EPOCH = FileTime.fromMillis(0L);
private final FTPFile ftpFile;
private final FileTime lastModified;
private FTPPathFileAttributes(FTPFile ftpFile, ZonedDateTime lastModified) {
this.ftpFile = ftpFile;
if (lastModified == null) {
ZonedDateTime timestamp = ftpFile.getTimestamp();
this.lastModified = timestamp == null ? EPOCH : FileTime.from(timestamp.toInstant());
} else {
this.lastModified = FileTime.from(lastModified.toInstant());
}
}
@Override
public UserPrincipal owner() {
String user = ftpFile.getUser();
return user == null ? null : new SimpleUserPrincipal(user);
}
@Override
public GroupPrincipal group() {
String group = ftpFile.getGroup();
return group == null ? null : new SimpleGroupPrincipal(group);
}
@Override
public Set<PosixFilePermission> permissions() {
Set<PosixFilePermission> permissions = EnumSet.noneOf(PosixFilePermission.class);
addPermissionIfSet(ftpFile, FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, PosixFilePermission.OWNER_READ, permissions);
addPermissionIfSet(ftpFile, FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, PosixFilePermission.OWNER_WRITE, permissions);
addPermissionIfSet(ftpFile, FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION, PosixFilePermission.OWNER_EXECUTE, permissions);
addPermissionIfSet(ftpFile, FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION, PosixFilePermission.GROUP_READ, permissions);
addPermissionIfSet(ftpFile, FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION, PosixFilePermission.GROUP_WRITE, permissions);
addPermissionIfSet(ftpFile, FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION, PosixFilePermission.GROUP_EXECUTE, permissions);
addPermissionIfSet(ftpFile, FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION, PosixFilePermission.OTHERS_READ, permissions);
addPermissionIfSet(ftpFile, FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION, PosixFilePermission.OTHERS_WRITE, permissions);
addPermissionIfSet(ftpFile, FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION, PosixFilePermission.OTHERS_EXECUTE, permissions);
return permissions;
}
private void addPermissionIfSet(FTPFile ftpFile, int access, int permission, PosixFilePermission value,
Set<PosixFilePermission> permissions) {
if (ftpFile.hasPermission(access, permission)) {
permissions.add(value);
}
}
@Override
public FileTime lastModifiedTime() {
return lastModified;
}
@Override
public FileTime lastAccessTime() {
return lastModifiedTime();
}
@Override
public FileTime creationTime() {
return lastModifiedTime();
}
@Override
public boolean isRegularFile() {
return ftpFile.isFile();
}
@Override
public boolean isDirectory() {
return ftpFile.isDirectory();
}
@Override
public boolean isSymbolicLink() {
return ftpFile.isSymbolicLink();
}
@Override
public boolean isOther() {
return false;
}
@Override
public long size() {
return ftpFile.getSize();
}
@Override
public Object fileKey() {
return null;
}
}
}

@ -0,0 +1,62 @@
package org.xbib.io.ftp.fs;
import java.nio.file.FileSystemException;
/**
* An exception that is thrown if an FTP command does not execute successfully.
*/
public class FTPFileSystemException extends FileSystemException implements FTPResponse {
private static final long serialVersionUID = 3914421047186137133L;
private final int replyCode;
/**
* Creates a new {@code FTPFileSystemException}.
* This constructor should be used when an operation not involving files fails.
*
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this exception.
* @param replyString The entire text from the last FTP response that triggered this exception. It will be used as the exception's reason.
*/
public FTPFileSystemException(int replyCode, String replyString) {
super(null, null, replyString);
this.replyCode = replyCode;
}
/**
* Creates a new {@code FTPFileSystemException}.
* This constructor should be used when an operation involving one file fails.
*
* @param file A string identifying the file, or {@code null} if not known.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this exception.
* @param replyString The entire text from the last FTP response that triggered this exception. It will be used as the exception's reason.
*/
public FTPFileSystemException(String file, int replyCode, String replyString) {
super(file, null, replyString);
this.replyCode = replyCode;
}
/**
* Creates a new {@code FTPFileSystemException}.
* This constructor should be used when an operation involving two files fails.
*
* @param file A string identifying the file, or {@code null} if not known.
* @param other A string identifying the other file, or {@code null} if there isn't another file or if not known.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this exception.
* @param replyString The entire text from the last FTP response that triggered this exception. It will be used as the exception's reason.
*/
public FTPFileSystemException(String file, String other, int replyCode, String replyString) {
super(file, other, replyString);
this.replyCode = replyCode;
}
@Override
public int getReplyCode() {
return replyCode;
}
@Override
public String getReplyString() {
return getReason();
}
}

@ -0,0 +1,513 @@
package org.xbib.io.ftp.fs;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessMode;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.ProviderMismatchException;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.GroupPrincipal;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.UserPrincipal;
import java.nio.file.spi.FileSystemProvider;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* A provider for FTP file systems.
*/
public class FTPFileSystemProvider extends FileSystemProvider {
private final Map<URI, FTPFileSystem> fileSystems = new HashMap<>();
private static FTPPath toFTPPath(Path path) {
Objects.requireNonNull(path);
if (path instanceof FTPPath) {
return (FTPPath) path;
}
throw new ProviderMismatchException();
}
/**
* Send a keep-alive signal for an FTP file system.
*
* @param fs The FTP file system to send a keep-alive signal for.
* @throws ProviderMismatchException If the given file system is not an FTP file system
* (not created by an {@code FTPFileSystemProvider}).
* @throws IOException If an I/O error occurred.
*/
public static void keepAlive(FileSystem fs) throws IOException {
if (fs instanceof FTPFileSystem) {
((FTPFileSystem) fs).keepAlive();
}
throw new ProviderMismatchException();
}
/**
* Returns the URI scheme that identifies this provider: {@code ftp}.
*/
@Override
public String getScheme() {
return "ftp";
}
/**
* Constructs a new {@code FileSystem} object identified by a URI.
* <p>
* The URI must have a {@link URI#getScheme() scheme} equal to {@link #getScheme()},
* and no {@link URI#getUserInfo() user information},
* {@link URI#getPath() path}, {@link URI#getQuery() query} or {@link URI#getFragment() fragment}.
* Authentication credentials must be set through
* the given environment map, preferably through {@link FTPEnvironment}.
* <p>
* This provider allows multiple file systems per host, but only one file system per user on a host.
* Once a file system is {@link FileSystem#close() closed}, this provider allows a new file system
* to be created with the same URI and credentials
* as the closed file system.
*/
@Override
public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
// user info must come from the environment map
checkURI(uri, false, false);
FTPEnvironment environment = wrapEnvironment(env);
String username = environment.getUsername();
URI normalizedURI = normalizeWithUsername(uri, username);
synchronized (fileSystems) {
if (fileSystems.containsKey(normalizedURI)) {
throw new FileSystemAlreadyExistsException(normalizedURI.toString());
}
FTPFileSystem fs = new FTPFileSystem(this, normalizedURI, environment);
fileSystems.put(normalizedURI, fs);
return fs;
}
}
FTPEnvironment wrapEnvironment(Map<String, ?> env) {
return FTPEnvironment.wrap(env);
}
/**
* Returns an existing {@code FileSystem} created by this provider.
* The URI must have a {@link URI#getScheme() scheme} equal to {@link #getScheme()},
* and no {@link URI#getPath() path},
* {@link URI#getQuery() query} or {@link URI#getFragment() fragment}.
* Because the original credentials were provided through an environment map,
* the URI can contain {@link URI#getUserInfo() user information}, although this should not
* contain a password for security reasons.
* Once a file system is {@link FileSystem#close() closed},
* this provided will throw a {@link FileSystemNotFoundException}.
*/
@Override
public FileSystem getFileSystem(URI uri) {
checkURI(uri, true, false);
return getExistingFileSystem(uri);
}
/**
* Return a {@code Path} object by converting the given {@link URI}. The resulting {@code Path}
* is associated with a {@link FileSystem} that
* already exists. This method does not support constructing {@code FileSystem}s automatically.
* <p>
* The URI must have a {@link URI#getScheme() scheme} equal to {@link #getScheme()},
* and no {@link URI#getQuery() query} or
* {@link URI#getFragment() fragment}. Because the original credentials were provided through an environment map,
* the URI can contain {@link URI#getUserInfo() user information},
* although this should not contain a password for security reasons.
*/
@Override
public Path getPath(URI uri) {
checkURI(uri, true, true);
FTPFileSystem fs = getExistingFileSystem(uri);
return fs.getPath(uri.getPath());
}
private FTPFileSystem getExistingFileSystem(URI uri) {
URI normalizedURI = normalizeWithoutPassword(uri);
synchronized (fileSystems) {
FTPFileSystem fs = fileSystems.get(normalizedURI);
if (fs == null) {
throw new FileSystemNotFoundException(uri.toString());
}
return fs;
}
}
private void checkURI(URI uri, boolean allowUserInfo, boolean allowPath) {
if (!uri.isAbsolute()) {
throw Messages.uri().notAbsolute(uri);
}
if (!getScheme().equalsIgnoreCase(uri.getScheme())) {
throw Messages.uri().invalidScheme(uri, getScheme());
}
if (!allowUserInfo && uri.getUserInfo() != null && !uri.getUserInfo().isEmpty()) {
throw Messages.uri().hasUserInfo(uri);
}
if (uri.isOpaque()) {
throw Messages.uri().notHierarchical(uri);
}
if (!allowPath && uri.getPath() != null && !uri.getPath().isEmpty()) {
throw Messages.uri().hasPath(uri);
}
if (uri.getQuery() != null && !uri.getQuery().isEmpty()) {
throw Messages.uri().hasQuery(uri);
}
if (uri.getFragment() != null && !uri.getFragment().isEmpty()) {
throw Messages.uri().hasFragment(uri);
}
}
void removeFileSystem(URI uri) {
URI normalizedURI = normalizeWithoutPassword(uri);
synchronized (fileSystems) {
fileSystems.remove(normalizedURI);
}
}
private URI normalizeWithoutPassword(URI uri) {
String userInfo = uri.getUserInfo();
if (userInfo == null && uri.getPath() == null && uri.getQuery() == null && uri.getFragment() == null) {
// nothing to normalize, return the URI
return uri;
}
String username = null;
if (userInfo != null) {
int index = userInfo.indexOf(':');
username = index == -1 ? userInfo : userInfo.substring(0, index);
}
// no path, query or fragment
return URISupport.create(uri.getScheme(), username, uri.getHost(), uri.getPort(), null, null, null);
}
private URI normalizeWithUsername(URI uri, String username) {
if (username == null && uri.getUserInfo() == null && uri.getPath() == null && uri.getQuery() == null && uri.getFragment() == null) {
// nothing to normalize or add, return the URI
return uri;
}
// no path, query or fragment
return URISupport.create(uri.getScheme(), username, uri.getHost(), uri.getPort(), null, null, null);
}
/**
* Opens a file, returning an input stream to read from the file.
* This method works in exactly the manner specified by the {@link Files#newInputStream(Path, OpenOption...)} method.
* <p>
* In addition to the standard open options, this method also supports single occurrences of each of
* {@link FileType}, {@link FileStructure} and
* {@link FileTransferMode}. These three option types, with defaults of {@link FileType#binary()},
* {@link FileStructure#FILE} and
* {@link FileTransferMode#STREAM}, persist for all calls that support file transfers:
* <ul>
* <li>{@link #newInputStream(Path, OpenOption...)}</li>
* <li>{@link #newOutputStream(Path, OpenOption...)}</li>
* <li>{@link #newByteChannel(Path, Set, FileAttribute...)}</li>
* <li>{@link #copy(Path, Path, CopyOption...)}</li>
* <li>{@link #move(Path, Path, CopyOption...)}</li>
* </ul>
* <p>
* Note: while the returned input stream is not closed, the path's file system will have
* one available connection fewer.
* It is therefore essential that the input stream is closed as soon as possible.
*/
@Override
public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
return toFTPPath(path).newInputStream(options);
}
/**
* Opens or creates a file, returning an output stream that may be used to write bytes to the file.
* This method works in exactly the manner specified by the {@link Files#newOutputStream(Path, OpenOption...)} method.
* <p>
* In addition to the standard open options, this method also supports single occurrences of each of
* {@link FileType}, {@link FileStructure} and
* {@link FileTransferMode}. These three option types, with defaults of {@link FileType#binary()},
* {@link FileStructure#FILE} and
* {@link FileTransferMode#STREAM}, persist for all calls that support file transfers:
* <ul>
* <li>{@link #newInputStream(Path, OpenOption...)}</li>
* <li>{@link #newOutputStream(Path, OpenOption...)}</li>
* <li>{@link #newByteChannel(Path, Set, FileAttribute...)}</li>
* <li>{@link #copy(Path, Path, CopyOption...)}</li>
* <li>{@link #move(Path, Path, CopyOption...)}</li>
* </ul>
* <p>
* Note: while the returned output stream is not closed, the path's file system will have one available
* connection fewer.
* It is therefore essential that the output stream is closed as soon as possible.
*/
@Override
public OutputStream newOutputStream(Path path, OpenOption... options) throws IOException {
return toFTPPath(path).newOutputStream(options);
}
/**
* Opens or creates a file, returning a seekable byte channel to access the file.
* This method works in exactly the manner specified by the
* {@link Files#newByteChannel(Path, Set, FileAttribute...)} method.
* <p>
* In addition to the standard open options, this method also supports single occurrences of
* each of {@link FileType}, {@link FileStructure} and
* {@link FileTransferMode}. These three option types, with defaults of {@link FileType#binary()},
* {@link FileStructure#FILE} and
* {@link FileTransferMode#STREAM}, persist for all calls that support file transfers:
* <ul>
* <li>{@link #newInputStream(Path, OpenOption...)}</li>
* <li>{@link #newOutputStream(Path, OpenOption...)}</li>
* <li>{@link #newByteChannel(Path, Set, FileAttribute...)}</li>
* <li>{@link #copy(Path, Path, CopyOption...)}</li>
* <li>{@link #move(Path, Path, CopyOption...)}</li>
* </ul>
* <p>
* This method does not support any file attributes to be set. If any file attributes are given,
* an {@link UnsupportedOperationException} will be
* thrown.
* <p>
* Note: while the returned channel is not closed, the path's file system will have one available connection fewer.
* It is therefore essential that the channel is closed as soon as possible.
*/
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options,
FileAttribute<?>... attrs) throws IOException {
return toFTPPath(path).newByteChannel(options, attrs);
}
@Override
public DirectoryStream<Path> newDirectoryStream(Path dir, Filter<? super Path> filter) throws IOException {
return toFTPPath(dir).newDirectoryStream(filter);
}
/**
* Creates a new directory.
* This method works in exactly the manner specified by the
* {@link Files#createDirectory(Path, FileAttribute...)} method.
* <p>
* This method does not support any file attributes to be set.
* If any file attributes are given, an {@link UnsupportedOperationException} will be
* thrown.
*/
@Override
public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
toFTPPath(dir).createDirectory(attrs);
}
@Override
public void delete(Path path) throws IOException {
toFTPPath(path).delete();
}
@Override
public Path readSymbolicLink(Path link) throws IOException {
return toFTPPath(link).readSymbolicLink();
}
/**
* Copy a file to a target file.
* This method works in exactly the manner specified by the {@link Files#copy(Path, Path, CopyOption...)}
* method except that both the source and
* target paths must be associated with this provider.
* <p>
* In addition to the standard copy options, this method also supports single occurrences of each of
* {@link FileType}, {@link FileStructure} and
* {@link FileTransferMode}. These three option types, with defaults of {@link FileType#binary()},
* {@link FileStructure#FILE} and
* {@link FileTransferMode#STREAM}, persist for all calls that support file transfers:
* <ul>
* <li>{@link #newInputStream(Path, OpenOption...)}</li>
* <li>{@link #newOutputStream(Path, OpenOption...)}</li>
* <li>{@link #newByteChannel(Path, Set, FileAttribute...)}</li>
* <li>{@link #copy(Path, Path, CopyOption...)}</li>
* <li>{@link #move(Path, Path, CopyOption...)}</li>
* </ul>
* <p>
* {@link StandardCopyOption#COPY_ATTRIBUTES} and {@link StandardCopyOption#ATOMIC_MOVE} are not supported though.
*/
@Override
public void copy(Path source, Path target, CopyOption... options) throws IOException {
toFTPPath(source).copy(toFTPPath(target), options);
}
/**
* Move or rename a file to a target file.
* This method works in exactly the manner specified by the {@link Files#move(Path, Path, CopyOption...)}
* method except that both the source and
* target paths must be associated with this provider.
* <p>
* In addition to the standard copy options, this method also supports single occurrences of each of
* {@link FileType}, {@link FileStructure} and
* {@link FileTransferMode}. These three option types, with defaults of {@link FileType#binary()},
* {@link FileStructure#FILE} and
* {@link FileTransferMode#STREAM}, persist for all calls that support file transfers:
* <ul>
* <li>{@link #newInputStream(Path, OpenOption...)}</li>
* <li>{@link #newOutputStream(Path, OpenOption...)}</li>
* <li>{@link #newByteChannel(Path, Set, FileAttribute...)}</li>
* <li>{@link #copy(Path, Path, CopyOption...)}</li>
* <li>{@link #move(Path, Path, CopyOption...)}</li>
* </ul>
* <p>
* {@link StandardCopyOption#COPY_ATTRIBUTES} is not supported though.
* {@link StandardCopyOption#ATOMIC_MOVE} is only supported if the paths have
* the same file system.
*/
@Override
public void move(Path source, Path target, CopyOption... options) throws IOException {
toFTPPath(source).move(toFTPPath(target), options);
}
@Override
public boolean isSameFile(Path path, Path path2) throws IOException {
return toFTPPath(path).isSameFile(path2);
}
@Override
public boolean isHidden(Path path) throws IOException {
return toFTPPath(path).isHidden();
}
@Override
public FileStore getFileStore(Path path) throws IOException {
return toFTPPath(path).getFileStore();
}
@Override
public void checkAccess(Path path, AccessMode... modes) throws IOException {
toFTPPath(path).checkAccess(modes);
}
/**
* Returns a file attribute view of a given type.
* This method works in exactly the manner specified by the
* {@link Files#getFileAttributeView(Path, Class, LinkOption...)} method.
* <p>
* This provider supports {@link BasicFileAttributeView}, {@link FileOwnerAttributeView} and
* {@link PosixFileAttributeView}.
* All other classes will result in a {@code null} return value.
* <p>
* Note that the returned {@link FileAttributeView} is read-only; any attempt to change any attributes
* through the view will result in an
* {@link UnsupportedOperationException} to be thrown.
*/
@Override
public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
Objects.requireNonNull(type);
if (type == BasicFileAttributeView.class) {
return type.cast(new AttributeView("basic", toFTPPath(path)));
}
if (type == FileOwnerAttributeView.class) {
return type.cast(new AttributeView("owner", toFTPPath(path)));
}
if (type == PosixFileAttributeView.class) {
return type.cast(new AttributeView("posix", toFTPPath(path)));
}
return null;
}
/**
* Reads a file's attributes as a bulk operation.
* This method works in exactly the manner specified by the
* {@link Files#readAttributes(Path, Class, LinkOption...)} method.
* This provider supports {@link BasicFileAttributes} and {@link PosixFileAttributes}
* (there is no {@code FileOwnerFileAttributes}).
* All other classes will result in an {@link UnsupportedOperationException} to be thrown.
*/
@Override
public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options)
throws IOException {
if (type == BasicFileAttributes.class || type == PosixFileAttributes.class) {
return type.cast(toFTPPath(path).readAttributes(options));
}
throw Messages.fileSystemProvider().unsupportedFileAttributesType(type);
}
/**
* Reads a set of file attributes as a bulk operation.
* This method works in exactly the manner specified by the {@link Files#readAttributes(Path, String, LinkOption...)} method.
* <p>
* This provider supports views {@code basic}, {@code owner} and {code posix}, where {@code basic} will be used if no view is given.
* All other views will result in an {@link UnsupportedOperationException} to be thrown.
*/
@Override
public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
return toFTPPath(path).readAttributes(attributes, options);
}
/**
* Sets the value of a file attribute.
* This method works in exactly the manner specified by the {@link Files#setAttribute(Path, String, Object, LinkOption...)} method.
* <p>
* This provider does not support attributes for paths to be set. This method will always throw an {@link UnsupportedOperationException}.
*/
@Override
public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
throw Messages.unsupportedOperation(FileSystemProvider.class, "setAttribute");
}
private static final class AttributeView implements PosixFileAttributeView {
private final String name;
private final FTPPath path;
private AttributeView(String name, FTPPath path) {
this.name = Objects.requireNonNull(name);
this.path = Objects.requireNonNull(path);
}
@Override
public String name() {
return name;
}
@Override
public UserPrincipal getOwner() throws IOException {
return readAttributes().owner();
}
@Override
public void setOwner(UserPrincipal owner) throws IOException {
throw Messages.unsupportedOperation(FileOwnerAttributeView.class, "setOwner");
}
@Override
public PosixFileAttributes readAttributes() throws IOException {
return path.readAttributes();
}
@Override
public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException {
throw Messages.unsupportedOperation(BasicFileAttributeView.class, "setTimes");
}
@Override
public void setGroup(GroupPrincipal group) throws IOException {
throw Messages.unsupportedOperation(PosixFileAttributeView.class, "setGroup");
}
@Override
public void setPermissions(Set<PosixFilePermission> perms) throws IOException {
throw Messages.unsupportedOperation(PosixFileAttributeView.class, "setPermissions");
}
}
}

@ -0,0 +1,26 @@
package org.xbib.io.ftp.fs;
import java.util.Locale;
import java.util.ResourceBundle;
/**
* A utility class for providing translated messages and exceptions.
*/
final class FTPMessages {
private static final String BUNDLE_NAME = "org.xbib.ftp.fs.messages";
private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME, Locale.ROOT, UTF8Control.INSTANCE);
private FTPMessages() {
throw new Error("cannot create instances of " + getClass().getName());
}
private static synchronized String getMessage(String key) {
return BUNDLE.getString(key);
}
public static String copyOfSymbolicLinksAcrossFileSystemsNotSupported() {
return getMessage("copyOfSymbolicLinksAcrossFileSystemsNotSupported");
}
}

@ -0,0 +1,48 @@
package org.xbib.io.ftp.fs;
import java.nio.file.NoSuchFileException;
/**
* An exception that is thrown if an FTP command does not execute successfully because a file does not exist.
*/
public class FTPNoSuchFileException extends NoSuchFileException implements FTPResponse {
private static final long serialVersionUID = 1547360368371410860L;
private final int replyCode;
/**
* Creates a new {@code FTPNoSuchFileException}.
*
* @param file A string identifying the file, or {@code null} if not known.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this exception.
* @param replyString The entire text from the last FTP response that triggered this exception. It will be used as the exception's reason.
*/
public FTPNoSuchFileException(String file, int replyCode, String replyString) {
super(file, null, replyString);
this.replyCode = replyCode;
}
/**
* Creates a new {@code FTPNoSuchFileException}.
*
* @param file A string identifying the file, or {@code null} if not known.
* @param other A string identifying the other file, or {@code null} if not known.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this exception.
* @param replyString The entire text from the last FTP response that triggered this exception. It will be used as the exception's reason.
*/
public FTPNoSuchFileException(String file, String other, int replyCode, String replyString) {
super(file, other, replyString);
this.replyCode = replyCode;
}
@Override
public int getReplyCode() {
return replyCode;
}
@Override
public String getReplyString() {
return getReason();
}
}

@ -0,0 +1,47 @@
package org.xbib.io.ftp.fs;
import java.nio.file.NotDirectoryException;
/**
* An exception that is thrown if an FTP command does not execute successfully because a file is not a directory.
*/
public class FTPNotDirectoryException extends NotDirectoryException implements FTPResponse {
private static final long serialVersionUID = -37768328123340304L;
private final int replyCode;
private final String replyString;
/**
* Creates a new {@code FTPNotLinkException}.
*
* @param file A string identifying the file, or {@code null} if not known.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this exception.
* @param replyString The entire text from the last FTP response that triggered this exception. It will be used as the exception's reason.
*/
public FTPNotDirectoryException(String file, int replyCode, String replyString) {
super(file);
this.replyCode = replyCode;
this.replyString = replyString;
}
@Override
public int getReplyCode() {
return replyCode;
}
@Override
public String getReplyString() {
return replyString;
}
@Override
public String getReason() {
return replyString;
}
@Override
public String getMessage() {
return replyString;
}
}

@ -0,0 +1,48 @@
package org.xbib.io.ftp.fs;
import java.nio.file.NotLinkException;
/**
* An exception that is thrown if an FTP command does not execute successfully because a file is not a symbolic link.
*/
public class FTPNotLinkException extends NotLinkException implements FTPResponse {
private static final long serialVersionUID = 2100528879214315190L;
private final int replyCode;
/**
* Creates a new {@code FTPNotLinkException}.
*
* @param file A string identifying the file, or {@code null} if not known.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this exception.
* @param replyString The entire text from the last FTP response that triggered this exception. It will be used as the exception's reason.
*/
public FTPNotLinkException(String file, int replyCode, String replyString) {
super(file, null, replyString);
this.replyCode = replyCode;
}
/**
* Creates a new {@code FTPNotLinkException}.
*
* @param file A string identifying the file, or {@code null} if not known.
* @param other A string identifying the other file, or {@code null} if not known.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this exception.
* @param replyString The entire text from the last FTP response that triggered this exception. It will be used as the exception's reason.
*/
public FTPNotLinkException(String file, String other, int replyCode, String replyString) {
super(file, other, replyString);
this.replyCode = replyCode;
}
@Override
public int getReplyCode() {
return replyCode;
}
@Override
public String getReplyString() {
return getReason();
}
}

@ -0,0 +1,198 @@
package org.xbib.io.ftp.fs;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessMode;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.FileStore;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchEvent.Modifier;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFileAttributes;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* A path for FTP file systems.
*/
class FTPPath extends SimpleAbstractPath {
private final FTPFileSystem fs;
FTPPath(FTPFileSystem fs, String path) {
super(path);
this.fs = Objects.requireNonNull(fs);
}
private FTPPath(FTPFileSystem fs, String path, boolean normalized) {
super(path, normalized);
this.fs = Objects.requireNonNull(fs);
}
@Override
protected FTPPath createPath(String path) {
return new FTPPath(fs, path, true);
}
@Override
public FTPFileSystem getFileSystem() {
return fs;
}
@Override
public FTPPath getRoot() {
return (FTPPath) super.getRoot();
}
@Override
public FTPPath getFileName() {
return (FTPPath) super.getFileName();
}
@Override
public FTPPath getParent() {
return (FTPPath) super.getParent();
}
@Override
public FTPPath getName(int index) {
return (FTPPath) super.getName(index);
}
@Override
public FTPPath subpath(int beginIndex, int endIndex) {
return (FTPPath) super.subpath(beginIndex, endIndex);
}
@Override
public FTPPath normalize() {
return (FTPPath) super.normalize();
}
@Override
public FTPPath resolve(Path other) {
return (FTPPath) super.resolve(other);
}
@Override
public FTPPath resolve(String other) {
return (FTPPath) super.resolve(other);
}
@Override
public FTPPath resolveSibling(Path other) {
return (FTPPath) super.resolveSibling(other);
}
@Override
public FTPPath resolveSibling(String other) {
return (FTPPath) super.resolveSibling(other);
}
@Override
public FTPPath relativize(Path other) {
return (FTPPath) super.relativize(other);
}
@Override
public URI toUri() {
return fs.toUri(this);
}
@Override
public FTPPath toAbsolutePath() {
return fs.toAbsolutePath(this);
}
@Override
public FTPPath toRealPath(LinkOption... options) throws IOException {
return fs.toRealPath(this, options);
}
@Override
public WatchKey register(WatchService watcher, Kind<?>[] events, Modifier... modifiers) throws IOException {
throw Messages.unsupportedOperation(Path.class, "register");
}
@Override
public String toString() {
return fs.toString(this);
}
InputStream newInputStream(OpenOption... options) throws IOException {
return fs.newInputStream(this, options);
}
OutputStream newOutputStream(OpenOption... options) throws IOException {
return fs.newOutputStream(this, options);
}
SeekableByteChannel newByteChannel(Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
return fs.newByteChannel(this, options, attrs);
}
DirectoryStream<Path> newDirectoryStream(Filter<? super Path> filter) throws IOException {
return fs.newDirectoryStream(this, filter);
}
void createDirectory(FileAttribute<?>... attrs) throws IOException {
fs.createDirectory(this, attrs);
}
void delete() throws IOException {
fs.delete(this);
}
FTPPath readSymbolicLink() throws IOException {
return fs.readSymbolicLink(this);
}
void copy(FTPPath target, CopyOption... options) throws IOException {
fs.copy(this, target, options);
}
void move(FTPPath target, CopyOption... options) throws IOException {
fs.move(this, target, options);
}
boolean isSameFile(Path other) throws IOException {
if (this.equals(other)) {
return true;
}
if (other == null || getFileSystem() != other.getFileSystem()) {
return false;
}
return fs.isSameFile(this, (FTPPath) other);
}
boolean isHidden() throws IOException {
return fs.isHidden(this);
}
FileStore getFileStore() throws IOException {
return fs.getFileStore(this);
}
void checkAccess(AccessMode... modes) throws IOException {
fs.checkAccess(this, modes);
}
PosixFileAttributes readAttributes(LinkOption... options) throws IOException {
return fs.readAttributes(this, options);
}
Map<String, Object> readAttributes(String attributes, LinkOption... options) throws IOException {
return fs.readAttributes(this, attributes, options);
}
}

@ -0,0 +1,21 @@
package org.xbib.io.ftp.fs;
/**
* Represents a response from an FTP server.
*/
public interface FTPResponse {
/**
* Returns the reply code of the FTP response.
*
* @return The integer value of the reply code of the FTP response.
*/
int getReplyCode();
/**
* Returns the entire text from the FTP response.
*
* @return The entire text from the FTP response.
*/
String getReplyString();
}

@ -0,0 +1,38 @@
package org.xbib.io.ftp.fs;
import org.xbib.io.ftp.client.FTP;
import org.xbib.io.ftp.client.FTPClient;
import java.io.IOException;
import java.nio.file.CopyOption;
import java.nio.file.OpenOption;
/**
* The possible FTP file structures.
*/
public enum FileStructure implements OpenOption, CopyOption {
/**
* Indicates that files are to be treated as a continuous sequence of bytes.
*/
FILE(FTP.FILE_STRUCTURE),
/**
* Indicates that files are to be treated as a sequence of records.
*/
RECORD(FTP.RECORD_STRUCTURE),
/**
* Indicates that files are to be treated as a set of independent indexed pages.
*/
PAGE(FTP.PAGE_STRUCTURE),;
private final int structure;
FileStructure(int structure) {
this.structure = structure;
}
void apply(FTPClient client) throws IOException {
if (!client.setFileStructure(structure)) {
throw new FTPFileSystemException(client.getReplyCode(), client.getReplyString());
}
}
}

@ -0,0 +1,107 @@
package org.xbib.io.ftp.fs;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystemException;
import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.util.Collection;
/**
* A factory for creating {@link FileSystemException}s based on replies from an FTP server.
* It's not always possible to distinguish different types of errors. For instance, a 550 error code (file unavailable) could indicate that a file
* does not exist (which should trigger a {@link NoSuchFileException}), or that a file is inaccessible (which should trigger a
* {@link AccessDeniedException}), or possibly another reason.
* This interface allows users to provide their own mapping, based on both the reply code and the reply string from an FTP reply.
* Ideally implementations return exceptions that implement {@link FTPResponse}.
* This way, the original FTP reply code and message will be reserved.
*/
public interface FileSystemExceptionFactory {
/**
* Creates a {@code FileSystemException} that indicates a file or directory cannot be retrieved.
* <p>
* Note that the LIST command is used to retrieve a file or directory. This will often return with a 226 code even if a file or directory cannot
* be retrieved. This does not mean that the LIST call was actually successful.
*
* @param file A string identifying the file or directory.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this method call.
* @param replyString The entire text from the last FTP response that triggered this method call.
* @return The created {@code FileSystemException}.
*/
FileSystemException createGetFileException(String file, int replyCode, String replyString);
/**
* Creates a {@code FileSystemException} that indicates a directory cannot be used as the current working directory.
*
* @param directory A string identifying the directory.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this method call.
* @param replyString The entire text from the last FTP response that triggered this method call.
* @return The created {@code FileSystemException}.
*/
FileSystemException createChangeWorkingDirectoryException(String directory, int replyCode, String replyString);
/**
* Creates a {@code FileSystemException} that indicates a directory cannot be created.
*
* @param directory A string identifying the directory.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this method call.
* @param replyString The entire text from the last FTP response that triggered this method call.
* @return The created {@code FileSystemException}.
*/
FileAlreadyExistsException createCreateDirectoryException(String directory, int replyCode, String replyString);
/**
* Creates a {@code FileSystemException} that indicates a file or directory cannot be deleted.
*
* @param file A string identifying the file or directory.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this method call.
* @param replyString The entire text from the last FTP response that triggered this method call.
* @param isDirectory {@code true} if a directory cannot be deleted, or {@code false} if a file cannot be deleted.
* @return The created {@code FileSystemException}.
*/
FileSystemException createDeleteException(String file, int replyCode, String replyString, boolean isDirectory);
/**
* Creates a {@code FileSystemException} that indicates a file cannot be opened for reading.
*
* @param file A string identifying the file.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this method call.
* @param replyString The entire text from the last FTP response that triggered this method call.
* @return The created {@code FileSystemException}.
*/
FileSystemException createNewInputStreamException(String file, int replyCode, String replyString);
/**
* Creates a {@code FileSystemException} that indicates a file cannot be opened for writing.
*
* @param file A string identifying the file.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this method call.
* @param replyString The entire text from the last FTP response that triggered this method call.
* @param options The open options used to open the file.
* @return The created {@code FileSystemException}.
*/
FileSystemException createNewOutputStreamException(String file, int replyCode, String replyString, Collection<? extends OpenOption> options);
/**
* Creates a {@code FileSystemException} that indicates a file or directory cannot be copied.
*
* @param file A string identifying the file or directory to be copied.
* @param other A string identifying the file or directory to be copied to.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this method call.
* @param replyString The entire text from the last FTP response that triggered this method call.
* @return The created {@code FileSystemException}.
*/
FileSystemException createCopyException(String file, String other, int replyCode, String replyString);
/**
* Creates a {@code FileSystemException} that indicates a file or directory cannot be moved.
*
* @param file A string identifying the file or directory to be moved.
* @param other A string identifying the file or directory to be moved to.
* @param replyCode The integer value of the reply code of the last FTP reply that triggered this method call.
* @param replyString The entire text from the last FTP response that triggered this method call.
* @return The created {@code FileSystemException}.
*/
FileSystemException createMoveException(String file, String other, int replyCode, String replyString);
}

@ -0,0 +1,630 @@
package org.xbib.io.ftp.fs;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Path;
import java.nio.file.spi.FileSystemProvider;
import java.util.Map;
import java.util.Objects;
/**
* A utility class that can assist in implementing {@link FileSystemProvider}s.
*/
public final class FileSystemProviderSupport {
private FileSystemProviderSupport() {
}
/**
* Creates a {@link SeekableByteChannel} wrapped around an {@link InputStream}.
* This {@code SeekableByteChannel}, with an initial position of {@code 0} does not support seeking a specific position, truncating or writing.
*
* @param in The {@code InputStream} to wrap.
* @param size The size of the source of the {@code InputStream}.
* @return The created {@code SeekableByteChannel}.
* @throws NullPointerException If the given {@code InputStream} is {@code null}.
* @throws IllegalArgumentException If the given size of initial position is negative.
*/
public static SeekableByteChannel createSeekableByteChannel(final InputStream in, final long size) {
return createSeekableByteChannel(in, size, 0);
}
/**
* Creates a {@link SeekableByteChannel} wrapped around an {@link InputStream}.
* This {@code SeekableByteChannel} does not support seeking a specific position, truncating or writing.
*
* @param in The {@code InputStream} to wrap.
* @param size The size of the source of the {@code InputStream}.
* @param initialPosition The initial position of the returned {@code SeekableByteChannel}.
* @return The created {@code SeekableByteChannel}.
* @throws NullPointerException If the given {@code InputStream} is {@code null}.
* @throws IllegalArgumentException If the given size of initial position is negative.
*/
public static SeekableByteChannel createSeekableByteChannel(final InputStream in, final long size, final long initialPosition) {
Objects.requireNonNull(in);
if (size < 0) {
throw new IllegalArgumentException(size + " < 0");
}
if (initialPosition < 0) {
throw new IllegalArgumentException(initialPosition + " < 0");
}
return new SeekableByteChannel() {
private final ReadableByteChannel channel = Channels.newChannel(in);
private long position = initialPosition;
@Override
public boolean isOpen() {
return channel.isOpen();
}
@Override
public void close() throws IOException {
channel.close();
}
@Override
public int write(ByteBuffer src) throws IOException {
throw new NonWritableChannelException();
}
@Override
public SeekableByteChannel truncate(long size) throws IOException {
throw Messages.unsupportedOperation(SeekableByteChannel.class, "truncate");
}
@Override
public long size() throws IOException {
return size;
}
@Override
public int read(ByteBuffer dst) throws IOException {
int read = channel.read(dst);
if (read > 0) {
position += read;
}
return read;
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
throw Messages.unsupportedOperation(SeekableByteChannel.class, "position");
}
@Override
public long position() throws IOException {
return position;
}
};
}
/**
* Creates a {@link SeekableByteChannel} wrapped around an {@link OutputStream}.
* This {@code SeekableByteChannel}, with an initial position of {@code 0}, does not support seeking a specific position, truncating or reading.
*
* @param out The {@code OutputStream} to wrap.
* @return The created {@code SeekableByteChannel}.
* @throws NullPointerException If the given {@code OutputStream} is {@code null}.
*/
public static SeekableByteChannel createSeekableByteChannel(OutputStream out) {
return createSeekableByteChannel(out, 0);
}
/**
* Creates a {@link SeekableByteChannel} wrapped around an {@link OutputStream}.
* This {@code SeekableByteChannel} does not support seeking a specific position, truncating or reading.
*
* @param out The {@code OutputStream} to wrap.
* @param initialPosition The initial position of the returned {@code SeekableByteChannel}.
* @return The created {@code SeekableByteChannel}.
* @throws NullPointerException If the given {@code OutputStream} is {@code null}.
* @throws IllegalArgumentException If the given initial position is negative.
*/
public static SeekableByteChannel createSeekableByteChannel(final OutputStream out, final long initialPosition) {
Objects.requireNonNull(out);
if (initialPosition < 0) {
throw new IllegalArgumentException(initialPosition + " < 0");
}
return new SeekableByteChannel() {
private final WritableByteChannel channel = Channels.newChannel(out);
private long position = initialPosition;
@Override
public boolean isOpen() {
return channel.isOpen();
}
@Override
public void close() throws IOException {
channel.close();
}
@Override
public int write(ByteBuffer src) throws IOException {
int written = channel.write(src);
position += written;
return written;
}
@Override
public SeekableByteChannel truncate(long size) throws IOException {
throw Messages.unsupportedOperation(SeekableByteChannel.class, "truncate");
}
@Override
public long size() throws IOException {
return position;
}
@Override
public int read(ByteBuffer dst) throws IOException {
throw new NonReadableChannelException();
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
throw Messages.unsupportedOperation(SeekableByteChannel.class, "position");
}
@Override
public long position() throws IOException {
return position;
}
};
}
/**
* Retrieves a required boolean property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Boolean} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @return The value for the given property.
* @throws IllegalArgumentException If the property is not present in the given map, or if its value has an incompatible type.
*/
public static boolean getBooleanValue(Map<String, ?> env, String property) {
Object value = env.get(property);
if (value == null) {
throw Messages.fileSystemProvider().env().missingProperty(property);
}
if (value instanceof Boolean) {
return (Boolean) value;
}
if ("true".equals(value)) {
return true;
}
if ("false".equals(value)) {
return false;
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves an optional boolean property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Boolean} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @param defaultValue The value that should be used if the property is not in the given map.
* @return The value for the given property.
* @throws IllegalArgumentException If the property's value has an incompatible type.
*/
public static boolean getBooleanValue(Map<String, ?> env, String property, boolean defaultValue) {
Object value = env.get(property);
if (value == null) {
return defaultValue;
}
if (value instanceof Boolean) {
return (Boolean) value;
}
if ("true".equals(value)) {
return true;
}
if ("false".equals(value)) {
return false;
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves a required byte property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Byte} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @return The value for the given property.
* @throws IllegalArgumentException If the property is not present in the given map, or if its value has an incompatible type.
*/
public static byte getByteValue(Map<String, ?> env, String property) {
Object value = env.get(property);
if (value == null) {
throw Messages.fileSystemProvider().env().missingProperty(property);
}
if (value instanceof Byte) {
return (Byte) value;
}
if (value instanceof String) {
try {
return Byte.parseByte(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves an optional byte property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Byte} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @param defaultValue The value that should be used if the property is not in the given map.
* @return The value for the given property.
* @throws IllegalArgumentException If the property's value has an incompatible type.
*/
public static byte getByteValue(Map<String, ?> env, String property, byte defaultValue) {
Object value = env.get(property);
if (value == null) {
return defaultValue;
}
if (value instanceof Byte) {
return (Byte) value;
}
if (value instanceof String) {
try {
return Byte.parseByte(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves a required short property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Short} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @return The value for the given property.
* @throws IllegalArgumentException If the property is not present in the given map, or if its value has an incompatible type.
*/
public static short getShortValue(Map<String, ?> env, String property) {
Object value = env.get(property);
if (value == null) {
throw Messages.fileSystemProvider().env().missingProperty(property);
}
if (value instanceof Short) {
return (Short) value;
}
if (value instanceof String) {
try {
return Short.parseShort(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves an optional short property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Short} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @param defaultValue The value that should be used if the property is not in the given map.
* @return The value for the given property.
* @throws IllegalArgumentException If the property's value has an incompatible type.
*/
public static short getShortValue(Map<String, ?> env, String property, short defaultValue) {
Object value = env.get(property);
if (value == null) {
return defaultValue;
}
if (value instanceof Short) {
return (Short) value;
}
if (value instanceof String) {
try {
return Short.parseShort(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves a required int property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Integer} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @return The value for the given property.
* @throws IllegalArgumentException If the property is not present in the given map, or if its value has an incompatible type.
*/
public static int getIntValue(Map<String, ?> env, String property) {
Object value = env.get(property);
if (value == null) {
throw Messages.fileSystemProvider().env().missingProperty(property);
}
if (value instanceof Integer) {
return (Integer) value;
}
if (value instanceof String) {
try {
return Integer.parseInt(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves an optional int property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Integer} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @param defaultValue The value that should be used if the property is not in the given map.
* @return The value for the given property.
* @throws IllegalArgumentException If the property's value has an incompatible type.
*/
public static int getIntValue(Map<String, ?> env, String property, int defaultValue) {
Object value = env.get(property);
if (value == null) {
return defaultValue;
}
if (value instanceof Integer) {
return (Integer) value;
}
if (value instanceof String) {
try {
return Integer.parseInt(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves a required long property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Long} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @return The value for the given property.
* @throws IllegalArgumentException If the property is not present in the given map, or if its value has an incompatible type.
*/
public static long getLongValue(Map<String, ?> env, String property) {
Object value = env.get(property);
if (value == null) {
throw Messages.fileSystemProvider().env().missingProperty(property);
}
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof String) {
try {
return Long.parseLong(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves an optional long property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Long} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @param defaultValue The value that should be used if the property is not in the given map.
* @return The value for the given property.
* @throws IllegalArgumentException If the property's value has an incompatible type.
*/
public static long getLongValue(Map<String, ?> env, String property, long defaultValue) {
Object value = env.get(property);
if (value == null) {
return defaultValue;
}
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof String) {
try {
return Long.parseLong(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves a required float property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Float} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @return The value for the given property.
* @throws IllegalArgumentException If the property is not present in the given map, or if its value has an incompatible type.
*/
public static float getFloatValue(Map<String, ?> env, String property) {
Object value = env.get(property);
if (value == null) {
throw Messages.fileSystemProvider().env().missingProperty(property);
}
if (value instanceof Float) {
return (Float) value;
}
if (value instanceof String) {
try {
return Float.parseFloat(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves an optional float property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Float} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @param defaultValue The value that should be used if the property is not in the given map.
* @return The value for the given property.
* @throws IllegalArgumentException If the property's value has an incompatible type.
*/
public static float getFloatValue(Map<String, ?> env, String property, float defaultValue) {
Object value = env.get(property);
if (value == null) {
return defaultValue;
}
if (value instanceof Float) {
return (Float) value;
}
if (value instanceof String) {
try {
return Float.parseFloat(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves a required double property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Double} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @return The value for the given property.
* @throws IllegalArgumentException If the property is not present in the given map, or if its value has an incompatible type.
*/
public static double getDoubleValue(Map<String, ?> env, String property) {
Object value = env.get(property);
if (value == null) {
throw Messages.fileSystemProvider().env().missingProperty(property);
}
if (value instanceof Double) {
return (Double) value;
}
if (value instanceof String) {
try {
return Double.parseDouble(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves an optional double property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
* It supports values of type {@link Double} and {@link String}.
*
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @param defaultValue The value that should be used if the property is not in the given map.
* @return The value for the given property.
* @throws IllegalArgumentException If the property's value has an incompatible type.
*/
public static double getDoubleValue(Map<String, ?> env, String property, double defaultValue) {
Object value = env.get(property);
if (value == null) {
return defaultValue;
}
if (value instanceof Double) {
return (Double) value;
}
if (value instanceof String) {
try {
return Double.parseDouble(property);
} catch (NumberFormatException e) {
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves a required property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
*
* @param <T> The property type.
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @param cls The class the property should be an instance of.
* @return The value for the given property.
* @throws IllegalArgumentException If the property is not present in the given map, or if its value has an incompatible type.
*/
public static <T> T getValue(Map<String, ?> env, String property, Class<T> cls) {
Object value = env.get(property);
if (value == null) {
throw Messages.fileSystemProvider().env().missingProperty(property);
}
if (cls.isInstance(value)) {
return cls.cast(value);
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
/**
* Retrieves an optional property from a map. This method can be used to retrieve properties in implementations of
* {@link FileSystemProvider#newFileSystem(URI, Map)} and {@link FileSystemProvider#newFileSystem(Path, Map)}.
*
* @param <T> The property type.
* @param env The map to retrieve the property from.
* @param property The name of the property.
* @param cls The class the property should be an instance of.
* @param defaultValue The value that should be used if the property is not in the given map.
* @return The value for the given property, or the given default value if the property is not in the given map.
* @throws IllegalArgumentException If the property's value has an incompatible type.
*/
public static <T> T getValue(Map<String, ?> env, String property, Class<T> cls, T defaultValue) {
Object value = env.get(property);
if (value == null) {
return defaultValue;
}
if (cls.isInstance(value)) {
return cls.cast(value);
}
throw Messages.fileSystemProvider().env().invalidProperty(property, value);
}
}

@ -0,0 +1,38 @@
package org.xbib.io.ftp.fs;
import org.xbib.io.ftp.client.FTP;
import org.xbib.io.ftp.client.FTPClient;
import java.io.IOException;
import java.nio.file.CopyOption;
import java.nio.file.OpenOption;
/**
* The possible FTP file transfer modes.
*/
public enum FileTransferMode implements OpenOption, CopyOption {
/**
* Indicates that files are to be transfered as streams of bytes.
*/
STREAM(FTP.STREAM_TRANSFER_MODE),
/**
* Indicates that files are to be transfered as series of blocks.
*/
BLOCK(FTP.BLOCK_TRANSFER_MODE),
/**
* Indicate that files are to be transfered as FTP compressed data.
*/
COMPRESSED(FTP.COMPRESSED_TRANSFER_MODE),;
private final int mode;
FileTransferMode(int mode) {
this.mode = mode;
}
void apply(FTPClient client) throws IOException {
if (!client.setFileTransferMode(mode)) {
throw new FTPFileSystemException(client.getReplyCode(), client.getReplyString());
}
}
}

@ -0,0 +1,197 @@
package org.xbib.io.ftp.fs;
import org.xbib.io.ftp.client.FTP;
import org.xbib.io.ftp.client.FTPClient;
import java.io.IOException;
import java.nio.file.CopyOption;
import java.nio.file.OpenOption;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
/**
* Represents FTP file types.
*/
public final class FileType implements OpenOption, CopyOption {
private static final FileType ASCII_NO_FORMAT = new FileType(FTP.ASCII_FILE_TYPE, "ascii");
private static final Map<Format, FileType> ASCII_WITH_FORMATS = getFileTypesWithFormats(FTP.ASCII_FILE_TYPE, "ascii");
private static final FileType EBCDIC_NO_FORMAT = new FileType(FTP.EBCDIC_FILE_TYPE, "ebcdic");
private static final Map<Format, FileType> EBCDIC_WITH_FORMATS = getFileTypesWithFormats(FTP.EBCDIC_FILE_TYPE, "ebcdic");
private static final FileType BINARY_NO_FORMAT = new FileType(FTP.BINARY_FILE_TYPE, "binary");
private static final FileType LOCAL_NO_BYTE_SIZE = new FileType(FTP.LOCAL_FILE_TYPE, "local");
private static final int NO_FORMAT_OR_BYTE_SIZE = Integer.MIN_VALUE;
private final int fileType;
private final String fileTypeString;
private final Format format;
private final int formatOrByteSize;
private FileType(int fileType, String fileTypeString) {
this.fileType = fileType;
this.fileTypeString = fileTypeString;
this.formatOrByteSize = NO_FORMAT_OR_BYTE_SIZE;
this.format = null;
}
private FileType(int fileType, String fileTypeString, Format format) {
this.fileType = fileType;
this.fileTypeString = fileTypeString;
this.formatOrByteSize = format.format;
this.format = format;
}
private FileType(int fileType, String fileTypeString, int byteSize) {
this.fileType = fileType;
this.fileTypeString = fileTypeString;
this.formatOrByteSize = byteSize;
this.format = null;
}
private static Map<Format, FileType> getFileTypesWithFormats(int fileType, String fileTypeString) {
Map<Format, FileType> fileTypes = new EnumMap<>(Format.class);
for (Format format : Format.values()) {
fileTypes.put(format, new FileType(fileType, fileTypeString, format));
}
return Collections.unmodifiableMap(fileTypes);
}
/**
* Returns an ASCII file type with an unspecified text format.
*
* @return An ASCII file type with an unspecified text format.
*/
public static FileType ascii() {
return ASCII_NO_FORMAT;
}
/**
* Returns an ASCII file type with a specific text format.
*
* @param format The text format for the file type; ignored if {@code null}.
* @return An ASCII file type with the given text format.
*/
public static FileType ascii(Format format) {
return format == null ? ASCII_NO_FORMAT : ASCII_WITH_FORMATS.get(format);
}
/**
* Returns an EBCDIC file type with an unspecified text format.
*
* @return An EBCDIC file type with an unspecified text format.
*/
public static FileType ebcdic() {
return EBCDIC_NO_FORMAT;
}
/**
* Returns an EBCDIC file type with a specific text format.
*
* @param format The text format for the file type; ignored if {@code null}.
* @return An EBCDIC file type with the given text format.
*/
public static FileType ebcdic(Format format) {
return format == null ? EBCDIC_NO_FORMAT : EBCDIC_WITH_FORMATS.get(format);
}
/**
* Returns a binary file type with an unspecified text format.
*
* @return A binary file type with an unspecified text format.
*/
public static FileType binary() {
return BINARY_NO_FORMAT;
}
/**
* Returns a local file type with an unspecified byte size.
*
* @return A local file type with an unspecified byte size.
*/
public static FileType local() {
return LOCAL_NO_BYTE_SIZE;
}
/**
* Returns a local file type with a specific byte size.
*
* @param byteSize The byte size for the file type; ignored if not larger than 0.
* @return A binary file type with the given text format.
*/
public static FileType local(int byteSize) {
return byteSize <= 0 ? LOCAL_NO_BYTE_SIZE : new FileType(FTP.LOCAL_FILE_TYPE, "local", byteSize);
}
void apply(FTPClient client) throws IOException {
boolean result = formatOrByteSize == NO_FORMAT_OR_BYTE_SIZE ? client.setFileType(fileType) : client.setFileType(fileType, formatOrByteSize);
if (!result) {
throw new FTPFileSystemException(client.getReplyCode(), client.getReplyString());
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || o.getClass() != getClass()) {
return false;
}
FileType other = (FileType) o;
return fileType == other.fileType
&& formatOrByteSize == other.formatOrByteSize;
}
@Override
public int hashCode() {
int hash = fileType;
hash = 31 * hash + formatOrByteSize;
return hash;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder()
.append(getClass().getSimpleName())
.append('.')
.append(fileTypeString);
if (format != null) {
sb.append('(')
.append(format)
.append(')');
} else if (formatOrByteSize != NO_FORMAT_OR_BYTE_SIZE) {
sb.append('(')
.append(formatOrByteSize)
.append(')');
}
return sb.toString();
}
/**
* The possible FTP text formats.
*/
public enum Format {
/**
* Indicates a non-print text format.
*/
NON_PRINT(FTP.NON_PRINT_TEXT_FORMAT),
/**
* Indicates that text files contain format vertical format control characters.
*/
TELNET(FTP.TELNET_TEXT_FORMAT),
/**
* Indicates that text files contain ASA vertical format control characters.
*/
CARRIAGE_CONTROL(FTP.CARRIAGE_CONTROL_TEXT_FORMAT),;
private final int format;
Format(int format) {
this.format = format;
}
}
}

@ -0,0 +1,28 @@
package org.xbib.io.ftp.fs;
import java.nio.file.LinkOption;
/**
* A utility class for {@link LinkOption}s.
*/
public final class LinkOptionSupport {
private LinkOptionSupport() {
throw new Error("cannot create instances of " + getClass().getName());
}
/**
* Returns whether or not the given link options indicate that links should be followed.
*
* @param options The link options to check.
* @return {@code false} if one of the given link options is {@link LinkOption#NOFOLLOW_LINKS}, or {@code true} otherwise.
*/
public static boolean followLinks(LinkOption... options) {
for (LinkOption option : options) {
if (option == LinkOption.NOFOLLOW_LINKS) {
return false;
}
}
return true;
}
}

@ -0,0 +1,808 @@
package org.xbib.io.ftp.fs;
import java.net.URI;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemException;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.nio.file.spi.FileSystemProvider;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.regex.PatternSyntaxException;
/**
* A utility class for providing translated messages and exceptions.
*/
public final class Messages {
private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("org.xbib.ftp.fs.messages", UTF8Control.INSTANCE);
private Messages() {
throw new Error("cannot create instances of " + getClass().getName());
}
private static synchronized String getMessage(String key) {
return BUNDLE.getString(key);
}
private static String getMessage(String key, Object... args) {
String format = getMessage(key);
return String.format(format, args);
}
/**
* Creates an exception that can be thrown for an invalid index.
*
* @param index The index that was invalid.
* @return The created exception.
*/
public static IllegalArgumentException invalidIndex(int index) {
return new IllegalArgumentException(getMessage("invalidIndex", index));
}
/**
* Creates an exception that can be thrown for an invalid range.
*
* @param beginIndex The begin index of the range, inclusive.
* @param endIndex The end index of the range, exclusive.
* @return The created exception.
*/
public static IllegalArgumentException invalidRange(int beginIndex, int endIndex) {
return new IllegalArgumentException(getMessage("invalidRange", beginIndex, endIndex));
}
/**
* Creates an {@code UnsupportedOperationException} that can be thrown if an I/O operation, such as
* {@link BasicFileAttributeView#setTimes(FileTime, FileTime, FileTime)}, is not supported.
*
* @param cls The class defining the operation, e.g. {@link BasicFileAttributeView}.
* @param operation The name of the operation (method).
* @return The created exception.
*/
public static UnsupportedOperationException unsupportedOperation(Class<?> cls, String operation) {
return new UnsupportedOperationException(getMessage("unsupportedOperation", cls.getSimpleName(), operation));
}
/**
* Returns an object for providing translated messages and exceptions for paths.
*
* @return An object for providing translated messages and exceptions for paths.
*/
public static PathMessages path() {
return PathMessages.INSTANCE;
}
/**
* Returns an object for providing translated messages and exceptions for path matchers.
*
* @return An object for providing translated messages and exceptions for path matchers.
*/
public static PathMatcherMessages pathMatcher() {
return PathMatcherMessages.INSTANCE;
}
/**
* Returns an object for providing translated messages and exceptions for file stores.
*
* @return An object for providing translated messages and exceptions for file stores.
*/
public static FileStoreMessages fileStore() {
return FileStoreMessages.INSTANCE;
}
/**
* Returns an object for providing translated messages and exceptions for file system providers.
*
* @return An object for providing translated messages and exceptions for file system providers.
*/
public static FileSystemProviderMessages fileSystemProvider() {
return FileSystemProviderMessages.INSTANCE;
}
/**
* Returns an object for providing translated messages and exceptions for directory streams.
*
* @return An object for providing translated messages and exceptions for directory streams.
*/
public static DirectoryStreamMessages directoryStream() {
return DirectoryStreamMessages.INSTANCE;
}
/**
* Returns an object for providing translated messages and exceptions for (seekable) byte channels.
*
* @return An object for providing translated messages and exceptions for (seekable) byte channels.
*/
public static ByteChannelMessages byteChannel() {
return ByteChannelMessages.INSTANCE;
}
/**
* Returns an object for providing translated messages and exceptions for URI validation.
*
* @return An object for providing translated messages and exceptions for URI validation.
*/
public static URIMessages uri() {
return URIMessages.INSTANCE;
}
/**
* A utility class for providing translated messages and exceptions for paths.
*
*/
public static final class PathMessages {
private static final PathMessages INSTANCE = new PathMessages();
private PathMessages() {
super();
}
/**
* Creates an exception that can be thrown if a path contains a nul ({@code \0}) character.
*
* @param path The path that contains a nul character.
* @return The created exception.
*/
public InvalidPathException nulCharacterNotAllowed(String path) {
return new InvalidPathException(path, getMessage("path.nulCharacterNotAllowed"));
}
/**
* Creates an exception that can be thrown if {@link Path#relativize(Path)} is called and one of the paths is absolute and the other is not.
*
* @return The created exception.
*/
public IllegalArgumentException relativizeAbsoluteRelativeMismatch() {
return new IllegalArgumentException(getMessage("path.relativizeAbsoluteRelativeMismatch"));
}
}
/**
* A utility class for providing translated messages and exceptions for path matchers.
*/
public static final class PathMatcherMessages {
private static final PathMatcherMessages INSTANCE = new PathMatcherMessages();
private PathMatcherMessages() {
super();
}
/**
* Returns an object for providing translated messages and exceptions for globs.
*
* @return An object for providing translated messages and exceptions for globs.
*/
public PathMatcherGlobMessages glob() {
return PathMatcherGlobMessages.INSTANCE;
}
/**
* Creates an exception that can be thrown if {@link FileSystem#getPathMatcher(String)} is called with a string that does not contain a
* syntax part.
*
* @param syntaxAndInput The input to {@link FileSystem#getPathMatcher(String)} that's missing the syntax.
* @return The created exception.
*/
public IllegalArgumentException syntaxNotFound(String syntaxAndInput) {
return new IllegalArgumentException(getMessage("pathMatcher.syntaxNotFound", syntaxAndInput));
}
/**
* Creates an exception that can be thrown if {@link FileSystem#getPathMatcher(String)} is called with an unsupported syntax.
*
* @param syntax The unsupported syntax.
* @return The created exception.
*/
public UnsupportedOperationException unsupportedPathMatcherSyntax(String syntax) {
return new UnsupportedOperationException(getMessage("pathMatcher.unsupportedPathMatcherSyntax", syntax));
}
}
/**
* A utility class for providing translated messages and exceptions for globs.
*
*/
public static final class PathMatcherGlobMessages {
private static final PathMatcherGlobMessages INSTANCE = new PathMatcherGlobMessages();
private PathMatcherGlobMessages() {
super();
}
/**
* Creates an exception that can be thrown if a glob contains nested groups.
*
* @param glob The glob that contains nested groups.
* @param index The index at which the (first) nested group is encountered.
* @return The created exception.
*/
public PatternSyntaxException nestedGroupsNotSupported(String glob, int index) {
return new PatternSyntaxException(getMessage("pathMatcher.glob.nestedGroupsNotSupported"), glob, index);
}
/**
* Creates an exception that can be thrown if a glob contains a group end character (<code>&#125;</code>) that does not close a group.
*
* @param glob The glob that contains the unexpected group end character.
* @param index The index at which the unexpected group end character occurs.
* @return The created exception.
*/
public PatternSyntaxException unexpectedGroupEnd(String glob, int index) {
return new PatternSyntaxException(getMessage("pathMatcher.glob.unexpectedGroupEnd"), glob, index);
}
/**
* Creates an exception that can be thrown if a glob missing a group end character (<code>&#125;</code>).
*
* @param glob The glob that misses a group end character.
* @return The created exception.
*/
public PatternSyntaxException missingGroupEnd(String glob) {
return new PatternSyntaxException(getMessage("pathMatcher.glob.missingGroupEnd"), glob, glob.length());
}
/**
* Creates an exception that can be thrown if a glob contains nested classes.
*
* @param glob The glob that contains nested classes.
* @param index The index at which the (first) nested class is encountered.
* @return The created exception.
*/
public PatternSyntaxException nestedClassesNotSupported(String glob, int index) {
return new PatternSyntaxException(getMessage("pathMatcher.glob.nestedClassesNotSupported"), glob, index);
}
/**
* Creates an exception that can be thrown if a glob contains a class end character ({@code ]}) that does not close a class.
*
* @param glob The glob that contains the unexpected class end character.
* @param index The index at which the unexpected class end character occurs.
* @return The created exception.
*/
public PatternSyntaxException unexpectedClassEnd(String glob, int index) {
return new PatternSyntaxException(getMessage("pathMatcher.glob.unexpectedClassEnd"), glob, index);
}
/**
* Creates an exception that can be thrown if a glob missing a class end character ({@code ]}).
*
* @param glob The glob that misses a class end character.
* @return The created exception.
*/
public PatternSyntaxException missingClassEnd(String glob) {
return new PatternSyntaxException(getMessage("pathMatcher.glob.missingClassEnd"), glob, glob.length());
}
/**
* Creates an exception that can be thrown if a glob contains a separator (e.g. {@code /}) in a class.
*
* @param glob The glob that contains a separator in a class.
* @param index The index at which the separator occurs.
* @return The created exception.
*/
public PatternSyntaxException separatorNotAllowedInClass(String glob, int index) {
return new PatternSyntaxException(getMessage("pathMatcher.glob.separatorNotAllowedInClass"), glob, index);
}
/**
* Creates an exception that can be thrown if a glob contains an escape character ({@code \}) that does not escape an actual glob meta
* character.
*
* @param glob The glob that contains an unexpected escape character.
* @param index The index at which the unexpected escape character occurs.
* @return The created exception.
*/
public PatternSyntaxException unescapableChar(String glob, int index) {
return new PatternSyntaxException(getMessage("pathMatcher.glob.unescapableChar"), glob, index);
}
}
/**
* A utility class for providing translated messages and exceptions for file stores.
*
*/
public static final class FileStoreMessages {
private static final FileStoreMessages INSTANCE = new FileStoreMessages();
private FileStoreMessages() {
super();
}
/**
* Creates an exception that can be thrown if {@link FileStore#getAttribute(String)} is called with an unsupported attribute.
*
* @param attribute The unsupported attribute.
* @return The created exception.
*/
public UnsupportedOperationException unsupportedAttribute(String attribute) {
return new UnsupportedOperationException(getMessage("fileStore.unsupportedAttribute", attribute));
}
}
/**
* A utility class for providing translated messages and exceptions for file system providers.
*
*/
public static final class FileSystemProviderMessages {
private static final FileSystemProviderMessages INSTANCE = new FileSystemProviderMessages();
private FileSystemProviderMessages() {
super();
}
/**
* Returns an object for providing translated messages and exceptions for file system provider properties.
*
* @return An object for providing translated messages and exceptions for file system provider properties.
*/
public FileSystemProviderEnvMessages env() {
return FileSystemProviderEnvMessages.INSTANCE;
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#newByteChannel(Path, Set, FileAttribute...)} or a similar method is
* called with a directory.
*
* @param dir The directory path.
* @return The created exception.
*/
public FileSystemException isDirectory(String dir) {
return new FileSystemException(dir, null, getMessage("fileSystemProvider.isDirectory"));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#newByteChannel(Path, Set, FileAttribute...)} or a similar method is
* called with an illegal combination of open options.
*
* @param options The illegal combination of open options.
* @return The created exception.
*/
public IllegalArgumentException illegalOpenOptionCombination(OpenOption... options) {
return illegalOpenOptionCombination(Arrays.asList(options));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#newByteChannel(Path, Set, FileAttribute...)} or a similar method is
* called with an illegal combination of open options.
*
* @param options The illegal combination of options.
* @return The created exception.
*/
public IllegalArgumentException illegalOpenOptionCombination(Collection<? extends OpenOption> options) {
return new IllegalArgumentException(getMessage("fileSystemProvider.illegalOpenOptionCombination", options));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#copy(Path, Path, CopyOption...)} or
* {@link FileSystemProvider#move(Path, Path, CopyOption...)} is called with an illegal combination of copy options.
*
* @param options The illegal combination of copy options.
* @return The created exception.
*/
public IllegalArgumentException illegalCopyOptionCombination(CopyOption... options) {
return illegalCopyOptionCombination(Arrays.asList(options));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#copy(Path, Path, CopyOption...)} or
* {@link FileSystemProvider#move(Path, Path, CopyOption...)} is called with an illegal combination of copy options.
*
* @param options The illegal combination of copy options.
* @return The created exception.
*/
public IllegalArgumentException illegalCopyOptionCombination(Collection<? extends CopyOption> options) {
return new IllegalArgumentException(getMessage("fileSystemProvider.illegalCopyOptionCombination", options));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#readAttributes(Path, Class, LinkOption...)} is called with an
* unsupported file attributes type.
*
* @param type The unsupported type.
* @return The created exception.
*/
public UnsupportedOperationException unsupportedFileAttributesType(Class<? extends BasicFileAttributes> type) {
return new UnsupportedOperationException(getMessage("fileSystemProvider.unsupportedFileAttributesType", type.getName()));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#readAttributes(Path, String, LinkOption...)} or
* {@link FileSystemProvider#setAttribute(Path, String, Object, LinkOption...)} is called with an unsupported file attribute view.
*
* @param view The unsupported view.
* @return The created exception.
*/
public UnsupportedOperationException unsupportedFileAttributeView(String view) {
return new UnsupportedOperationException(getMessage("fileSystemProvider.unsupportedFileAttributeView", view));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#newByteChannel(Path, Set, FileAttribute...)},
* {@link FileSystemProvider#createDirectory(Path, FileAttribute...)} or a similar method is called with an unsupported file attribute.
*
* @param attribute The unsupported attribute.
* @return The created exception.
*/
public UnsupportedOperationException unsupportedCreateFileAttribute(String attribute) {
return new UnsupportedOperationException(getMessage("fileSystemProvider.unsupportedFileAttribute", attribute));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#readAttributes(Path, String, LinkOption...)} or
* {@link FileSystemProvider#setAttribute(Path, String, Object, LinkOption...)} is called with an unsupported file attribute.
*
* @param attribute The unsupported attribute.
* @return The created exception.
*/
public IllegalArgumentException unsupportedFileAttribute(String attribute) {
return new IllegalArgumentException(getMessage("fileSystemProvider.unsupportedFileAttribute", attribute));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#newByteChannel(Path, Set, FileAttribute...)} or a similar method is
* called with an unsupported open option.
*
* @param option The unsupported open option.
* @return The created exception.
*/
public UnsupportedOperationException unsupportedOpenOption(OpenOption option) {
return new UnsupportedOperationException(getMessage("fileSystemProvider.unsupportedOpenOption", option));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#copy(Path, Path, CopyOption...)} or
* {@link FileSystemProvider#move(Path, Path, CopyOption...)} is called with an unsupported copy option.
*
* @param option The unsupported copy option.
* @return The created exception.
*/
public UnsupportedOperationException unsupportedCopyOption(CopyOption option) {
return new UnsupportedOperationException(getMessage("fileSystemProvider.unsupportedCopyOption", option));
}
}
/**
* A utility class for providing translated messages and exceptions for file system provider properties.
*
*/
public static final class FileSystemProviderEnvMessages {
private static final FileSystemProviderEnvMessages INSTANCE = new FileSystemProviderEnvMessages();
private FileSystemProviderEnvMessages() {
super();
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#newFileSystem(URI, Map)} or
* {@link FileSystemProvider#newFileSystem(Path, Map)} is called with a required property missing.
*
* @param property The name of the missing property.
* @return The created exception.
*/
public IllegalArgumentException missingProperty(String property) {
return new IllegalArgumentException(getMessage("fileSystemProvider.env.missingProperty", property));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#newFileSystem(URI, Map)} or
* {@link FileSystemProvider#newFileSystem(Path, Map)} is called with an invalid value for a property.
*
* @param property The name of the property.
* @param value The invalid value.
* @return The created exception.
*/
public IllegalArgumentException invalidProperty(String property, Object value) {
return new IllegalArgumentException(getMessage("fileSystemProvider.env.invalidProperty", property, value));
}
/**
* Creates an exception that can be thrown if {@link FileSystemProvider#newFileSystem(URI, Map)} or
* {@link FileSystemProvider#newFileSystem(Path, Map)} is called with an invalid combination of properties.
*
* @param properties The names of the properties.
* @return The created exception.
*/
public IllegalArgumentException invalidPropertyCombination(Collection<String> properties) {
return new IllegalArgumentException(getMessage("fileSystemProvider.env.invalidPropertyCombination", properties));
}
}
/**
* A utility class for providing translated messages and exceptions for directory streams.
*/
public static final class DirectoryStreamMessages {
private static final DirectoryStreamMessages INSTANCE = new DirectoryStreamMessages();
private DirectoryStreamMessages() {
super();
}
/**
* Creates an exception that can be closed if {@link DirectoryStream#iterator()} is called on a closed directory stream.
*
* @return The created exception.
*/
public IllegalStateException closed() {
return new IllegalStateException(getMessage("directoryStream.closed"));
}
/**
* Creates an exception that can be closed if {@link DirectoryStream#iterator()} is called after the iterator was already returned.
*
* @return The created exception.
*/
public IllegalStateException iteratorAlreadyReturned() {
return new IllegalStateException(getMessage("directoryStream.iteratorAlreadyReturned"));
}
}
/**
* A utility class for providing translated messages and exceptions for (seekable) byte channels.
*/
public static final class ByteChannelMessages {
private static final ByteChannelMessages INSTANCE = new ByteChannelMessages();
private ByteChannelMessages() {
super();
}
/**
* Creates an exception that can be thrown if {@link SeekableByteChannel#position(long)} is called with a negative position.
*
* @param position The negative position.
* @return The created exception.
*/
public IllegalArgumentException negativePosition(long position) {
return new IllegalArgumentException(getMessage("byteChannel.negativePosition", position));
}
/**
* Creates an exception that can be thrown if {@link SeekableByteChannel#truncate(long)} is called with a negative size.
*
* @param size The negative size.
* @return The created exception.
*/
public IllegalArgumentException negativeSize(long size) {
return new IllegalArgumentException(getMessage("byteChannel.negativeSize", size));
}
}
/**
* A utility class for providing translated messages and exceptions for URI validation.
*/
public static final class URIMessages {
private static final URIMessages INSTANCE = new URIMessages();
private URIMessages() {
super();
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) has an invalid scheme.
*
* @param uri The URI with the invalid scheme.
* @param expectedScheme The expected scheme.
* @return The created exception.
* @see URI#getScheme()
*/
public IllegalArgumentException invalidScheme(URI uri, String expectedScheme) {
return new IllegalArgumentException(getMessage("uri.invalidScheme", expectedScheme, uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) is not absolute.
*
* @param uri The non-absolute URI.
* @return The created exception.
* @see URI#isAbsolute()
*/
public IllegalArgumentException notAbsolute(URI uri) {
return new IllegalArgumentException(getMessage("uri.notAbsolute", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) is not hierarchical.
* This is the same as being opaque.
*
* @param uri The non-hierarchical URI.
* @return The created exception.
* @see URI#isOpaque()
*/
public IllegalArgumentException notHierarchical(URI uri) {
return new IllegalArgumentException(getMessage("uri.notHierarchical", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) has an authority component.
*
* @param uri The URI with the authority component.
* @return The created exception.
* @see URI#getAuthority()
* @see #hasNoAuthority(URI)
*/
public IllegalArgumentException hasAuthority(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasAuthority", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) has a fragment component.
*
* @param uri The URI with the fragment component.
* @return The created exception.
* @see URI#getFragment()
* @see #hasNoFragment(URI)
*/
public IllegalArgumentException hasFragment(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasFragment", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) has a host component.
*
* @param uri The URI with the host component.
* @return The created exception.
* @see URI#getHost()
* @see #hasNoHost(URI)
*/
public IllegalArgumentException hasHost(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasHost", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) has a path component.
*
* @param uri The URI with the path component.
* @return The created exception.
* @see URI#getPath()
* @see #hasNoHost(URI)
*/
public IllegalArgumentException hasPath(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasPath", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) has a port number.
*
* @param uri The URI with the port number.
* @return The created exception.
* @see URI#getPort()
* @see #hasNoPort(URI)
*/
public IllegalArgumentException hasPort(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasPort", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) has a query component.
*
* @param uri The URI with the query component.
* @return The created exception.
* @see URI#getQuery()
* @see #hasNoQuery(URI)
*/
public IllegalArgumentException hasQuery(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasQuery", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) has a user-info component.
*
* @param uri The URI with the user-info component.
* @return The created exception.
* @see URI#getUserInfo()
* @see #hasNoUserInfo(URI)
*/
public IllegalArgumentException hasUserInfo(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasUserInfo", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) does not have an authority
* component.
*
* @param uri The URI without the authority component.
* @return The created exception.
* @see URI#getAuthority()
* @see #hasAuthority(URI)
*/
public IllegalArgumentException hasNoAuthority(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasNoAuthority", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) does not have a fragment
* component.
*
* @param uri The URI without the fragment component.
* @return The created exception.
* @see URI#getFragment()
* @see #hasFragment(URI)
*/
public IllegalArgumentException hasNoFragment(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasNoFragment", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) does not have a host component.
*
* @param uri The URI without the host component.
* @return The created exception.
* @see URI#getHost()
* @see #hasHost(URI)
*/
public IllegalArgumentException hasNoHost(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasNoHost", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) does not have a path component.
*
* @param uri The URI without the path component.
* @return The created exception.
* @see URI#getPath()
* @see #hasPath(URI)
*/
public IllegalArgumentException hasNoPath(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasNoPath", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) does not have a port number.
*
* @param uri The URI without the port number.
* @return The created exception.
* @see URI#getPort()
* @see #hasPort(URI)
*/
public IllegalArgumentException hasNoPort(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasNoPort", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) does not have a query component.
*
* @param uri The URI without the query component.
* @return The created exception.
* @see URI#getQuery()
* @see #hasQuery(URI)
*/
public IllegalArgumentException hasNoQuery(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasNoQuery", uri));
}
/**
* Creates an exception that can be thrown if a URI (e.g. used for {@link FileSystemProvider#getPath(URI)}) does not have a user-info
* component.
*
* @param uri The URI without the user-info component.
* @return The created exception.
* @see URI#getUserInfo()
* @see #hasUserInfo(URI)
*/
public IllegalArgumentException hasNoUserInfo(URI uri) {
return new IllegalArgumentException(getMessage("uri.hasNoUserInfo", uri));
}
}
}

@ -0,0 +1,196 @@
package org.xbib.io.ftp.fs;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
/**
* A representation of possible open options.
*/
final class OpenOptions extends TransferOptions {
public final boolean read;
public final boolean write;
public final boolean append;
public final boolean create;
public final boolean createNew;
public final boolean deleteOnClose;
public final Collection<? extends OpenOption> options;
private OpenOptions(boolean read, boolean write, boolean append, boolean create, boolean createNew, boolean deleteOnClose,
FileType fileType, FileStructure fileStructure, FileTransferMode fileTransferMode,
Collection<? extends OpenOption> options) {
super(fileType, fileStructure, fileTransferMode);
this.read = read;
this.write = write;
this.append = append;
this.create = create;
this.createNew = createNew;
this.deleteOnClose = deleteOnClose;
this.options = options;
}
static OpenOptions forNewInputStream(OpenOption... options) {
return forNewInputStream(Arrays.asList(options));
}
static OpenOptions forNewInputStream(Collection<? extends OpenOption> options) {
if (options.isEmpty()) {
return new OpenOptions(true, false, false, false, false, false, null, null, null, Collections.<OpenOption>emptySet());
}
boolean deleteOnClose = false;
FileType fileType = null;
FileStructure fileStructure = null;
FileTransferMode fileTransferMode = null;
for (OpenOption option : options) {
if (option == StandardOpenOption.DELETE_ON_CLOSE) {
deleteOnClose = true;
} else if (option instanceof FileType) {
fileType = setOnce((FileType) option, fileType, options);
} else if (option instanceof FileStructure) {
fileStructure = setOnce((FileStructure) option, fileStructure, options);
} else if (option instanceof FileTransferMode) {
fileTransferMode = setOnce((FileTransferMode) option, fileTransferMode, options);
} else if (option != StandardOpenOption.READ && option != StandardOpenOption.TRUNCATE_EXISTING && !isIgnoredOpenOption(option)) {
// TRUNCATE_EXISTING is ignored in combination with READ
throw Messages.fileSystemProvider().unsupportedOpenOption(option);
}
}
return new OpenOptions(true, false, false, false, false, deleteOnClose, fileType, fileStructure, fileTransferMode, options);
}
static OpenOptions forNewOutputStream(OpenOption... options) {
return forNewOutputStream(Arrays.asList(options));
}
static OpenOptions forNewOutputStream(Collection<? extends OpenOption> options) {
if (options.isEmpty()) {
// CREATE, TRUNCATE_EXISTING and WRITE, i.e. create, not createNew, and not append
return new OpenOptions(false, true, false, true, false, false, null, null, null, Collections.<OpenOption>emptySet());
}
boolean append = false;
boolean truncateExisting = false;
boolean create = false;
boolean createNew = false;
boolean deleteOnClose = false;
FileType fileType = null;
FileStructure fileStructure = null;
FileTransferMode fileTransferMode = null;
for (OpenOption option : options) {
if (option == StandardOpenOption.APPEND) {
append = true;
} else if (option == StandardOpenOption.TRUNCATE_EXISTING) {
truncateExisting = true;
} else if (option == StandardOpenOption.CREATE) {
create = true;
} else if (option == StandardOpenOption.CREATE_NEW) {
createNew = true;
} else if (option == StandardOpenOption.DELETE_ON_CLOSE) {
deleteOnClose = true;
} else if (option instanceof FileType) {
fileType = setOnce((FileType) option, fileType, options);
} else if (option instanceof FileStructure) {
fileStructure = setOnce((FileStructure) option, fileStructure, options);
} else if (option instanceof FileTransferMode) {
fileTransferMode = setOnce((FileTransferMode) option, fileTransferMode, options);
} else if (option != StandardOpenOption.WRITE && !isIgnoredOpenOption(option)) {
throw Messages.fileSystemProvider().unsupportedOpenOption(option);
}
}
// append and truncateExisting contradict each other
if (append && truncateExisting) {
throw Messages.fileSystemProvider().illegalOpenOptionCombination(options);
}
return new OpenOptions(false, true, append, create, createNew, deleteOnClose, fileType, fileStructure, fileTransferMode, options);
}
static OpenOptions forNewByteChannel(Set<? extends OpenOption> options) {
boolean read = false;
boolean write = false;
boolean append = false;
boolean truncateExisting = false;
boolean create = false;
boolean createNew = false;
boolean deleteOnClose = false;
FileType fileType = null;
FileStructure fileStructure = null;
FileTransferMode fileTransferMode = null;
for (OpenOption option : options) {
if (option == StandardOpenOption.READ) {
read = true;
} else if (option == StandardOpenOption.WRITE) {
write = true;
} else if (option == StandardOpenOption.APPEND) {
append = true;
} else if (option == StandardOpenOption.TRUNCATE_EXISTING) {
truncateExisting = true;
} else if (option == StandardOpenOption.CREATE) {
create = true;
} else if (option == StandardOpenOption.CREATE_NEW) {
createNew = true;
} else if (option == StandardOpenOption.DELETE_ON_CLOSE) {
deleteOnClose = true;
} else if (option instanceof FileType) {
fileType = setOnce((FileType) option, fileType, options);
} else if (option instanceof FileStructure) {
fileStructure = setOnce((FileStructure) option, fileStructure, options);
} else if (option instanceof FileTransferMode) {
fileTransferMode = setOnce((FileTransferMode) option, fileTransferMode, options);
} else if (!isIgnoredOpenOption(option)) {
throw Messages.fileSystemProvider().unsupportedOpenOption(option);
}
}
// as per Files.newByteChannel, if none of these options is given, default to read
if (!read && !write && !append) {
read = true;
}
// read contradicts with write, append, create and createNew; TRUNCATE_EXISTING is ignored in combination with READ
if (read && (write || append || create || createNew)) {
throw Messages.fileSystemProvider().illegalOpenOptionCombination(options);
}
// append and truncateExisting contract each other
if (append && truncateExisting) {
throw Messages.fileSystemProvider().illegalOpenOptionCombination(options);
}
// read and write contract each other; read and append contract each other; append and truncateExisting contract each other
if ((read && write) || (read && append) || (append && truncateExisting)) {
throw Messages.fileSystemProvider().illegalOpenOptionCombination(options);
}
return new OpenOptions(read, write, append, create, createNew, deleteOnClose, fileType, fileStructure, fileTransferMode, options);
}
static <T> T setOnce(T newValue, T existing, Collection<? extends OpenOption> options) {
if (existing != null && !existing.equals(newValue)) {
throw Messages.fileSystemProvider().illegalOpenOptionCombination(options);
}
return newValue;
}
private static boolean isIgnoredOpenOption(OpenOption option) {
return option == StandardOpenOption.SPARSE
|| option == StandardOpenOption.SYNC
|| option == StandardOpenOption.DSYNC
|| option == LinkOption.NOFOLLOW_LINKS;
}
}

@ -0,0 +1,215 @@
package org.xbib.io.ftp.fs;
import java.nio.file.FileSystem;
import java.nio.file.PathMatcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* A utility class that can assist in implementing {@link PathMatcher}s.
*/
public final class PathMatcherSupport {
private PathMatcherSupport() {
throw new Error("cannot create instances of " + getClass().getName());
}
/**
* Creates a {@code Pattern} for the given syntax and pattern combination. This follows the rules of {@link FileSystem#getPathMatcher(String)}.
* <p>
* This method supports two syntaxes: {@code glob} and {@code regex}. If the syntax is {@code glob}, this method delegates to
* {@link #toGlobPattern(String)}. Otherwise it will call {@link Pattern#compile(String)}.
*
* @param syntaxAndPattern The syntax and pattern.
* @return A {@code Pattern} based on the given syntax and pattern.
* @throws IllegalArgumentException If the parameter does not take the form {@code syntax:pattern}.
* @throws PatternSyntaxException If the pattern is invalid.
* @throws UnsupportedOperationException If the pattern syntax is not {@code glob} or {@code regex}.
*/
public static Pattern toPattern(String syntaxAndPattern) {
final int index = syntaxAndPattern.indexOf(':');
if (index == -1) {
throw Messages.pathMatcher().syntaxNotFound(syntaxAndPattern);
}
String syntax = syntaxAndPattern.substring(0, index);
String expression = syntaxAndPattern.substring(index + 1);
if ("glob".equals(syntax)) {
return toGlobPattern(expression);
}
if ("regex".equals(syntax)) {
return Pattern.compile(expression);
}
throw Messages.pathMatcher().unsupportedPathMatcherSyntax(syntax);
}
/**
* Converts the given glob into a {@code Pattern}.
* <p>
* Note that this method uses a single forward slash ({@code /}) as path separator.
*
* @param glob The glob to convert.
* @return A {@code Pattern} built from the given glob.
* @throws PatternSyntaxException If the given glob is invalid.
* @see FileSystem#getPathMatcher(String)
*/
public static Pattern toGlobPattern(String glob) {
return toGlobPattern(glob, 0);
}
/**
* Converts the given glob into a {@code Pattern}.
* <p>
* Note that this method uses a single forward slash ({@code /}) as path separator.
*
* @param glob The glob to convert.
* @param flags {@link Pattern#compile(String, int) Match flags} for the {@code Pattern}.
* @return A {@code Pattern} built from the given glob.
* @throws PatternSyntaxException If the given glob is invalid.
* @throws IllegalArgumentException If the match flags are invalid.
* @see FileSystem#getPathMatcher(String)
*/
public static Pattern toGlobPattern(String glob, int flags) {
StringBuilder regex = new StringBuilder();
regex.append('^');
buildPattern(glob, 0, regex, false);
regex.append('$');
return Pattern.compile(regex.toString(), flags);
}
private static int buildPattern(String glob, int i, StringBuilder regex, boolean inGroup) {
while (i < glob.length()) {
char c = glob.charAt(i++);
switch (c) {
case '\\':
ensureGlobMetaChar(glob, i);
appendLiteral(glob.charAt(i++), regex);
break;
case '*':
if (isCharAt(glob, i, '*')) {
// anything including separators
regex.append(".*");
i++;
} else {
// anything but a separator
regex.append("[^/]*");
}
break;
case '?':
// anything but a separator
regex.append("[^/]");
break;
case '[':
// a class
i = appendClass(glob, i, regex);
break;
case ']':
throw Messages.pathMatcher().glob().unexpectedClassEnd(glob, i - 1);
case '{':
if (inGroup) {
throw Messages.pathMatcher().glob().nestedGroupsNotSupported(glob, i - 1);
}
i = appendGroup(glob, i, regex);
break;
case '}':
if (!inGroup) {
throw Messages.pathMatcher().glob().unexpectedGroupEnd(glob, i - 1);
}
// Return out of this method to appendGroup
return i;
case ',':
if (inGroup) {
regex.append(")|(?:");
} else {
appendLiteral(c, regex);
}
break;
default:
appendLiteral(c, regex);
break;
}
}
if (inGroup) {
throw Messages.pathMatcher().glob().missingGroupEnd(glob);
}
return i;
}
private static void appendLiteral(char c, StringBuilder regex) {
if (isRegexMetaChar(c)) {
regex.append('\\');
}
regex.append(c);
}
private static int appendClass(String glob, int i, StringBuilder regex) {
regex.append("[[");
if (isCharAt(glob, i, '^')) {
// If ^ is the first char in the class, escape it
regex.append("\\^");
i++;
} else if (isCharAt(glob, i, '!')) {
regex.append('^');
i++;
}
boolean inClass = true;
while (i < glob.length() && inClass) {
char c = glob.charAt(i++);
switch (c) {
case '\\':
ensureGlobMetaChar(glob, i);
appendLiteral(glob.charAt(i++), regex);
break;
case '/':
throw Messages.pathMatcher().glob().separatorNotAllowedInClass(glob, i - 1);
case '[':
throw Messages.pathMatcher().glob().nestedClassesNotSupported(glob, i - 1);
case ']':
inClass = false;
break;
default:
appendLiteral(c, regex);
break;
}
}
if (inClass) {
throw Messages.pathMatcher().glob().missingClassEnd(glob);
}
regex.append("]&&[^/]]");
return i;
}
private static int appendGroup(String glob, int i, StringBuilder regex) {
// Open two groups: an inner for the content, and an outer in case there are multiple sub patterns
regex.append("(?:(?:");
i = buildPattern(glob, i, regex, true);
regex.append("))");
return i;
}
private static void ensureGlobMetaChar(String glob, int i) {
if (!isGlobMetaChar(glob, i)) {
throw Messages.pathMatcher().glob().unescapableChar(glob, i - 1);
}
}
private static boolean isCharAt(String s, int index, char c) {
return index < s.length() && s.charAt(index) == c;
}
private static boolean isRegexMetaChar(char c) {
final String chars = ".^\\$+{[]|()";
return chars.indexOf(c) != -1;
}
private static boolean isGlobMetaChar(String s, int index) {
return index < s.length() && isGlobMetaChar(s.charAt(index));
}
private static boolean isGlobMetaChar(char c) {
final String chars = "*?\\[]{}";
return chars.indexOf(c) != -1;
}
}

@ -0,0 +1,125 @@
package org.xbib.io.ftp.fs;
import java.nio.file.attribute.PosixFilePermission;
import java.util.EnumSet;
import java.util.Set;
/**
* A utility class for {@link PosixFilePermission}.
*/
public final class PosixFilePermissionSupport {
private static final int S_IRUSR = 0400;
private static final int S_IWUSR = 0200;
private static final int S_IXUSR = 0100;
private static final int S_IRGRP = 040;
private static final int S_IWGRP = 020;
private static final int S_IXGRP = 010;
private static final int S_IROTH = 04;
private static final int S_IWOTH = 02;
private static final int S_IXOTH = 01;
private PosixFilePermissionSupport() {
throw new Error("cannot create instances of " + getClass().getName());
}
/**
* Returns the set of permissions corresponding to a permission bit mask. This method uses the most usual mapping:
* <ul>
* <li>0400 maps to {@link PosixFilePermission#OWNER_READ}</li>
* <li>0200 maps to {@link PosixFilePermission#OWNER_WRITE}</li>
* <li>0100 maps to {@link PosixFilePermission#OWNER_EXECUTE}</li>
* <li>0040 maps to {@link PosixFilePermission#GROUP_READ}</li>
* <li>0020 maps to {@link PosixFilePermission#GROUP_WRITE}</li>
* <li>0010 maps to {@link PosixFilePermission#GROUP_EXECUTE}</li>
* <li>0004 maps to {@link PosixFilePermission#OTHERS_READ}</li>
* <li>0002 maps to {@link PosixFilePermission#OTHERS_WRITE}</li>
* <li>0001 maps to {@link PosixFilePermission#OTHERS_EXECUTE}</li>
* </ul>
*
* @param mask The bit mask representing a set of permissions.
* @return The resulting set of permissions.
*/
public static Set<PosixFilePermission> fromMask(int mask) {
Set<PosixFilePermission> permissions = EnumSet.noneOf(PosixFilePermission.class);
addIfSet(permissions, mask, S_IRUSR, PosixFilePermission.OWNER_READ);
addIfSet(permissions, mask, S_IWUSR, PosixFilePermission.OWNER_WRITE);
addIfSet(permissions, mask, S_IXUSR, PosixFilePermission.OWNER_EXECUTE);
addIfSet(permissions, mask, S_IRGRP, PosixFilePermission.GROUP_READ);
addIfSet(permissions, mask, S_IWGRP, PosixFilePermission.GROUP_WRITE);
addIfSet(permissions, mask, S_IXGRP, PosixFilePermission.GROUP_EXECUTE);
addIfSet(permissions, mask, S_IROTH, PosixFilePermission.OTHERS_READ);
addIfSet(permissions, mask, S_IWOTH, PosixFilePermission.OTHERS_WRITE);
addIfSet(permissions, mask, S_IXOTH, PosixFilePermission.OTHERS_EXECUTE);
return permissions;
}
private static void addIfSet(Set<PosixFilePermission> permissions, int mask, int bits, PosixFilePermission permission) {
if (isSet(mask, bits)) {
permissions.add(permission);
}
}
/**
* Returns a permission bit mask corresponding to a set of permissions. This method is the inverse of {@link #fromMask(int)}.
*
* @param permissions The set of permissions.
* @return The resulting permission bit mask.
*/
public static int toMask(Set<PosixFilePermission> permissions) {
int mask = 0;
mask |= getBitsIfSet(permissions, S_IRUSR, PosixFilePermission.OWNER_READ);
mask |= getBitsIfSet(permissions, S_IWUSR, PosixFilePermission.OWNER_WRITE);
mask |= getBitsIfSet(permissions, S_IXUSR, PosixFilePermission.OWNER_EXECUTE);
mask |= getBitsIfSet(permissions, S_IRGRP, PosixFilePermission.GROUP_READ);
mask |= getBitsIfSet(permissions, S_IWGRP, PosixFilePermission.GROUP_WRITE);
mask |= getBitsIfSet(permissions, S_IXGRP, PosixFilePermission.GROUP_EXECUTE);
mask |= getBitsIfSet(permissions, S_IROTH, PosixFilePermission.OTHERS_READ);
mask |= getBitsIfSet(permissions, S_IWOTH, PosixFilePermission.OTHERS_WRITE);
mask |= getBitsIfSet(permissions, S_IXOTH, PosixFilePermission.OTHERS_EXECUTE);
return mask;
}
private static int getBitsIfSet(Set<PosixFilePermission> permissions, int bits, PosixFilePermission permission) {
return permissions.contains(permission) ? bits : 0;
}
/**
* Returns whether or not a specific permission is set in a permission bit mask.
* <p>
* More formally, this method returns {@code true} only if the given permission is contained in the set returned by {@link #fromMask(int)}.
*
* @param mask The permission bit mask to check.
* @param permission The permission to check for.
* @return {@code true} if the permission is set in the given permission bit mask, or {@code false} otherwise.
*/
public static boolean hasPermission(int mask, PosixFilePermission permission) {
switch (permission) {
case OWNER_READ:
return isSet(mask, S_IRUSR);
case OWNER_WRITE:
return isSet(mask, S_IWUSR);
case OWNER_EXECUTE:
return isSet(mask, S_IXUSR);
case GROUP_READ:
return isSet(mask, S_IRGRP);
case GROUP_WRITE:
return isSet(mask, S_IWGRP);
case GROUP_EXECUTE:
return isSet(mask, S_IXGRP);
case OTHERS_READ:
return isSet(mask, S_IROTH);
case OTHERS_WRITE:
return isSet(mask, S_IWOTH);
case OTHERS_EXECUTE:
return isSet(mask, S_IXOTH);
default:
// should not occur
return false;
}
}
private static boolean isSet(int mask, int bits) {
return (mask & bits) != 0;
}
}

@ -0,0 +1,21 @@
package org.xbib.io.ftp.fs;
/**
* The possible FTPS security modes.
*/
public enum SecurityMode {
/**
* Indicates <a href="https://en.wikipedia.org/wiki/FTPS#Implicit">implicit</a> security should be used.
*/
IMPLICIT(true),
/**
* Indicates <a href="https://en.wikipedia.org/wiki/FTPS#Explicit">explicit</a> security should be used.
*/
EXPLICIT(false),;
final boolean isImplicit;
SecurityMode(boolean isImplicit) {
this.isImplicit = isImplicit;
}
}

@ -0,0 +1,517 @@
package org.xbib.io.ftp.fs;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.ProviderMismatchException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.Objects;
/**
* This class provides a base implementation of the {@link Path} interface that uses a string to store the actual path.
* This class can be used to minimize the effort required to implement the {@code Path} interface.
* Note that this class assumes that the file system uses a single forward slash ({@code /}) as its {@link FileSystem#getSeparator() separator}.
*/
public abstract class SimpleAbstractPath extends AbstractPath {
private static final String ROOT_PATH = "/";
private static final String EMPTY_PATH = "";
private static final String CURRENT_DIR = ".";
private static final String PARENT_DIR = "..";
/**
* The full path.
*/
private final String path;
/**
* The offsets in the full path of all the separate name elements.
*/
private int[] offsets;
/**
* Creates a new path.
*
* @param path The actual path.
*/
protected SimpleAbstractPath(String path) {
this(path, false);
}
/**
* Creates a new path.
*
* @param path The actual path.
* @param normalized If not {@code true}, the path will be normalized (e.g. by removing redundant forward slashes).
*/
protected SimpleAbstractPath(String path, boolean normalized) {
Objects.requireNonNull(path);
this.path = normalized ? path : normalize(path);
}
/**
* Normalizes the given path by removing redundant forward slashes and checking for invalid characters.
*/
private String normalize(String path) {
if (path.isEmpty()) {
return path;
}
StringBuilder sb = new StringBuilder(path.length());
char prev = '\0';
for (int i = 0; i < path.length(); i++) {
char c = path.charAt(i);
if (c == '/' && prev == '/') {
continue;
}
if (c == '\0') {
throw Messages.path().nulCharacterNotAllowed(path);
}
sb.append(c);
prev = c;
}
if (sb.length() > 1 && sb.charAt(sb.length() - 1) == '/') {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
/**
* Creates a new path. Implementations should create instances of the implementing class.
*
* @param path The actual path for the new path. This will already be normalized when called by the implementations of this class.
* @return The created path.
*/
protected abstract SimpleAbstractPath createPath(String path);
/**
* Returns the actual path.
*
* @return The actual path.
*/
public final String path() {
return path;
}
/**
* Returns the name at the given index. This method is similar to {@link #getName(int)} but returns the name as a string, not a {@link Path}.
*
* @param index The index of the name.
* @return The name at the given index.
* @throws IllegalArgumentException If the index is invalid.
*/
public final String nameAt(int index) {
initOffsets();
if (index < 0 || index >= offsets.length) {
throw Messages.invalidIndex(index);
}
final int begin = begin(index);
final int end = end(index);
return path.substring(begin, end);
}
/**
* Returns the file name. This method is similar to {@link #getFileName()} but returns the file name as a string, not a {@link Path}.
*
* @return The file name, or {@code null} if there is no file name.
*/
public final String fileName() {
initOffsets();
return offsets.length == 0 ? null : nameAt(offsets.length - 1);
}
/**
* Tells whether or not this path is absolute.
* <p>
* This implementation returns {@code true} if the path starts with a forward slash, or {@code false} otherwise.
*/
@Override
public boolean isAbsolute() {
return path.startsWith(ROOT_PATH);
}
/**
* Returns the root path. This method is similar to {@link #getRoot()} but returns the root as a string, not a {@link Path}.
*
* @return The root path, or {@code null} if this path is relative.
*/
public final String rootPath() {
return isAbsolute() ? ROOT_PATH : null;
}
/**
* Returns the root component of this path as a {@code Path} object, or {@code null} if this path does not have a root component.
* <p>
* This implementation returns a path {@link #createPath(String) created} with a single forward slash as its path if this path is absolute,
* or {@code null} otherwise.
*/
@Override
public Path getRoot() {
return isAbsolute() ? createPath(ROOT_PATH) : null;
}
/**
* Returns the <em>parent path</em>. This method is similar to {@link #getParent()} but returns the parent as a string, not a {@link Path}.
*
* @return The parent, or {@code null} if this path has no parent.
*/
public final String parentPath() {
initOffsets();
final int count = offsets.length;
if (count == 0) {
return null;
}
final int end = offsets[count - 1] - 1;
if (end <= 0) {
// The parent is the root (possibly null)
return rootPath();
}
return path.substring(0, end);
}
/**
* Returns the <em>parent path</em>, or {@code null} if this path does not have a parent.
* <p>
* This implementation returns:
* <ul>
* <li>{@code null} if this path has no name elements.</li>
* <li>{@link #getRoot()} if this path has only one name element.</li>
* <li>A path {@link #createPath(String) created} with this path's path up until the last forward slash otherwise.</li>
* </ul>
*/
@Override
public Path getParent() {
initOffsets();
String parentPath = parentPath();
return parentPath != null ? createPath(parentPath) : null;
}
/**
* Returns the number of name elements in the path.
* <p>
* This implementation returns a value calculated from the number of forward slashes in the actual path.
*/
@Override
public int getNameCount() {
initOffsets();
return offsets.length;
}
/**
* Returns a relative {@code Path} that is a subsequence of the name elements of this path.
* <p>
* This implementation returns a non-absolute path {@link #createPath(String) created} with a path that is the appropriate substring of this
* path's actual path.
*/
@Override
public Path subpath(int beginIndex, int endIndex) {
initOffsets();
if (beginIndex < 0 || beginIndex >= offsets.length
|| endIndex <= beginIndex || endIndex > offsets.length) {
throw Messages.invalidRange(beginIndex, endIndex);
}
final int begin = begin(beginIndex);
final int end = end(endIndex - 1);
final String subpath = path.substring(begin, end);
return createPath(subpath);
}
/**
* Tests if this path starts with the given path.
* <p>
* This implementation will first check if the two paths have the same {@link #getFileSystem() FileSystem} and class.
* If not, {@code false} is returned.
* It will then check if the actual path of this path starts with the actual path of the given path.
*/
@Override
public boolean startsWith(Path other) {
if (getFileSystem() != other.getFileSystem() || getClass() != other.getClass()) {
return false;
}
final SimpleAbstractPath that = (SimpleAbstractPath) other;
if (that.path.isEmpty()) {
return path.isEmpty();
}
if (ROOT_PATH.equals(that.path)) {
return isAbsolute();
}
if (!path.startsWith(that.path)) {
return false;
}
return path.length() == that.path.length() || path.charAt(that.path.length()) == '/';
}
/**
* Tests if this path starts with the given path.
* <p>
* This implementation will first check if the two paths have the same {@link #getFileSystem() FileSystem} and class.
* If not, {@code false} is returned.
* It will then check if the actual path of this path ends with the actual path of the given path.
*/
@Override
public boolean endsWith(Path other) {
if (getFileSystem() != other.getFileSystem() || getClass() != other.getClass()) {
return false;
}
final SimpleAbstractPath that = (SimpleAbstractPath) other;
if (that.path.isEmpty()) {
return path.isEmpty();
}
if (that.isAbsolute()) {
return path.equals(that.path);
}
if (!path.endsWith(that.path)) {
return false;
}
return path.length() == that.path.length() || path.charAt(path.length() - that.path.length() - 1) == '/';
}
/**
* Returns a path that is this path with redundant name elements eliminated.
* <p>
* This implementation will go over the name elements, removing all occurrences of single dots ({@code .}).
* For any occurrence of a double dot ({@code ..}), any previous element (if any) is removed as well.
* With the remaining name elements, a new path is {@link #createPath(String) created}.
*/
@Override
public Path normalize() {
int count = getNameCount();
if (count == 0) {
return this;
}
Deque<String> nameElements = new ArrayDeque<>(count);
int nonParentCount = 0;
for (int i = 0; i < count; i++) {
if (equalsNameAt(CURRENT_DIR, i)) {
continue;
}
boolean isParent = equalsNameAt(PARENT_DIR, i);
// If this is a parent and there is at least one non-parent, pop it.
if (isParent && nonParentCount > 0) {
nameElements.pollLast();
nonParentCount--;
continue;
}
if (!isAbsolute() || !isParent) {
// For non-absolute paths, this may add a parent if there are only parents, but that's OK.
// Example: foo/../../bar will lead to ../bar
// For absolute paths, any leading .. will not be included though.
String nameElement = nameAt(i);
nameElements.addLast(nameElement);
}
if (!isParent) {
nonParentCount++;
}
}
StringBuilder sb = new StringBuilder(path.length());
if (isAbsolute()) {
sb.append('/');
}
for (Iterator<String> i = nameElements.iterator(); i.hasNext(); ) {
sb.append(i.next());
if (i.hasNext()) {
sb.append('/');
}
}
return createPath(sb.toString());
}
private boolean equalsNameAt(String name, int index) {
final int thisBegin = begin(index);
final int thisEnd = end(index);
final int thisLength = thisEnd - thisBegin;
if (thisLength != name.length()) {
return false;
}
return path.regionMatches(thisBegin, name, 0, thisLength);
}
/**
* Resolve the given path against this path.
* <p>
* This implementation returns the given path if it's {@link Path#isAbsolute() absolute} or if this path has no name elements,
* this path if the given path has no name elements,
* or a path {@link #createPath(String) created} with the paths of this path and the given path joined with a forward slash otherwise.
*/
@Override
public Path resolve(Path other) {
final SimpleAbstractPath that = checkPath(other);
if (path.isEmpty() || that.isAbsolute()) {
return that;
}
if (that.path.isEmpty()) {
return this;
}
final String resolvedPath;
if (path.endsWith("/")) {
resolvedPath = path + that.path;
} else {
resolvedPath = path + "/" + that.path; //
}
return createPath(resolvedPath);
}
/**
* Constructs a relative path between this path and a given path.
* <p>
* This implementation skips past any shared name elements, then adds as many occurrences of double dots ({@code ..}) as needed, then adds
* the remainder of the given path to the result.
*/
@Override
public Path relativize(Path other) {
final SimpleAbstractPath that = checkPath(other);
if (this.equals(that)) {
return createPath(EMPTY_PATH);
}
if (isAbsolute() != that.isAbsolute()) {
throw Messages.path().relativizeAbsoluteRelativeMismatch();
}
if (path.isEmpty()) {
return other;
}
final int thisNameCount = getNameCount();
final int thatNameCount = that.getNameCount();
final int nameCount = Math.min(thisNameCount, thatNameCount);
int index = 0;
while (index < nameCount) {
if (!equalsNameAt(that, index)) {
break;
}
index++;
}
final int parentDirs = thisNameCount - index;
int length = parentDirs * 3 - 1;
if (index < thatNameCount) {
length += that.path.length() - that.offsets[index] + 1;
}
final StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < parentDirs; i++) {
sb.append(PARENT_DIR);
if (i < length) {
sb.append('/');
}
// Else don't add a trailing slash at the end
}
if (index < thatNameCount) {
sb.append(that.path, that.offsets[index], that.path.length());
}
return createPath(sb.toString());
}
private boolean equalsNameAt(SimpleAbstractPath that, int index) {
final int thisBegin = begin(index);
final int thisEnd = end(index);
final int thisLength = thisEnd - thisBegin;
final int thatBegin = that.begin(index);
final int thatEnd = that.end(index);
final int thatLength = thatEnd - thatBegin;
if (thisLength != thatLength) {
return false;
}
return path.regionMatches(thisBegin, that.path, thatBegin, thisLength);
}
/**
* Compares two abstract paths lexicographically.
* <p>
* This implementation checks if the given path is an instance of the same class, then compares the actual paths of the two abstract paths.
*/
@Override
public int compareTo(Path other) {
Objects.requireNonNull(other);
final SimpleAbstractPath that = getClass().cast(other);
return path.compareTo(that.path);
}
/**
* Tests this path for equality with the given object.
* <p>
* This implementation will return {@code true} if the given object is an instance of the same class as this path, with the same file system,
* and with the same actual path.
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || obj.getClass() != getClass()) {
return false;
}
SimpleAbstractPath other = (SimpleAbstractPath) obj;
return getFileSystem() == other.getFileSystem()
&& path.equals(other.path);
}
@Override
public int hashCode() {
return path.hashCode();
}
/**
* Returns the string representation of this path.
* <p>
* This implementation only returns the actual path.
*/
@Override
public String toString() {
return path;
}
private synchronized void initOffsets() {
if (offsets == null) {
if ("/".equals(path)) {
offsets = new int[0];
return;
}
boolean isAbsolute = isAbsolute();
// At least one result for non-root paths
int count = 1;
int start = isAbsolute ? 1 : 0;
while ((start = path.indexOf('/', start)) != -1) {
count++;
start++;
}
int[] result = new int[count];
start = isAbsolute ? 1 : 0;
int index = 0;
result[index++] = start;
while ((start = path.indexOf('/', start)) != -1) {
start++;
result[index++] = start;
}
offsets = result;
}
}
private int begin(int index) {
return offsets[index];
}
private int end(int index) {
return index == offsets.length - 1 ? path.length() : offsets[index + 1] - 1;
}
private SimpleAbstractPath checkPath(Path path) {
Objects.requireNonNull(path);
if (getClass().isInstance(path)) {
return getClass().cast(path);
}
throw new ProviderMismatchException();
}
}

@ -0,0 +1,63 @@
package org.xbib.io.ftp.fs;
import java.nio.file.attribute.FileAttribute;
import java.util.Objects;
/**
* A simple file attribute implementation.
*/
public class SimpleFileAttribute<T> implements FileAttribute<T> {
private final String name;
private final T value;
/**
* Creates a new file attribute.
*
* @param name The attribute name.
* @param value The attribute value.
* @throws NullPointerException If the name or value is {@code null}.
*/
public SimpleFileAttribute(String name, T value) {
this.name = Objects.requireNonNull(name);
this.value = Objects.requireNonNull(value);
}
@Override
public String name() {
return name;
}
@Override
public T value() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || o.getClass() != getClass()) {
return false;
}
SimpleFileAttribute<?> other = (SimpleFileAttribute<?>) o;
return name.equals(other.name)
&& value.equals(other.value);
}
@Override
public int hashCode() {
int hash = name.hashCode();
hash = 31 * hash + value.hashCode();
return hash;
}
@Override
public String toString() {
return getClass().getSimpleName()
+ "[name=" + name
+ ",value=" + value
+ "]";
}
}

@ -0,0 +1,18 @@
package org.xbib.io.ftp.fs;
import java.nio.file.attribute.GroupPrincipal;
/**
* A {@link GroupPrincipal} implementation that simply stores a name.
*/
public class SimpleGroupPrincipal extends SimpleUserPrincipal implements GroupPrincipal {
/**
* Creates a new group principal.
*
* @param name The name of the group principal.
*/
public SimpleGroupPrincipal(String name) {
super(name);
}
}

@ -0,0 +1,48 @@
package org.xbib.io.ftp.fs;
import java.nio.file.attribute.UserPrincipal;
import java.util.Objects;
/**
* A {@link UserPrincipal} implementation that simply stores a name.
*/
public class SimpleUserPrincipal implements UserPrincipal {
private final String name;
/**
* Creates a new user principal.
*
* @param name The name of the user principal.
*/
public SimpleUserPrincipal(String name) {
this.name = Objects.requireNonNull(name);
}
@Override
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || o.getClass() != getClass()) {
return false;
}
SimpleUserPrincipal other = (SimpleUserPrincipal) o;
return name.equals(other.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return getClass().getSimpleName() + "[" + name + "]";
}
}

@ -0,0 +1,17 @@
package org.xbib.io.ftp.fs;
/**
* The base class of option combinations that support file transfers.
*/
abstract class TransferOptions {
public final FileType fileType;
public final FileStructure fileStructure;
public final FileTransferMode fileTransferMode;
TransferOptions(FileType fileType, FileStructure fileStructure, FileTransferMode fileTransferMode) {
this.fileType = fileType;
this.fileStructure = fileStructure;
this.fileTransferMode = fileTransferMode;
}
}

@ -0,0 +1,97 @@
package org.xbib.io.ftp.fs;
import java.net.URI;
import java.net.URISyntaxException;
/**
* A utility class for {@link URI}s.
*/
public final class URISupport {
private URISupport() {
throw new Error("cannot create instances of " + getClass().getName());
}
/**
* Utility method that calls {@link URI#URI(String, String, String, int, String, String, String)}, wrapping any thrown {@link URISyntaxException}
* in an {@link IllegalArgumentException}.
*
* @param scheme The scheme name.
* @param userInfo The user name and authorization information.
* @param host The host name.
* @param port The port number.
* @param path The path.
* @param query The query.
* @param fragment The fragment.
* @return The created URI.
* @throws IllegalArgumentException If creating the URI caused a {@link URISyntaxException} to be thrown.
* @see URI#create(String)
*/
public static URI create(String scheme, String userInfo, String host, int port, String path, String query, String fragment) {
try {
return new URI(scheme, userInfo, host, port, path, query, fragment);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
/**
* Utility method that calls {@link URI#URI(String, String, String, String, String)}, wrapping any thrown {@link URISyntaxException} in an
* {@link IllegalArgumentException}.
*
* @param scheme The scheme name.
* @param authority The authority.
* @param path The path.
* @param query The query.
* @param fragment The fragment.
* @return The created URI.
* @throws IllegalArgumentException If creating the URI caused a {@link URISyntaxException} to be thrown.
* @see URI#create(String)
*/
public static URI create(String scheme, String authority, String path, String query, String fragment) {
try {
return new URI(scheme, authority, path, query, fragment);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
/**
* Utility method that calls {@link URI#URI(String, String, String, String)}, wrapping any thrown {@link URISyntaxException} in an
* {@link IllegalArgumentException}.
*
* @param scheme The scheme name.
* @param host The host name.
* @param path The path.
* @param fragment The fragment.
* @return The created URI.
* @throws IllegalArgumentException If creating the URI caused a {@link URISyntaxException} to be thrown.
* @see URI#create(String)
*/
public static URI create(String scheme, String host, String path, String fragment) {
try {
return new URI(scheme, host, path, fragment);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
/**
* Utility method that calls {@link URI#URI(String, String, String)}, wrapping any thrown {@link URISyntaxException} in an
* {@link IllegalArgumentException}.
*
* @param scheme The scheme name.
* @param ssp The scheme-specific part.
* @param fragment The fragment.
* @return The created URI.
* @throws IllegalArgumentException If creating the URI caused a {@link URISyntaxException} to be thrown.
* @see URI#create(String)
*/
public static URI create(String scheme, String ssp, String fragment) {
try {
return new URI(scheme, ssp, fragment);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
}

@ -0,0 +1,73 @@
package org.xbib.io.ftp.fs;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Locale;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;
/**
* A resource bundle control that uses UTF-8 instead of the default encoding when reading resources from properties files. It is thread-safe.
*/
public final class UTF8Control extends Control {
/**
* The single instance.
*/
public static final UTF8Control INSTANCE = new UTF8Control();
private UTF8Control() {
super();
}
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, final ClassLoader loader, final boolean reload)
throws IllegalAccessException, InstantiationException, IOException {
if (!"java.properties".equals(format)) {
return super.newBundle(baseName, locale, format, loader, reload);
}
String bundleName = toBundleName(baseName, locale);
ResourceBundle bundle = null;
final String resourceName = toResourceName(bundleName, "properties");
InputStream in = null;
try {
in = AccessController.doPrivileged(new PrivilegedExceptionAction<InputStream>() {
@Override
public InputStream run() throws Exception {
InputStream in = null;
if (reload) {
URL url = loader.getResource(resourceName);
if (url != null) {
URLConnection connection = url.openConnection();
if (connection != null) {
// Disable caches to get fresh data for reloading.
connection.setUseCaches(false);
in = connection.getInputStream();
}
}
} else {
in = loader.getResourceAsStream(resourceName);
}
return in;
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
if (in != null) {
try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
bundle = new PropertyResourceBundle(reader);
}
}
return bundle;
}
}

@ -0,0 +1,64 @@
invalidIndex=invalid index: %d
invalidRange=invalid range: %d - %d
unsupportedOperation=unsupported operation: %s.%s
byteChannel.negativePosition=invalid new position: %d
byteChannel.negativeSize=invalid new size: %d
directoryStream.closed=directory stream closed
directoryStream.iteratorAlreadyReturned=iterator already obtained
fileStore.unsupportedAttribute=unsupported attribute: %s
fileSystemProvider.illegalCopyOptionCombination=illegal combination of copy options: %s
fileSystemProvider.illegalOpenOptionCombination=illegal combination of open options: %s
fileSystemProvider.isDirectory=is a directory
fileSystemProvider.unsupportedFileAttributesType=unsupported file attributes type: %s
fileSystemProvider.unsupportedFileAttributeView=unsupported file attribute view: %s
fileSystemProvider.unsupportedFileAttribute=unsupported file attribute: %s
fileSystemProvider.unsupportedCopyOption=unsupported copy option: %s
fileSystemProvider.unsupportedOpenOption=unsupported open option: %s
fileSystemProvider.env.missingProperty=missing value for property '%s'
fileSystemProvider.env.invalidProperty=invalid value for property '%s': %s
fileSystemProvider.env.invalidPropertyCombination=invalid combination of properties: %s
path.nulCharacterNotAllowed=nul character not allowed
path.relativizeAbsoluteRelativeMismatch=cannot mix absolute and non-absolute paths in relativize
pathMatcher.unsupportedPathMatcherSyntax=unsupported syntax: %s
pathMatcher.syntaxNotFound=syntax not found in '%s'
pathMatcher.glob.nestedGroupsNotSupported=nested groups are not supported
pathMatcher.glob.unexpectedGroupEnd=unexpected }
pathMatcher.glob.missingGroupEnd=missing }
pathMatcher.glob.nestedClassesNotSupported=nested classes are not supported
pathMatcher.glob.unexpectedClassEnd=unexpected ]
pathMatcher.glob.missingClassEnd=missing ]
pathMatcher.glob.separatorNotAllowedInClass=separator not allowed in class
pathMatcher.glob.unescapableChar=no character to escape
uri.invalidScheme=URI has an invalid scheme (should be '%s'): %s
uri.notAbsolute=not an absolute URI: %s
uri.notHierarchical=not a hierarchical URI: %s
uri.hasAuthority=URI has an authority component: %s
uri.hasFragment=URI has a fragment component: %s
uri.hasHost=URI has a host component: %s
uri.hasPath=URI has a path component: %s
uri.hasPort=URI has a port number: %s
uri.hasQuery=URI has a query component: %s
uri.hasUserInfo=URI has a user-info component: %s
uri.hasNoAuthority=URI has no authority component: %s
uri.hasNoFragment=URI has no fragment component: %s
uri.hasNoHost=URI has no host component: %s
uri.hasNoPath=URI has no path component: %s
uri.hasNoPort=URI has no port number: %s
uri.hasNoQuery=URI has no query component: %s
uri.hasNoUserInfo=URI has no user-info component: %s
copyOfSymbolicLinksAcrossFileSystemsNotSupported=copying of symbolic links is not supported across file systems

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save