initial commit
This commit is contained in:
commit
6eca3c6d2b
967 changed files with 143643 additions and 0 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
gradlew.bat text eol=crlf
|
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
data
|
||||
work
|
||||
out
|
||||
logs
|
||||
/.idea
|
||||
/target
|
||||
/.settings
|
||||
/.classpath
|
||||
/.project
|
||||
/.gradle
|
||||
/plugins
|
||||
/sessions
|
||||
.DS_Store
|
||||
*.iml
|
||||
*~
|
||||
.secret
|
||||
build
|
||||
**/*.crt
|
||||
**/*.pkcs8
|
||||
**/*.gz
|
34
build.gradle
Normal file
34
build.gradle
Normal file
|
@ -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')
|
5
files-eddsa/NOTICE.txt
Normal file
5
files-eddsa/NOTICE.txt
Normal file
|
@ -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
|
4
files-eddsa/build.gradle
Normal file
4
files-eddsa/build.gradle
Normal file
|
@ -0,0 +1,4 @@
|
|||
dependencies {
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${project.property('junit.version')}"
|
||||
testImplementation "junit:junit:${project.property('junit4.version')}"
|
||||
}
|
6
files-eddsa/src/main/java/module-info.java
Normal file
6
files-eddsa/src/main/java/module-info.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
95
files-eddsa/src/main/java/org/xbib/io/sshd/eddsa/Utils.java
Normal file
95
files-eddsa/src/main/java/org/xbib/io/sshd/eddsa/Utils.java
Normal file
|
@ -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()
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
File diff suppressed because it is too large
Load diff
|
@ -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 &= 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) \\
|
||||
* &= 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 \\
|
||||
* &= 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");
|
||||
}
|
||||
}
|
121
files-eddsa/src/test/java/org/xbib/io/sshd/eddsa/UtilsTest.java
Normal file
121
files-eddsa/src/test/java/org/xbib/io/sshd/eddsa/UtilsTest.java
Normal file
|
@ -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 },
|
||||
},
|
File diff suppressed because it is too large
Load diff
1024
files-eddsa/src/test/resources/org/xbib/io/sshd/eddsa/test.data
Normal file
1024
files-eddsa/src/test/resources/org/xbib/io/sshd/eddsa/test.data
Normal file
File diff suppressed because it is too large
Load diff
6
files-ftp-fs/NOTICE.txt
Normal file
6
files-ftp-fs/NOTICE.txt
Normal file
|
@ -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
|
7
files-ftp-fs/build.gradle
Normal file
7
files-ftp-fs/build.gradle
Normal file
|
@ -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')}"
|
||||
}
|
5
files-ftp-fs/src/main/java/module-info.java
Normal file
5
files-ftp-fs/src/main/java/module-info.java
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
151
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/AbstractPath.java
Normal file
151
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/AbstractPath.java
Normal file
|
@ -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);
|
||||
}
|
101
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/CopyOptions.java
Normal file
101
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/CopyOptions.java
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
446
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/FTPClientPool.java
Normal file
446
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/FTPClientPool.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
803
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/FTPFileSystem.java
Normal file
803
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/FTPFileSystem.java
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
198
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/FTPPath.java
Normal file
198
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/FTPPath.java
Normal file
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
197
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/FileType.java
Normal file
197
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/FileType.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
808
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/Messages.java
Normal file
808
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/Messages.java
Normal file
|
@ -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>}</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>}</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));
|
||||
}
|
||||
}
|
||||
}
|
196
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/OpenOptions.java
Normal file
196
files-ftp-fs/src/main/java/org/xbib/io/ftp/fs/OpenOptions.java
Normal file
|
@ -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 @@
|
|||
org.xbib.io.ftp.fs.FTPFileSystemProvider
|
|
@ -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…
Reference in a new issue