From 4e8db250174d8ba2a8eab7289522eaea1531fda3 Mon Sep 17 00:00:00 2001 From: Adam Murdoch Date: Sat, 1 Sep 2012 16:11:30 +1000 Subject: [PATCH] Added PosixFile.symlink() and readLink(). --- readme.md | 3 +- src/main/cpp/generic.cpp | 2 +- src/main/cpp/posix.cpp | 42 +++++++++++ .../rubygrapefruit/platform/PosixFile.java | 14 ++++ .../platform/internal/DefaultPosixFile.java | 35 ++++++++- .../internal/jni/NativeLibraryFunctions.java | 2 +- .../internal/jni/PosixFileFunctions.java | 4 ++ .../platform/PosixFileTest.groovy | 72 ++++++++++++++++--- 8 files changed, 161 insertions(+), 13 deletions(-) diff --git a/readme.md b/readme.md index d7f0750..b5ba2e8 100755 --- a/readme.md +++ b/readme.md @@ -27,6 +27,7 @@ These bindings work for both the UNIX terminal and the Windows console: ### File systems * Get and set UNIX file mode. +* Create and read symbolic links. * List the available file systems on the machine * Query file system mount point. * Query file system type. @@ -37,7 +38,7 @@ These bindings work for both the UNIX terminal and the Windows console: Currently ported to OS X, Linux, Solaris and Windows. Tested on: -* OS X 10.7.4, 10.8 (x86_64) +* OS X 10.7.4, 10.8 (x86_64), 10.6.7 (i386) * Ubunutu 12.04 (amd64) * Solaris 11 (x86) * Windows 7 (amd64) diff --git a/src/main/cpp/generic.cpp b/src/main/cpp/generic.cpp index ea18805..b594450 100755 --- a/src/main/cpp/generic.cpp +++ b/src/main/cpp/generic.cpp @@ -17,5 +17,5 @@ void mark_failed_with_code(JNIEnv *env, const char* message, int error_code, job JNIEXPORT jint JNICALL Java_net_rubygrapefruit_platform_internal_jni_NativeLibraryFunctions_getVersion(JNIEnv *env, jclass target) { - return 6; + return 7; } diff --git a/src/main/cpp/posix.cpp b/src/main/cpp/posix.cpp index 72deb3a..30d18af 100755 --- a/src/main/cpp/posix.cpp +++ b/src/main/cpp/posix.cpp @@ -80,6 +80,48 @@ Java_net_rubygrapefruit_platform_internal_jni_PosixFileFunctions_stat(JNIEnv *en env->SetIntField(dest, modeField, 0777 & fileInfo.st_mode); } +JNIEXPORT void JNICALL +Java_net_rubygrapefruit_platform_internal_jni_PosixFileFunctions_symlink(JNIEnv *env, jclass target, jbyteArray path, jbyteArray contents, jobject result) { + jbyte* pathUtf8 = env->GetByteArrayElements(path, NULL); + jbyte* contentsUtf8 = env->GetByteArrayElements(contents, NULL); + int retval = symlink((const char*)contentsUtf8, (const char*)pathUtf8); + env->ReleaseByteArrayElements(path, pathUtf8, JNI_ABORT); + env->ReleaseByteArrayElements(contents, contentsUtf8, JNI_ABORT); + if (retval != 0) { + mark_failed_with_errno(env, "could not symlink", result); + } +} + +JNIEXPORT jbyteArray JNICALL +Java_net_rubygrapefruit_platform_internal_jni_PosixFileFunctions_readlink(JNIEnv *env, jclass target, jbyteArray path, jobject result) { + struct stat link_info; + jbyte* pathUtf8 = env->GetByteArrayElements(path, NULL); + int retval = lstat((const char*)pathUtf8, &link_info); + if (retval != 0) { + env->ReleaseByteArrayElements(path, pathUtf8, JNI_ABORT); + mark_failed_with_errno(env, "could not lstat file", result); + return NULL; + } + + jbyteArray contents = env->NewByteArray(link_info.st_size); + if (contents == NULL) { + env->ReleaseByteArrayElements(path, pathUtf8, JNI_ABORT); + mark_failed_with_message(env, "could not create array", result); + return NULL; + } + + jbyte* contentsUtf8 = env->GetByteArrayElements(contents, NULL); + retval = readlink((const char*)pathUtf8, (char*)contentsUtf8, link_info.st_size); + env->ReleaseByteArrayElements(path, pathUtf8, JNI_ABORT); + if (retval < 0) { + mark_failed_with_errno(env, "could not readlink", result); + env->ReleaseByteArrayElements(contents, contentsUtf8, JNI_ABORT); + return NULL; + } + env->ReleaseByteArrayElements(contents, contentsUtf8, JNI_COMMIT); + return contents; +} + /* * Process functions */ diff --git a/src/main/java/net/rubygrapefruit/platform/PosixFile.java b/src/main/java/net/rubygrapefruit/platform/PosixFile.java index 433d092..ccae574 100644 --- a/src/main/java/net/rubygrapefruit/platform/PosixFile.java +++ b/src/main/java/net/rubygrapefruit/platform/PosixFile.java @@ -19,4 +19,18 @@ public interface PosixFile extends NativeIntegration { * @throws NativeException On failure. */ int getMode(File path) throws NativeException; + + /** + * Creates a symbolic link. + * + * @throws NativeException On failure. + */ + void symlink(File link, String contents) throws NativeException; + + /** + * Reads the contents of a symbolic link. + * + * @throws NativeException On failure. + */ + String readLink(File link) throws NativeException; } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/DefaultPosixFile.java b/src/main/java/net/rubygrapefruit/platform/internal/DefaultPosixFile.java index 46bddf9..9f43348 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/DefaultPosixFile.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/DefaultPosixFile.java @@ -40,12 +40,43 @@ public class DefaultPosixFile implements PosixFile { return stat.mode; } + @Override + public String readLink(File link) throws NativeException { + FunctionResult result = new FunctionResult(); + byte[] encodedContents = PosixFileFunctions.readlink(encode(link), result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not read symlink %s: %s", link, result.getMessage())); + } + return decode(encodedContents); + } + + @Override + public void symlink(File link, String contents) throws NativeException { + FunctionResult result = new FunctionResult(); + PosixFileFunctions.symlink(encode(link), encode(contents), result); + if (result.isFailed()) { + throw new NativeException(String.format("Could not create symlink %s: %s", link, result.getMessage())); + } + } + + private String decode(byte[] path) { + try { + return new String(path, 0, path.length, characterEncoding); + } catch (UnsupportedEncodingException e) { + throw new NativeException(String.format("Could not decode path using encoding %s.", characterEncoding), e); + } + } + private byte[] encode(File file) { + return encode(file.getPath()); + } + + private byte[] encode(String path) { byte[] encodedName; try { - encodedName = file.getPath().getBytes(characterEncoding); + encodedName = path.getBytes(characterEncoding); } catch (UnsupportedEncodingException e) { - throw new NativeException(String.format("Could not encode path for file '%s' using encoding %s.", file.getName(), characterEncoding), e); + throw new NativeException(String.format("Could not encode path '%s' using encoding %s.", path, characterEncoding), e); } byte[] buffer = new byte[encodedName.length + 1]; System.arraycopy(encodedName, 0, buffer, 0, encodedName.length); diff --git a/src/main/java/net/rubygrapefruit/platform/internal/jni/NativeLibraryFunctions.java b/src/main/java/net/rubygrapefruit/platform/internal/jni/NativeLibraryFunctions.java index bbeeb43..b5310a0 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/jni/NativeLibraryFunctions.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/jni/NativeLibraryFunctions.java @@ -4,7 +4,7 @@ import net.rubygrapefruit.platform.internal.FunctionResult; import net.rubygrapefruit.platform.internal.MutableSystemInfo; public class NativeLibraryFunctions { - public static final int VERSION = 6; + public static final int VERSION = 7; public static native int getVersion(); diff --git a/src/main/java/net/rubygrapefruit/platform/internal/jni/PosixFileFunctions.java b/src/main/java/net/rubygrapefruit/platform/internal/jni/PosixFileFunctions.java index ae02e02..4acf530 100755 --- a/src/main/java/net/rubygrapefruit/platform/internal/jni/PosixFileFunctions.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/jni/PosixFileFunctions.java @@ -7,4 +7,8 @@ public class PosixFileFunctions { public static native void chmod(byte[] file, int perms, FunctionResult result); public static native void stat(byte[] file, FileStat stat, FunctionResult result); + + public static native void symlink(byte[] file, byte[] content, FunctionResult result); + + public static native byte[] readlink(byte[] file, FunctionResult result); } diff --git a/src/test/groovy/net/rubygrapefruit/platform/PosixFileTest.groovy b/src/test/groovy/net/rubygrapefruit/platform/PosixFileTest.groovy index 2336fa0..1e9584b 100755 --- a/src/test/groovy/net/rubygrapefruit/platform/PosixFileTest.groovy +++ b/src/test/groovy/net/rubygrapefruit/platform/PosixFileTest.groovy @@ -31,25 +31,81 @@ class PosixFileTest extends Specification { file.getMode(testFile) == 0740 } - def "throws exception on failure to set mode"() { - def file = new File(tmpDir.root, "unknown") + def "cannot set mode on file that does not exist"() { + def testFile = new File(tmpDir.root, "unknown") when: - this.file.setMode(file, 0660) + file.setMode(testFile, 0660) then: NativeException e = thrown() - e.message == "Could not set UNIX mode on $file: could not chmod file (errno 2)" + e.message == "Could not set UNIX mode on $testFile: could not chmod file (errno 2)" } - def "throws exception on failure to get mode"() { - def file = new File(tmpDir.root, "unknown") + def "cannot get mode on file that does not exist"() { + def testFile = new File(tmpDir.root, "unknown") when: - this.file.getMode(file) + file.getMode(testFile) then: NativeException e = thrown() - e.message == "Could not get UNIX mode on $file: could not stat file (errno 2)" + e.message == "Could not get UNIX mode on $testFile: could not stat file (errno 2)" + } + + def "can create symbolic link"() { + def testFile = new File(tmpDir.root, "test.txt") + testFile.text = "hi" + def symlinkFile = new File(tmpDir.root, "symlink") + + when: + file.symlink(symlinkFile, testFile.name) + + then: + symlinkFile.file + symlinkFile.text == "hi" + symlinkFile.canonicalFile == testFile.canonicalFile + } + + def "can read symbolic link"() { + def symlinkFile = new File(tmpDir.root, "symlink") + + when: + file.symlink(symlinkFile, "target") + + then: + file.readLink(symlinkFile) == "target" + } + + def "cannot read a symlink that does not exist"() { + def symlinkFile = new File(tmpDir.root, "symlink") + + when: + file.readLink(symlinkFile) + + then: + NativeException e = thrown() + e.message == "Could not read symlink $symlinkFile: could not lstat file (errno 2)" + } + + def "cannot read a symlink that is not a symlink"() { + def symlinkFile = tmpDir.newFile("not-a-symlink.txt") + + when: + file.readLink(symlinkFile) + + then: + NativeException e = thrown() + e.message == "Could not read symlink $symlinkFile: could not readlink (errno 22)" + } + + def "can create and read symlink with unicode in its name"() { + def symlinkFile = new File(tmpDir.root, "symlink\u03b2") + + when: + file.symlink(symlinkFile, "target\u03b2") + + then: + file.readLink(symlinkFile) == "target\u03b2" } }