#include #include #include #include #include #include #include #include #include #include #include "jni.hpp" #include "libsmb2.h" namespace { jni::LocalRef CreateDirEntry(JNIEnv* env, const std::string& name, const smb2_stat_64& stat); class Dir { public: Dir(std::shared_ptr context, smb2dir* dir) : context_(std::move(context)), dir_(dir) { assert(context_ && dir_); } ~Dir() { smb2_closedir(context_.get(), dir_); } Dir(const Dir&) = delete; Dir& operator=(const Dir&) = delete; jni::LocalRef List(JNIEnv* env, const jni::Ref& dir_entry_clazz) { smb2_rewinddir(context_.get(), dir_); std::vector> tmp; while (auto* ent = smb2_readdir(context_.get(), dir_)) { // Skip . and .. entries. if (ent->name[0] == '.' && (ent->name[1] == '\0' || (ent->name[1] == '.' && ent->name[2] == '\0'))) continue; auto obj = CreateDirEntry(env, ent->name, ent->st); if (obj) { tmp.push_back(std::move(obj)); } } return jni::CreateArray(env, dir_entry_clazz, std::move(tmp)); } private: std::shared_ptr context_; smb2dir* const dir_; }; class File { public: File(std::shared_ptr context, smb2fh* fh) : context_(std::move(context)), fh_(fh) { assert(context_ && fh_); } ~File() { smb2_close(context_.get(), fh_); } File(const File&) = delete; File& operator=(const File&) = delete; int32_t read(uint8_t* data, int32_t size) { if (size <= 0) return 0; const uint32_t max = std::min(smb2_get_max_read_size(context_.get()), static_cast(std::numeric_limits::max())); int32_t total = 0; while (size > max) { int ret = smb2_read(context_.get(), fh_, data, max); if (ret < 0) return total ? total : ret; total += ret; if (ret != max) return total; data += ret; size -= ret; } int ret = smb2_read(context_.get(), fh_, data, size); if (ret < 0) return total ? total : ret; return total + ret; } int32_t write(const uint8_t* data, int32_t size) { if (size <= 0) return 0; const uint32_t max = std::min(smb2_get_max_write_size(context_.get()), static_cast(std::numeric_limits::max())); int32_t total = 0; while (size > max) { int ret = smb2_write(context_.get(), fh_, data, max); if (ret < 0) return total ? total : ret; total += ret; if (ret != max) return total; data += ret; size -= ret; } int ret = smb2_write(context_.get(), fh_, data, size); if (ret < 0) return total ? total : ret; return total + ret; } int64_t seek(int64_t offset, int32_t native_whence) { int whence; switch (native_whence) { case 0: whence = SEEK_SET; break; case 1: whence = SEEK_CUR; break; default: assert(false); return -1; } return smb2_lseek(context_.get(), fh_, offset, whence, nullptr); } private: std::shared_ptr context_; smb2fh* const fh_; }; class Url { public: explicit Url(smb2_url* url) : url_(url) { assert(url_); } ~Url() { smb2_destroy_url(url_); } Url(const Url&) = delete; Url& operator=(const Url&) = delete; [[nodiscard]] const char* path() const { return url_->path; } [[nodiscard]] const char* server() const { return url_->server; } [[nodiscard]] const char* share() const { return url_->share; } [[nodiscard]] const char* user() const { return url_->user; } private: smb2_url* url_; }; class Context { public: explicit Context(int timeout) : context_(smb2_init_context(), ContextDeleter{}) { smb2_set_timeout(context_.get(), timeout); } ~Context() = default; Context(const Context&) = delete; Context& operator=(const Context&) = delete; [[nodiscard]] std::unique_ptr ParseUrl(const std::string& url) { auto* ptr = smb2_parse_url(context_.get(), url.c_str()); return ptr ? std::make_unique(ptr): nullptr; } bool Connect(const Url& url, const std::optional& username, const std::optional& password) { if (password.has_value()) smb2_set_password(context_.get(), password->c_str()); auto* user = username.has_value() ? username->c_str() : url.user(); return smb2_connect_share(context_.get(), url.server(), url.share(), user) == 0; } [[nodiscard]] std::string_view GetError() { return smb2_get_error(context_.get()); } [[nodiscard]] std::unique_ptr OpenDir(const std::string& path) { auto* ptr = smb2_opendir(context_.get(), path.c_str()); return ptr ? std::make_unique(context_, ptr) : nullptr; } [[nodiscard]] jni::LocalRef Entry(JNIEnv* env, const std::string& path) { struct smb2_stat_64 stat; // NOLINT(*-pro-type-member-init) auto ret = smb2_stat(context_.get(), path.c_str(), &stat); if (ret) return {env, nullptr}; auto slash = path.find_last_of('/'); return CreateDirEntry(env, slash == std::string::npos ? path : path.substr(slash + 1), stat); } bool MakeDir(const std::string& path) { return smb2_mkdir(context_.get(), path.c_str()) == 0; } bool RemoveDir(const std::string& path) { return smb2_rmdir(context_.get(), path.c_str()) == 0; } bool Unlink(const std::string& path) { return smb2_unlink(context_.get(), path.c_str()) == 0; } std::optional ReadLink(const std::string& path) { // Good to start with a fairly small size as current implementation // of smb2_readlink uses strncpy, which pads the whole unused buffer // with zeros. uint32_t bufsize = 256; std::vector buf; while (true) { buf.resize(bufsize); auto ret = smb2_readlink(context_.get(), path.c_str(), buf.data(), bufsize); if (ret != 0) return std::nullopt; // smb2_readlink uses strncpy, so if actual path was larger than bufsize // there will be no terminating zero. auto it = std::find(buf.begin(), buf.end(), '\0'); if (it != buf.end()) return std::string(buf.begin(), it); const auto previous = bufsize; bufsize *= 2; // Check for bufsize (a uint32_t) overflow. if (bufsize <= previous) return std::nullopt; } } [[nodiscard]] std::unique_ptr OpenFile(const std::string& path, int32_t mode) { int flags; switch (mode) { case 0: flags = O_RDONLY; break; case 1: flags = O_WRONLY | O_CREAT | O_TRUNC; break; default: assert(false); return nullptr; } auto* ptr = smb2_open(context_.get(), path.c_str(), flags); return ptr ? std::make_unique(context_, ptr) : nullptr; } private: struct ContextDeleter { void operator()(smb2_context* context) { smb2_destroy_context(context); } }; std::shared_ptr context_; }; jlong nativeContextNew(JNIEnv* env, jclass clazz, jint timeout) { return reinterpret_cast(new Context(static_cast(timeout))); } void nativeContextDestroy(JNIEnv* env, jclass clazz, jlong ptr) { delete reinterpret_cast(ptr); } jlong nativeContextParseUrl(JNIEnv* env, jclass clazz, jlong ptr, jstring url) { return reinterpret_cast(reinterpret_cast(ptr)->ParseUrl(jni::StringToUTF8(env, jni::ParamRef(env, url))).release()); } jboolean nativeContextConnect(JNIEnv* env, jclass clazz, jlong context_ptr, jlong url_ptr, jstring j_username, jstring j_password) { auto* url = reinterpret_cast(url_ptr); if (!url) return JNI_FALSE; std::optional username; std::optional password; if (j_username) username = jni::StringToUTF8(env, jni::ParamRef(env, j_username)); if (j_password) password = jni::StringToUTF8(env, jni::ParamRef(env, j_password)); return reinterpret_cast(context_ptr)->Connect(*url, username, password) ? JNI_TRUE : JNI_FALSE; } jstring nativeContextGetError(JNIEnv* env, jclass clazz, jlong ptr) { return jni::UTF8ToString(env, std::string(reinterpret_cast(ptr)->GetError())).release(); } jlong nativeContextOpenDir(JNIEnv* env, jclass clazz, jlong ptr, jstring path) { return reinterpret_cast(reinterpret_cast(ptr)->OpenDir(jni::StringToUTF8(env, jni::ParamRef(env, path))).release()); } jobject nativeContextEntry(JNIEnv* env, jclass clazz, jlong ptr, jstring path) { return reinterpret_cast(ptr)->Entry(env, jni::StringToUTF8(env, jni::ParamRef(env, path))).release(); } jboolean nativeContextMakeDir(JNIEnv* env, jclass clazz, jlong ptr, jstring path) { return reinterpret_cast(ptr)->MakeDir(jni::StringToUTF8(env, jni::ParamRef(env, path))) ? JNI_TRUE : JNI_FALSE; } jboolean nativeContextRemoveDir(JNIEnv* env, jclass clazz, jlong ptr, jstring path) { return reinterpret_cast(ptr)->RemoveDir(jni::StringToUTF8(env, jni::ParamRef(env, path))) ? JNI_TRUE : JNI_FALSE; } jboolean nativeContextUnlink(JNIEnv* env, jclass clazz, jlong ptr, jstring path) { return reinterpret_cast(ptr)->Unlink(jni::StringToUTF8(env, jni::ParamRef(env, path))) ? JNI_TRUE : JNI_FALSE; } jstring nativeContextReadLink(JNIEnv* env, jclass clazz, jlong ptr, jstring path) { auto ret = reinterpret_cast(ptr)->ReadLink(jni::StringToUTF8(env, jni::ParamRef(env, path))); if (ret.has_value()) return jni::UTF8ToString(env, ret.value()).release(); return nullptr; } jlong nativeContextOpenFile(JNIEnv* env, jclass clazz, jlong ptr, jstring path, jint mode) { return reinterpret_cast(reinterpret_cast(ptr)->OpenFile(jni::StringToUTF8(env, jni::ParamRef(env, path)), mode).release()); } void nativeUrlDestroy(JNIEnv* env, jclass clazz, jlong ptr) { delete reinterpret_cast(ptr); } jstring nativeUrlPath(JNIEnv* env, jclass clazz, jlong ptr) { auto* path = reinterpret_cast(ptr)->path(); return jni::UTF8ToString(env, std::string(path ? path : "")).release(); } void nativeDirDestroy(JNIEnv* env, jclass clazz, jlong ptr) { delete reinterpret_cast(ptr); } jni::GlobalRef g_DirEntryClass(nullptr, nullptr); jobjectArray nativeDirList(JNIEnv* env, jclass clazz, jlong ptr) { return reinterpret_cast(ptr)->List(env, g_DirEntryClass).release(); } void nativeFileDestroy(JNIEnv* env, jclass clazz, jlong ptr) { delete reinterpret_cast(ptr); } jint nativeFileRead(JNIEnv* env, jclass clazz, jlong ptr, jbyteArray array, jint offset, jint length) { jboolean is_copy = JNI_FALSE; bool critical = true; auto* data = reinterpret_cast(env->GetPrimitiveArrayCritical(array, &is_copy)); if (!data) { critical = false; data = env->GetByteArrayElements(array, &is_copy); if (!data) return -1; } auto ret = reinterpret_cast(ptr)->read(reinterpret_cast(data + offset), length); if (critical) { env->ReleasePrimitiveArrayCritical(array, data, JNI_COMMIT); } else { env->ReleaseByteArrayElements(array, data, JNI_COMMIT); } return ret; } jint nativeFileWrite(JNIEnv* env, jclass clazz, jlong ptr, jbyteArray array, jint offset, jint length) { jboolean is_copy = JNI_FALSE; bool critical = true; auto* data = reinterpret_cast(env->GetPrimitiveArrayCritical(array, &is_copy)); if (!data) { critical = false; data = env->GetByteArrayElements(array, &is_copy); if (!data) return -1; } auto ret = reinterpret_cast(ptr)->write(reinterpret_cast(data + offset), length); if (critical) { env->ReleasePrimitiveArrayCritical(array, data, JNI_ABORT); } else { env->ReleaseByteArrayElements(array, data, JNI_ABORT); } return ret; } jlong nativeFileSeek(JNIEnv* env, jclass clazz, jlong ptr, jlong offset, jint whence) { return reinterpret_cast(ptr)->seek(offset, whence); } jni::GlobalRef g_NativeSambaClass(nullptr, nullptr); jmethodID g_CreateDirEntry; void RegisterSamba(JNIEnv* env) { auto clazz = jni::FindClass(env, "org/the_jk/cleversync/io/samba/NativeSamba"); ABORT_IF_NULL(env, clazz); auto dir_entry_clazz = jni::FindClass(env, "org/the_jk/cleversync/io/samba/NativeSamba$DirEntry"); ABORT_IF_NULL(env, dir_entry_clazz); static const JNINativeMethod methods[] = { { "nativeContextNew", "(I)J", reinterpret_cast(&nativeContextNew) }, { "nativeContextDestroy", "(J)V", reinterpret_cast(&nativeContextDestroy) }, { "nativeContextParseUrl", "(JLjava/lang/String;)J", reinterpret_cast(&nativeContextParseUrl) }, { "nativeContextConnect", "(JJLjava/lang/String;Ljava/lang/String;)Z", reinterpret_cast(&nativeContextConnect) }, { "nativeContextGetError", "(J)Ljava/lang/String;", reinterpret_cast(&nativeContextGetError) }, { "nativeContextOpenDir", "(JLjava/lang/String;)J", reinterpret_cast(&nativeContextOpenDir) }, { "nativeContextEntry", "(JLjava/lang/String;)Lorg/the_jk/cleversync/io/samba/NativeSamba$DirEntry;", reinterpret_cast(&nativeContextEntry) }, { "nativeContextMakeDir", "(JLjava/lang/String;)Z", reinterpret_cast(&nativeContextMakeDir) }, { "nativeContextRemoveDir", "(JLjava/lang/String;)Z", reinterpret_cast(&nativeContextRemoveDir) }, { "nativeContextUnlink", "(JLjava/lang/String;)Z", reinterpret_cast(&nativeContextUnlink) }, { "nativeContextReadLink", "(JLjava/lang/String;)Ljava/lang/String;", reinterpret_cast(&nativeContextReadLink) }, { "nativeContextOpenFile", "(JLjava/lang/String;I)J", reinterpret_cast(&nativeContextOpenFile) }, { "nativeUrlDestroy", "(J)V", reinterpret_cast(&nativeUrlDestroy) }, { "nativeUrlPath", "(J)Ljava/lang/String;", reinterpret_cast(&nativeUrlPath) }, { "nativeDirDestroy", "(J)V", reinterpret_cast(&nativeDirDestroy) }, { "nativeDirList", "(J)[Lorg/the_jk/cleversync/io/samba/NativeSamba$DirEntry;", reinterpret_cast(&nativeDirList) }, { "nativeFileDestroy", "(J)V", reinterpret_cast(&nativeFileDestroy) }, { "nativeFileRead", "(J[BII)I", reinterpret_cast(&nativeFileRead) }, { "nativeFileSeek", "(JJI)J", reinterpret_cast(&nativeFileSeek) }, { "nativeFileWrite", "(J[BII)I", reinterpret_cast(&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/samba/NativeSamba$DirEntry;"); ABORT_IF_NULL(env, g_CreateDirEntry); g_NativeSambaClass = clazz; g_DirEntryClass = dir_entry_clazz; } void UnregisterSamba() { g_CreateDirEntry = 0; g_NativeSambaClass.reset(); g_DirEntryClass.reset(); } jni::LocalRef CreateDirEntry(JNIEnv* env, const std::string& name, const smb2_stat_64& stat) { auto j_name = jni::UTF8ToString(env, name); // Kotlin size casts Long to ULong auto size = static_cast(stat.smb2_size); auto last_modified = static_cast(stat.smb2_mtime); jint type; switch (stat.smb2_type) { case SMB2_TYPE_DIRECTORY: type = 0; break; case SMB2_TYPE_FILE: type = 1; break; case SMB2_TYPE_LINK: type = 2; break; default: return {env, nullptr}; } return jni::CallStaticObjectMethod(env, g_NativeSambaClass, g_CreateDirEntry, j_name.get(), type, size, last_modified); } } // namespace jint JNI_OnLoad(JavaVM *vm, void *reserved) { auto* env = jni::OnLoad(vm); RegisterSamba(env); return jni::JNI_VERSION; } void JNI_OnUnload(JavaVM *vm, void *reserved) { // Not called on Android (or in general), but if it where, this would be the place to unregister. UnregisterSamba(); }