summaryrefslogtreecommitdiff
path: root/libs/sftp/src/main/cpp/sftp.cpp
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2024-09-25 21:12:24 +0200
committerJoel Klinghed <the_jk@spawned.biz>2024-09-25 21:12:24 +0200
commit28a55fdc69e31490a4086ecae8cc687f40ba0b94 (patch)
tree9bde6e49eb091f912e8a9f8b2853d87f6a932d27 /libs/sftp/src/main/cpp/sftp.cpp
parent07d35782b377a8b98cf8dbbb5734d3f2514bccd5 (diff)
Add libs:sftp
sftp implementation using libssh2 and openssl
Diffstat (limited to 'libs/sftp/src/main/cpp/sftp.cpp')
-rw-r--r--libs/sftp/src/main/cpp/sftp.cpp688
1 files changed, 688 insertions, 0 deletions
diff --git a/libs/sftp/src/main/cpp/sftp.cpp b/libs/sftp/src/main/cpp/sftp.cpp
new file mode 100644
index 0000000..0feafb1
--- /dev/null
+++ b/libs/sftp/src/main/cpp/sftp.cpp
@@ -0,0 +1,688 @@
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <unistd.h>
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "jni.hpp"
+#include "libssh2.h"
+#include "libssh2_sftp.h"
+
+namespace {
+
+class SftpOwner {
+ public:
+ SftpOwner(std::shared_ptr<LIBSSH2_SESSION> ssh_session, LIBSSH2_SFTP* sftp)
+ : ssh_session_(std::move(ssh_session)), sftp_(sftp) {}
+
+ ~SftpOwner() {
+ libssh2_sftp_shutdown(sftp_);
+ }
+
+ SftpOwner(const SftpOwner&) = delete;
+ SftpOwner& operator=(const SftpOwner&) = delete;
+
+ [[nodiscard]] LIBSSH2_SFTP* get() const { return sftp_; }
+
+ private:
+ std::shared_ptr<LIBSSH2_SESSION> ssh_session_;
+ LIBSSH2_SFTP* sftp_;
+};
+
+class SftpHandle {
+ public:
+ SftpHandle(std::shared_ptr<SftpOwner> sftp, LIBSSH2_SFTP_HANDLE* handle)
+ : sftp_(std::move(sftp)), handle_(handle) {}
+
+ ~SftpHandle() {
+ libssh2_sftp_close_handle(handle_);
+ }
+
+ SftpHandle(const SftpHandle&) = delete;
+ SftpHandle& operator=(const SftpHandle&) = delete;
+
+ protected:
+ [[nodiscard]] LIBSSH2_SFTP_HANDLE* get() const { return handle_; }
+
+ private:
+ std::shared_ptr<SftpOwner> sftp_;
+ LIBSSH2_SFTP_HANDLE* handle_;
+};
+
+jni::LocalRef<jobject> CreateDirEntry(JNIEnv* env, const char* name,
+ const LIBSSH2_SFTP_ATTRIBUTES& attrs);
+
+class Dir : SftpHandle {
+ public:
+ Dir(std::shared_ptr<SftpOwner> sftp, LIBSSH2_SFTP_HANDLE *handle)
+ : SftpHandle(std::move(sftp), handle) {}
+
+ jni::LocalRef<jobjectArray> List(JNIEnv* env, const jni::Ref<jclass>& entry_clazz) {
+ std::vector<jni::LocalRef<jobject>> tmp;
+ size_t bufsize = 1024;
+ auto buf = std::make_unique<char[]>(bufsize);
+ LIBSSH2_SFTP_ATTRIBUTES attrs;
+ while (true) {
+ auto size = bufsize / 2;
+ auto ret = libssh2_sftp_readdir_ex(get(), buf.get(), size, buf.get() + size, size, &attrs);
+ if (ret == 0)
+ break;
+ if (ret > 0) {
+ // Skip . and .. entries.
+ if (buf[0] == '.' && (ret == 1 || (buf[1] == '.' && ret == 2)))
+ continue;
+ auto obj = CreateDirEntry(env, buf.get(), attrs);
+ if (obj)
+ tmp.push_back(std::move(obj));
+ } else if (ret == LIBSSH2_ERROR_BUFFER_TOO_SMALL) {
+ auto newsize = bufsize * 2;
+ // Protect against overflow
+ if (newsize <= bufsize)
+ break;
+ buf = std::make_unique<char[]>(newsize);
+ } else {
+ // Error
+ break;
+ }
+ }
+ return jni::CreateArray(env, entry_clazz, std::move(tmp));
+ }
+};
+
+class File : SftpHandle {
+ public:
+ File(std::shared_ptr<SftpOwner> sftp, LIBSSH2_SFTP_HANDLE *handle)
+ : SftpHandle(std::move(sftp), handle) {}
+
+ int32_t Read(uint8_t* data, int32_t size) {
+ if (size <= 0) return 0;
+ return libssh2_sftp_read(get(), reinterpret_cast<char*>(data), size);
+ }
+
+ void Seek(int64_t offset) {
+ libssh2_sftp_seek64(get(), offset);
+ }
+
+ int32_t Write(const uint8_t* data, int32_t size) {
+ if (size <= 0) return 0;
+ return libssh2_sftp_write(get(), reinterpret_cast<const char*>(data), size);
+ }
+};
+
+class SftpSession {
+ public:
+ explicit SftpSession(std::shared_ptr<SftpOwner> sftp)
+ : sftp_(std::move(sftp)) {}
+
+ ~SftpSession() = default;
+
+ SftpSession(const SftpSession &) = delete;
+
+ SftpSession &operator=(const SftpSession &) = delete;
+
+ std::string GetLastError() {
+ switch (libssh2_sftp_last_error(sftp_->get())) {
+ case LIBSSH2_FX_OK:
+ return "";
+ case LIBSSH2_FX_EOF:
+ return "End of file";
+ case LIBSSH2_FX_NO_SUCH_FILE:
+ return "No such file";
+ case LIBSSH2_FX_PERMISSION_DENIED:
+ return "Permission denied";
+ case LIBSSH2_FX_FAILURE:
+ return "Failure";
+ case LIBSSH2_FX_BAD_MESSAGE:
+ return "Bad message";
+ case LIBSSH2_FX_NO_CONNECTION:
+ return "No connection";
+ case LIBSSH2_FX_CONNECTION_LOST:
+ return "Connection lost";
+ case LIBSSH2_FX_OP_UNSUPPORTED:
+ return "Operation unsupported";
+ case LIBSSH2_FX_INVALID_HANDLE:
+ return "Invalid handle";
+ case LIBSSH2_FX_NO_SUCH_PATH:
+ return "No such path";
+ case LIBSSH2_FX_FILE_ALREADY_EXISTS:
+ return "File already exists";
+ case LIBSSH2_FX_WRITE_PROTECT:
+ return "Write protected";
+ case LIBSSH2_FX_NO_MEDIA:
+ return "No media";
+ case LIBSSH2_FX_NO_SPACE_ON_FILESYSTEM:
+ return "No space on filesystem";
+ case LIBSSH2_FX_QUOTA_EXCEEDED:
+ return "Quota exceeded";
+ case LIBSSH2_FX_UNKNOWN_PRINCIPAL:
+ return "Unknown principal";
+ case LIBSSH2_FX_LOCK_CONFLICT:
+ return "Lock conflict";
+ case LIBSSH2_FX_DIR_NOT_EMPTY:
+ return "Directory is not empty";
+ case LIBSSH2_FX_NOT_A_DIRECTORY:
+ return "Not a directory";
+ case LIBSSH2_FX_INVALID_FILENAME:
+ return "Invalid filename";
+ case LIBSSH2_FX_LINK_LOOP:
+ return "Link loop";
+ default:
+ return "Unknown error";
+ }
+ }
+
+ std::unique_ptr<Dir> OpenDir(const std::string &path) {
+ auto* handle = libssh2_sftp_open_ex(sftp_->get(), path.data(), path.size(),
+ LIBSSH2_FXF_READ,
+ 0700,
+ LIBSSH2_SFTP_OPENDIR);
+ if (!handle) return nullptr;
+ return std::make_unique<Dir>(sftp_, handle);
+ }
+
+ bool MakeDir(const std::string &path, int32_t mode) {
+ return libssh2_sftp_mkdir_ex(sftp_->get(), path.data(), path.size(), mode) == 0;
+ }
+
+ bool RemoveDir(const std::string &path) {
+ return libssh2_sftp_rmdir_ex(sftp_->get(), path.data(), path.size()) == 0;
+ }
+
+ std::unique_ptr<File> OpenFile(const std::string &path, int32_t mode) {
+ unsigned long flags = 0;
+ switch (mode) {
+ case 0: // READ(0),
+ flags |= LIBSSH2_FXF_READ;
+ break;
+ case 1: // WRITE_CREATE_TRUNCATE(1),
+ flags |= LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_TRUNC;
+ break;
+ default:
+ return nullptr;
+ }
+ auto* handle = libssh2_sftp_open_ex(sftp_->get(), path.data(), path.size(),
+ flags,
+ 0700,
+ LIBSSH2_SFTP_OPENFILE);
+ if (!handle) return nullptr;
+ return std::make_unique<File>(sftp_, handle);
+ }
+
+ bool Unlink(const std::string &path) {
+ return libssh2_sftp_unlink_ex(sftp_->get(), path.data(), path.size()) == 0;
+ }
+
+ bool Symlink(const std::string &target, const std::string &path) {
+ // symlink fails is path already exists, so remove any existing entry first.
+ libssh2_sftp_unlink_ex(sftp_->get(), path.data(), path.size());
+ // The argument order does not seem to match the documentation, so this
+ // might change?
+ return libssh2_sftp_symlink_ex(sftp_->get(), target.data(), target.size(),
+ const_cast<char*>(path.data()), path.size(),
+ LIBSSH2_SFTP_SYMLINK) == 0;
+ }
+
+ std::optional<std::string> Readlink(const std::string &path) {
+ unsigned int bufsize = 8192;
+ std::vector<char> buf;
+ while (true) {
+ buf.resize(bufsize);
+ auto ret = libssh2_sftp_symlink_ex(sftp_->get(), path.data(), path.size(),
+ buf.data(), buf.size(), LIBSSH2_SFTP_READLINK);
+ if (ret != LIBSSH2_ERROR_BUFFER_TOO_SMALL) {
+ if (ret >= 0) return std::string(buf.data(), ret);
+ return std::nullopt;
+ }
+ const auto previous = bufsize;
+ bufsize *= 2;
+ // Check for bufsize overflow.
+ if (bufsize <= previous)
+ return std::nullopt;
+ }
+ }
+
+ jni::LocalRef<jobject> Stat(JNIEnv* env, const std::string &path, bool follow_link) {
+ LIBSSH2_SFTP_ATTRIBUTES attrs;
+ auto ret = libssh2_sftp_stat_ex(sftp_->get(), path.data(), path.size(),
+ follow_link ? LIBSSH2_SFTP_STAT : LIBSSH2_SFTP_LSTAT,
+ &attrs);
+ if (ret) return nullptr;
+ return CreateDirEntry(env, path.c_str(), attrs);
+ }
+
+ private:
+ std::shared_ptr<SftpOwner> sftp_;
+};
+
+class unique_fd {
+ public:
+ constexpr unique_fd()
+ : fd_(-1) {}
+ explicit unique_fd(int fd)
+ : fd_(fd) {}
+
+ ~unique_fd() {
+ if (fd_ != -1) close(fd_);
+ }
+
+ unique_fd(const unique_fd&) = delete;
+ unique_fd& operator=(const unique_fd&) = delete;
+ unique_fd(unique_fd&& other) noexcept
+ : fd_(other.release()) {}
+ unique_fd& operator=(unique_fd&& other) noexcept {
+ reset(other.release());
+ return *this;
+ }
+
+ [[nodiscard]] int get() const { return fd_; }
+
+ explicit operator bool() const { return fd_ != -1; }
+
+ int release() {
+ int fd = fd_;
+ fd_ = -1;
+ return fd;
+ }
+
+ void reset() {
+ reset(-1);
+ }
+
+ void reset(int fd) {
+ if (fd_ != -1) close(fd_);
+ fd_ = fd;
+ }
+
+ private:
+ int fd_;
+};
+
+class SshSession {
+ public:
+ explicit SshSession(LIBSSH2_SESSION* session) : session_(session, SessionDeleter{}) {
+ libssh2_session_set_blocking(session_.get(), 1);
+ }
+ ~SshSession() = default;
+
+ SshSession(const SshSession&) = delete;
+ SshSession& operator=(const SshSession&) = delete;
+
+ std::string GetLastError() {
+ char* ptr = nullptr;
+ int len = 0;
+ libssh2_session_last_error(session_.get(), &ptr, &len, /* want_buf */ 0);
+ return {ptr, static_cast<size_t>(len)};
+ }
+
+ bool Connect(const std::string& host, int32_t port) {
+ struct addrinfo hints{};
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = 0;
+ hints.ai_flags = AI_ADDRCONFIG;
+ struct addrinfo* ptr = nullptr;
+ char tmp[10];
+ snprintf(tmp, sizeof(tmp), "%d", static_cast<int>(port));
+ int ret = getaddrinfo(host.c_str(), tmp, &hints, &ptr);
+ if (ret) {
+ libssh2_session_set_last_error(session_.get(), LIBSSH2_ERROR_SOCKET_NONE, gai_strerror(ret));
+ return false;
+ }
+ auto* i = ptr;
+ for (; i; i = i->ai_next) {
+ unique_fd fd(socket(i->ai_family, i->ai_socktype, i->ai_protocol));
+ if (fd) {
+ if (connect(fd.get(), i->ai_addr, i->ai_addrlen) == 0) {
+ fd_ = std::move(fd);
+ freeaddrinfo(ptr);
+ return true;
+ }
+ }
+ libssh2_session_set_last_error(session_.get(), LIBSSH2_ERROR_SOCKET_NONE, strerror(errno));
+ }
+ freeaddrinfo(ptr);
+ return false;
+ }
+
+ std::optional<std::vector<uint8_t>> Handshake() {
+ auto ret = libssh2_session_handshake(session_.get(), fd_.get());
+ if (ret)
+ return std::nullopt;
+ auto* fingerprint = libssh2_hostkey_hash(session_.get(), LIBSSH2_HOSTKEY_HASH_SHA256);
+ if (!fingerprint)
+ return std::nullopt;
+ return std::vector<uint8_t>(fingerprint, fingerprint + 32);
+ }
+
+ bool Authenticate(const std::string& username, const std::string& password) {
+ return libssh2_userauth_password_ex(session_.get(),
+ username.data(), username.size(),
+ password.data(), password.size(),
+ nullptr) == 0;
+ }
+
+ bool Authenticate(const std::string& username, const std::vector<uint8_t>& public_key,
+ const std::vector<uint8_t>& private_key, const std::string& passphrase) {
+ return libssh2_userauth_publickey_frommemory(
+ session_.get(), username.data(), username.size(),
+ reinterpret_cast<const char*>(public_key.data()), public_key.size(),
+ reinterpret_cast<const char*>(private_key.data()), private_key.size(),
+ passphrase.c_str()) == 0;
+ }
+
+ std::unique_ptr<SftpSession> NewSftpSession() {
+ auto* sftp_session = libssh2_sftp_init(session_.get());
+ if (!sftp_session) return nullptr;
+ return std::make_unique<SftpSession>(std::make_shared<SftpOwner>(session_, sftp_session));
+ }
+
+ private:
+ struct SessionDeleter {
+ void operator()(LIBSSH2_SESSION* session) {
+ if (session) {
+ libssh2_session_disconnect(session, "Normal Shutdown");
+ libssh2_session_free(session);
+ }
+ }
+ };
+
+ std::shared_ptr<LIBSSH2_SESSION> session_;
+ unique_fd fd_;
+};
+
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "MemoryLeak"
+jlong nativeSshSessionNew(JNIEnv*, jclass) {
+ auto* session = libssh2_session_init();
+ if (!session) return 0;
+ return reinterpret_cast<jlong>(new SshSession(session));
+}
+#pragma clang diagnostic pop
+
+void nativeSshSessionDestroy(JNIEnv*, jclass, jlong ptr) {
+ delete reinterpret_cast<SshSession*>(ptr);
+}
+
+jstring nativeSshSessionGetLastError(JNIEnv* env, jclass, jlong ptr) {
+ return jni::UTF8ToString(env, reinterpret_cast<SshSession*>(ptr)->GetLastError()).release();
+}
+
+jboolean nativeSshSessionConnect(JNIEnv* env, jclass, jlong ptr, jstring host, jint port) {
+ return reinterpret_cast<SshSession*>(ptr)->Connect(
+ jni::StringToUTF8(env, jni::ParamRef<jstring>(env, host)),
+ static_cast<int32_t>(port)) ? JNI_TRUE : JNI_FALSE;
+}
+
+jbyteArray nativeSshSessionHandshake(JNIEnv* env, jclass, jlong ptr) {
+ auto fingerprint = reinterpret_cast<SshSession*>(ptr)->Handshake();
+ if (fingerprint.has_value()) {
+ return jni::VectorToByteArray(env, fingerprint.value()).release();
+ }
+ return nullptr;
+}
+
+jboolean nativeSshSessionAuthenticate(JNIEnv* env, jclass, jlong ptr, jstring j_username,
+ jstring password, jbyteArray public_key,
+ jbyteArray private_key) {
+ auto username = jni::StringToUTF8(env, jni::ParamRef<jstring>(env, j_username));
+ if (public_key != nullptr && private_key != nullptr) {
+ return reinterpret_cast<SshSession*>(ptr)->Authenticate(
+ username,
+ jni::ByteArrayToVector(env, jni::ParamRef<jbyteArray>(env, public_key)),
+ jni::ByteArrayToVector(env, jni::ParamRef<jbyteArray>(env, private_key)),
+ password != nullptr ? jni::StringToUTF8(env, jni::ParamRef<jstring>(env, password)) : "")
+ ? JNI_TRUE : JNI_FALSE;
+ }
+ if (password != nullptr) {
+ return reinterpret_cast<SshSession *>(ptr)->Authenticate(
+ username,
+ jni::StringToUTF8(env, jni::ParamRef<jstring>(env, password)))
+ ? JNI_TRUE : JNI_FALSE;
+ }
+ return JNI_FALSE;
+}
+
+jlong nativeSshSessionNewSftpSession(JNIEnv*, jclass, jlong ptr) {
+ return reinterpret_cast<jlong>(reinterpret_cast<SshSession*>(ptr)->NewSftpSession().release());
+}
+
+void nativeSftpSessionDestroy(JNIEnv*, jclass, jlong ptr) {
+ delete reinterpret_cast<SftpSession*>(ptr);
+}
+
+jstring nativeSftpSessionGetLastError(JNIEnv* env, jclass, jlong ptr) {
+ return jni::UTF8ToString(env, reinterpret_cast<SftpSession*>(ptr)->GetLastError()).release();
+}
+
+jlong nativeSftpSessionOpenDir(JNIEnv* env, jclass, jlong ptr, jstring path) {
+ auto dir = reinterpret_cast<SftpSession*>(ptr)->OpenDir(
+ jni::StringToUTF8(env, jni::ParamRef<jstring>(env, path)));
+ if (!dir) return 0L;
+ return reinterpret_cast<jlong>(dir.release());
+}
+
+jboolean nativeSftpSessionMakeDir(JNIEnv* env, jclass, jlong ptr, jstring path, jint mode) {
+ return reinterpret_cast<SftpSession*>(ptr)->MakeDir(
+ jni::StringToUTF8(env, jni::ParamRef<jstring>(env, path)), mode)
+ ? JNI_TRUE : JNI_FALSE;
+}
+
+jboolean nativeSftpSessionRemoveDir(JNIEnv* env, jclass, jlong ptr, jstring path) {
+ return reinterpret_cast<SftpSession*>(ptr)->RemoveDir(
+ jni::StringToUTF8(env, jni::ParamRef<jstring>(env, path)))
+ ? JNI_TRUE : JNI_FALSE;
+}
+
+jlong nativeSftpSessionOpenFile(JNIEnv* env, jclass, jlong ptr, jstring path, jint mode) {
+ auto file = reinterpret_cast<SftpSession*>(ptr)->OpenFile(
+ jni::StringToUTF8(env, jni::ParamRef<jstring>(env, path)), mode);
+ if (!file) return 0L;
+ return reinterpret_cast<jlong>(file.release());
+}
+
+jboolean nativeSftpSessionUnlink(JNIEnv* env, jclass, jlong ptr, jstring path) {
+ return reinterpret_cast<SftpSession*>(ptr)->Unlink(
+ jni::StringToUTF8(env, jni::ParamRef<jstring>(env, path)))
+ ? JNI_TRUE : JNI_FALSE;
+}
+
+jboolean nativeSftpSessionSymlink(JNIEnv* env, jclass, jlong ptr, jstring target, jstring path) {
+ return reinterpret_cast<SftpSession*>(ptr)->Symlink(
+ jni::StringToUTF8(env, jni::ParamRef<jstring>(env, target)),
+ jni::StringToUTF8(env, jni::ParamRef<jstring>(env, path)))
+ ? JNI_TRUE : JNI_FALSE;
+}
+
+jstring nativeSftpSessionReadlink(JNIEnv* env, jclass, jlong ptr, jstring path) {
+ auto target = reinterpret_cast<SftpSession*>(ptr)->Readlink(
+ jni::StringToUTF8(env, jni::ParamRef<jstring>(env, path)));
+ if (target.has_value()) return jni::UTF8ToString(env, target.value()).release();
+ return nullptr;
+}
+
+jobject nativeSftpSessionStat(JNIEnv* env, jclass, jlong ptr, jstring path, jboolean follow_link) {
+ return reinterpret_cast<SftpSession*>(ptr)->Stat(
+ env, jni::StringToUTF8(env, jni::ParamRef<jstring>(env, path)), follow_link).release();
+}
+
+void nativeDirDestroy(JNIEnv*, jclass, jlong ptr) {
+ delete reinterpret_cast<Dir*>(ptr);
+}
+
+jni::GlobalRef<jclass> g_DirEntryClass(nullptr, nullptr);
+
+jobjectArray nativeDirList(JNIEnv* env, jclass, jlong ptr) {
+ return reinterpret_cast<Dir*>(ptr)->List(env, g_DirEntryClass).release();
+}
+
+void nativeFileDestroy(JNIEnv*, jclass, jlong ptr) {
+ delete reinterpret_cast<File*>(ptr);
+}
+
+jint nativeFileRead(JNIEnv* env, jclass, jlong ptr, jbyteArray array, jint offset, jint length) {
+ jboolean is_copy = JNI_FALSE;
+ bool critical = true;
+ auto* data = reinterpret_cast<jbyte*>(env->GetPrimitiveArrayCritical(array, &is_copy));
+ if (!data) {
+ critical = false;
+ data = env->GetByteArrayElements(array, &is_copy);
+ if (!data) return -1;
+ }
+ auto ret = reinterpret_cast<File *>(ptr)->Read(reinterpret_cast<uint8_t *>(data + offset), length);
+ if (critical) {
+ env->ReleasePrimitiveArrayCritical(array, data, JNI_COMMIT);
+ } else {
+ env->ReleaseByteArrayElements(array, data, JNI_COMMIT);
+ }
+ return ret;
+}
+
+void nativeFileSeek(JNIEnv*, jclass, jlong ptr, jlong offset) {
+ reinterpret_cast<File*>(ptr)->Seek(offset);
+}
+
+jint nativeFileWrite(JNIEnv* env, jclass, jlong ptr, jbyteArray array, jint offset, jint length) {
+ jboolean is_copy = JNI_FALSE;
+ bool critical = true;
+ auto* data = reinterpret_cast<jbyte*>(env->GetPrimitiveArrayCritical(array, &is_copy));
+ if (!data) {
+ critical = false;
+ data = env->GetByteArrayElements(array, &is_copy);
+ if (!data) return -1;
+ }
+ auto ret = reinterpret_cast<File *>(ptr)->Write(reinterpret_cast<uint8_t *>(data + offset), length);
+ if (critical) {
+ env->ReleasePrimitiveArrayCritical(array, data, JNI_ABORT);
+ } else {
+ env->ReleaseByteArrayElements(array, data, JNI_ABORT);
+ }
+ return ret;
+}
+
+jni::GlobalRef<jclass> g_NativeSftpClass(nullptr, nullptr);
+jmethodID g_CreateDirEntry;
+
+void RegisterSftp(JNIEnv* env) {
+ auto clazz = jni::FindClass(env, "org/the_jk/cleversync/io/sftp/NativeSftp");
+ ABORT_IF_NULL(env, clazz);
+ auto dir_entry_clazz = jni::FindClass(env, "org/the_jk/cleversync/io/sftp/NativeSftp$DirEntry");
+ ABORT_IF_NULL(env, dir_entry_clazz);
+ static const JNINativeMethod methods[] = {
+ { "nativeSshSessionNew", "()J", reinterpret_cast<void*>(&nativeSshSessionNew) },
+ { "nativeSshSessionDestroy", "(J)V", reinterpret_cast<void*>(&nativeSshSessionDestroy) },
+ { "nativeSshSessionGetLastError", "(J)Ljava/lang/String;", reinterpret_cast<void*>(&nativeSshSessionGetLastError) },
+ { "nativeSshSessionConnect", "(JLjava/lang/String;I)Z", reinterpret_cast<void*>(&nativeSshSessionConnect) },
+ { "nativeSshSessionHandshake", "(J)[B", reinterpret_cast<void*>(&nativeSshSessionHandshake) },
+ { "nativeSshSessionAuthenticate", "(JLjava/lang/String;Ljava/lang/String;[B[B)Z", reinterpret_cast<void*>(&nativeSshSessionAuthenticate) },
+ { "nativeSshSessionNewSftpSession", "(J)J", reinterpret_cast<void*>(&nativeSshSessionNewSftpSession) },
+
+ { "nativeSftpSessionDestroy", "(J)V", reinterpret_cast<void*>(&nativeSftpSessionDestroy) },
+ { "nativeSftpSessionGetLastError", "(J)Ljava/lang/String;", reinterpret_cast<void*>(&nativeSftpSessionGetLastError) },
+ { "nativeSftpSessionOpenDir", "(JLjava/lang/String;)J", reinterpret_cast<void*>(&nativeSftpSessionOpenDir) },
+ { "nativeSftpSessionMakeDir", "(JLjava/lang/String;I)Z", reinterpret_cast<void*>(&nativeSftpSessionMakeDir) },
+ { "nativeSftpSessionRemoveDir", "(JLjava/lang/String;)Z", reinterpret_cast<void*>(&nativeSftpSessionRemoveDir) },
+ { "nativeSftpSessionOpenFile", "(JLjava/lang/String;I)J", reinterpret_cast<void*>(&nativeSftpSessionOpenFile) },
+ { "nativeSftpSessionUnlink", "(JLjava/lang/String;)Z", reinterpret_cast<void*>(&nativeSftpSessionUnlink) },
+ { "nativeSftpSessionSymlink", "(JLjava/lang/String;Ljava/lang/String;)Z", reinterpret_cast<void*>(&nativeSftpSessionSymlink) },
+ { "nativeSftpSessionReadlink", "(JLjava/lang/String;)Ljava/lang/String;", reinterpret_cast<void*>(&nativeSftpSessionReadlink) },
+ { "nativeSftpSessionStat", "(JLjava/lang/String;Z)Lorg/the_jk/cleversync/io/sftp/NativeSftp$DirEntry;", reinterpret_cast<void*>(&nativeSftpSessionStat) },
+
+ { "nativeDirDestroy", "(J)V", reinterpret_cast<void*>(&nativeDirDestroy) },
+ { "nativeDirList", "(J)[Lorg/the_jk/cleversync/io/sftp/NativeSftp$DirEntry;", reinterpret_cast<void*>(&nativeDirList) },
+
+ { "nativeFileDestroy", "(J)V", reinterpret_cast<void*>(&nativeFileDestroy) },
+ { "nativeFileRead", "(J[BII)I", reinterpret_cast<void*>(&nativeFileRead) },
+ { "nativeFileSeek", "(JJ)V", reinterpret_cast<void*>(&nativeFileSeek) },
+ { "nativeFileWrite", "(J[BII)I", reinterpret_cast<void*>(&nativeFileWrite) },
+
+ };
+ auto ret = env->RegisterNatives(clazz.get(), methods, sizeof(methods) / sizeof(methods[0]));
+ ABORT_IF_NOT_OK(ret);
+
+ g_CreateDirEntry = env->GetStaticMethodID(
+ clazz.get(), "createDirEntry",
+ "(Ljava/lang/String;IJJ)Lorg/the_jk/cleversync/io/sftp/NativeSftp$DirEntry;");
+ ABORT_IF_NULL(env, g_CreateDirEntry);
+ g_NativeSftpClass = clazz;
+ g_DirEntryClass = dir_entry_clazz;
+}
+
+void UnregisterSftp() {
+ g_CreateDirEntry = nullptr;
+ g_NativeSftpClass.reset();
+ g_DirEntryClass.reset();
+}
+
+jni::LocalRef<jobject> CreateDirEntry(JNIEnv* env, const char* name,
+ const LIBSSH2_SFTP_ATTRIBUTES& attrs) {
+ auto j_name = jni::UTF8ToString(env, name);
+ jlong size;
+ jlong last_modified;
+ jint type;
+
+ if (attrs.flags & LIBSSH2_SFTP_ATTR_SIZE) {
+ // Kotlin size casts Long to ULong
+ size = static_cast<jlong>(attrs.filesize);
+ } else {
+ size = 0L;
+ }
+
+ if (attrs.flags & LIBSSH2_SFTP_ATTR_ACMODTIME) {
+ // Kotlin size casts Long to ULong
+ last_modified = static_cast<jlong>(attrs.mtime);
+ } else {
+ last_modified = 0L;
+ }
+
+ if (attrs.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) {
+ switch (attrs.permissions & LIBSSH2_SFTP_S_IFMT) {
+ case LIBSSH2_SFTP_S_IFREG:
+ type = 1;
+ break;
+ case LIBSSH2_SFTP_S_IFDIR:
+ type = 0;
+ break;
+ case LIBSSH2_SFTP_S_IFLNK:
+ type = 2;
+ break;
+ case LIBSSH2_SFTP_S_IFIFO:
+ case LIBSSH2_SFTP_S_IFCHR:
+ case LIBSSH2_SFTP_S_IFBLK:
+ case LIBSSH2_SFTP_S_IFSOCK:
+ default:
+ // Skip unknown or "weird" types
+ return nullptr;
+ }
+ } else {
+ // Skip unknown entries
+ return nullptr;
+ }
+
+ return jni::CallStaticObjectMethod<jobject>(env, g_NativeSftpClass, g_CreateDirEntry,
+ j_name.get(), type, size, last_modified);
+}
+
+} // namespace
+
+jint JNI_OnLoad(JavaVM *vm, void *) {
+ auto* env = jni::OnLoad(vm);
+
+ // TODO: Check return
+ libssh2_init(0);
+
+ RegisterSftp(env);
+
+ return jni::JNI_VERSION;
+}
+
+void JNI_OnUnload(JavaVM *, void *) {
+ // Not called on Android (or in general), but if it where, this would be the place to unregister.
+ UnregisterSftp();
+
+ libssh2_exit();
+}