Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- We fixed an issue that caused a FileNotFoundException during file deletion operations.

## [v0.1.3] - 2025-12-05

- We introduced AndroidNavigationBar module to get navigation bar height and active status.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Mendix native mobile package for React Native applications.

Before you begin, ensure you have the following installed:

- **Node.js**: Version 18 (specified in `.nvmrc`)
- **Node.js**: Version 24 (specified in `.nvmrc`)
- **Yarn**: Package manager (Yarn workspaces are required)
- **React Native development environment** (optional, only needed if running the example app): Follow the [React Native environment setup guide](https://reactnative.dev/docs/environment-setup)
- For iOS: Xcode and CocoaPods
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ PODS:
- hermes-engine (0.77.3):
- hermes-engine/Pre-built (= 0.77.3)
- hermes-engine/Pre-built (0.77.3)
- MendixNative (0.1.2):
- MendixNative (0.1.3):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -1825,7 +1825,7 @@ SPEC CHECKSUMS:
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8
hermes-engine: b2187dbe13edb0db8fcb2a93a69c1987a30d98a4
MendixNative: 0405210432ee514e2d7906fe5714f719eb6d7c75
MendixNative: 36190d86a65cb57b351c6396bc1349a7823206b0
op-sqlite: 12554de3e1a0cb86cbad3cf1f0c50450f57d3855
OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
Expand Down
6 changes: 5 additions & 1 deletion ios/Modules/Helper/StorageHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public class StorageHelper {

public static func clearDataAt(url: URL, component: String) {
let path = url.appendingPathComponent(component).path
_ = NativeFsModule.remove(path, error: nil)
do {
try NativeFsModule.remove(path)
} catch {
NSLog("Failed to clear data at path: \(path), error: \(error.localizedDescription)")
}
}
}
200 changes: 62 additions & 138 deletions ios/Modules/NativeFsModule/NativeFsModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,87 +49,41 @@ public class NativeFsModule: NSObject {
}
}

static func readJson(_ filePath: String, error: NSErrorPointer) -> [String: Any]? {
static func readJson(_ filePath: String) throws -> [String: Any]? {
guard let data = readData(filePath) else {
return nil
}

do {
let result = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
return result as? [String: Any]
} catch let jsonError {
error?.pointee = jsonError as NSError
return nil
}
let result = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
return result as? [String: Any]
}

static func save(_ data: Data, filepath: String, error: NSErrorPointer) -> Bool {
static func save(_ data: Data, filepath: String) throws {
let directoryURL = URL(fileURLWithPath: (filepath as NSString).deletingLastPathComponent)

do {
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
} catch let directoryError {
error?.pointee = directoryError as NSError
return false
}

var options: Data.WritingOptions = .atomic
if encryptionEnabled {
options = [.atomic, .completeFileProtection]
}

do {
try data.write(to: URL(fileURLWithPath: filepath), options: options)
return true
} catch let writeError {
error?.pointee = writeError as NSError
return false
}
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
let options: Data.WritingOptions = encryptionEnabled ? [.atomic, .completeFileProtection] : .atomic
try data.write(to: URL(fileURLWithPath: filepath), options: options)
}

static func move(_ filepath: String, newPath: String, error: NSErrorPointer) -> Bool {
static func move(_ filepath: String, newPath: String) throws {
let fileManager = FileManager.default

guard fileManager.fileExists(atPath: filepath) else {
error?.pointee = NSError(domain: NativeFsErrorDomain, code: -1, userInfo: [NSLocalizedDescriptionKey: "File does not exist"])
return false
throw NSError(domain: NativeFsErrorDomain, code: -1, userInfo: [NSLocalizedDescriptionKey: "File does not exist"])
}

let directoryURL = URL(fileURLWithPath: (newPath as NSString).deletingLastPathComponent)

do {
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
} catch let directoryError {
error?.pointee = directoryError as NSError
return false
}

do {
try fileManager.moveItem(atPath: filepath, toPath: newPath)
return true
} catch let moveError {
error?.pointee = moveError as NSError
return false
}
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
try fileManager.moveItem(atPath: filepath, toPath: newPath)
}

static func remove(_ filepath: String, error: NSErrorPointer) -> Bool {
static func remove(_ filepath: String) throws {
let fileManager = FileManager.default

guard fileManager.fileExists(atPath: filepath) else {
return false
}

do {
try fileManager.removeItem(atPath: filepath)
return true
} catch let removeError {
error?.pointee = removeError as NSError
return false
NSLog("Trying to delete non-existing file: \(filepath)")
return
}
try fileManager.removeItem(atPath: filepath)
}

static func ensureWhiteListedPath(_ paths: [String], error: NSErrorPointer) -> Bool {
static func ensureWhiteListedPath(_ paths: [String]) throws {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first ?? ""
let cachesPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first ?? ""
let tempPath = (NSTemporaryDirectory() as NSString).standardizingPath
Expand All @@ -138,15 +92,13 @@ public class NativeFsModule: NSObject {
if !path.hasPrefix(documentsPath) &&
!path.hasPrefix(cachesPath) &&
!path.hasPrefix(tempPath) {
error?.pointee = NSError(
throw NSError(
domain: NativeFsErrorDomain,
code: 999,
userInfo: [NSLocalizedDescriptionKey: "The path \(path) does not point to the documents directory"]
)
return false
}
}
return true
}

static func list(_ dirPath: String) -> [String] {
Expand All @@ -169,34 +121,26 @@ public class NativeFsModule: NSObject {
reject: @escaping RCTPromiseRejectBlock
) {

var error: NSError?
if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) {
reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error)
return
}
guard isWhiteListedPath(filepath, reject: reject) else { return }

guard let data = readBlobRefAsData(blob) else {
reject(NativeFsModule.ERROR_READ_FAILED, NativeFsModule.formatError("Failed to read blob"), nil)
return
}

if !NativeFsModule.save(data, filepath: filepath, error: &error) {
do {
try NativeFsModule.save(data, filepath: filepath)
resolve(nil)
} catch {
reject(NativeFsModule.ERROR_SAVE_FAILED, NativeFsModule.formatError("Save failed"), error)
return
}

resolve(nil)
}

public func read(_ filepath: String,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {

var error: NSError?
if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) {
reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error)
return
}
guard isWhiteListedPath(filepath, reject: reject) else { return }

guard let data = NativeFsModule.readData(filepath) else {
resolve(nil)
Expand All @@ -216,45 +160,33 @@ public class NativeFsModule: NSObject {
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {

var error: NSError?
if !NativeFsModule.ensureWhiteListedPath([filepath, newPath], error: &error) {
reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error)
return
}
guard isWhiteListedPath(filepath, newPath, reject: reject) else { return }

if !NativeFsModule.move(filepath, newPath: newPath, error: &error) {
do {
try NativeFsModule.move(filepath, newPath: newPath)
resolve(nil)
} catch {
reject(NativeFsModule.ERROR_MOVE_FAILED, NativeFsModule.formatError("Failed to move file"), error)
return
}

resolve(nil)
}

public func remove(_ filepath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {

var error: NSError?
if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) {
reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error)
return
}
guard isWhiteListedPath(filepath, reject: reject) else { return }

if !NativeFsModule.remove(filepath, error: &error) {
do {
try NativeFsModule.remove(filepath)
resolve(nil)
} catch {
reject(NativeFsModule.ERROR_DELETE_FAILED, NativeFsModule.formatError("Failed to delete file"), error)
return
}

resolve(nil)
}

public func list(_ dirPath: String,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {

var error: NSError?
if !NativeFsModule.ensureWhiteListedPath([dirPath], error: &error) {
reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error)
return
}
guard isWhiteListedPath(dirPath, reject: reject) else { return }

resolve(NativeFsModule.list(dirPath))
}
Expand All @@ -263,11 +195,7 @@ public class NativeFsModule: NSObject {
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {

var error: NSError?
if !NativeFsModule.ensureWhiteListedPath([filePath], error: &error) {
reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error)
return
}
guard isWhiteListedPath(filePath, reject: reject) else { return }

guard let data = NativeFsModule.readData(filePath) else {
resolve(nil)
Expand All @@ -282,12 +210,7 @@ public class NativeFsModule: NSObject {
public func fileExists(_ filepath: String,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {

var error: NSError?
if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) {
reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error)
return
}
guard isWhiteListedPath(filepath, reject: reject) else { return }

let exists = FileManager.default.fileExists(atPath: filepath)
resolve(NSNumber(value: exists))
Expand All @@ -297,46 +220,36 @@ public class NativeFsModule: NSObject {
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {

var error: NSError?
if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) {
reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error)
return
}
guard isWhiteListedPath(filepath, reject: reject) else { return }

guard let data = NativeFsModule.readJson(filepath, error: &error) else {
if let error = error {
reject(NativeFsModule.ERROR_SERIALIZATION_FAILED, NativeFsModule.formatError("Failed to deserialize JSON"), error)
} else {
resolve(nil)
}
return
do {
let data = try NativeFsModule.readJson(filepath)
resolve(data)
} catch {
reject(NativeFsModule.ERROR_SERIALIZATION_FAILED, NativeFsModule.formatError("Failed to deserialize JSON"), error)
}

resolve(data)
}

public func writeJson(_ data: [String: Any],
filepath: String,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {

var error: NSError?
if !NativeFsModule.ensureWhiteListedPath([filepath], error: &error) {
reject(NativeFsModule.INVALID_PATH, "Path not accessible", error)
guard isWhiteListedPath(filepath, reject: reject) else { return }

var jsonData: Data
do {
jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
} catch {
reject(NativeFsModule.ERROR_SERIALIZATION_FAILED, NativeFsModule.formatError("Failed to serialize JSON"), error)
return
}

do {
let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)

if !NativeFsModule.save(jsonData, filepath: filepath, error: &error) {
reject(NativeFsModule.ERROR_SAVE_FAILED, NativeFsModule.formatError("Failed to write JSON"), error)
return
}

try NativeFsModule.save(jsonData, filepath: filepath)
resolve(nil)
} catch {
reject(NativeFsModule.ERROR_SERIALIZATION_FAILED, NativeFsModule.formatError("Failed to serialize JSON"), error)
reject(NativeFsModule.ERROR_SAVE_FAILED, NativeFsModule.formatError("Failed to write JSON"), error)
}
}

Expand All @@ -345,4 +258,15 @@ public class NativeFsModule: NSObject {
"SUPPORTS_DIRECTORY_MOVE": true,
"SUPPORTS_ENCRYPTION": true
]

private func isWhiteListedPath(_ paths: String..., reject: RCTPromiseRejectBlock) -> Bool {
do {
try NativeFsModule.ensureWhiteListedPath(paths)
return true
} catch let error {
reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error)
return false
}
}
}

6 changes: 3 additions & 3 deletions ios/Modules/NativeOtaModule/OtaHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ class OtaHelpers: NSObject {
}

static func getNativeDependencies() -> [String: Any] {
guard let path = Bundle.main.path(forResource: "native_dependencies", ofType: "json") else {
guard let path = Bundle.main.path(forResource: "native_dependencies", ofType: "json"),
let data = try? NativeFsModule.readJson(path) else {
return [:]
}

return NativeFsModule.readJson(path, error: nil) ?? [:]
return data
}
}