Navigate password folders on iPhone
This commit is contained in:
@@ -1523,6 +1523,138 @@ public func FfiConverterTypeMobilePage_lower(_ value: MobilePage) -> RustBuffer
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobilePasswordPage: Equatable, Hashable {
|
||||||
|
public var id: String
|
||||||
|
public var path: String
|
||||||
|
public var title: String
|
||||||
|
public var rows: [MobilePasswordRow]
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(id: String, path: String, title: String, rows: [MobilePasswordRow]) {
|
||||||
|
self.id = id
|
||||||
|
self.path = path
|
||||||
|
self.title = title
|
||||||
|
self.rows = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobilePasswordPage: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobilePasswordPage: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordPage {
|
||||||
|
return
|
||||||
|
try MobilePasswordPage(
|
||||||
|
id: FfiConverterString.read(from: &buf),
|
||||||
|
path: FfiConverterString.read(from: &buf),
|
||||||
|
title: FfiConverterString.read(from: &buf),
|
||||||
|
rows: FfiConverterSequenceTypeMobilePasswordRow.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobilePasswordPage, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.id, into: &buf)
|
||||||
|
FfiConverterString.write(value.path, into: &buf)
|
||||||
|
FfiConverterString.write(value.title, into: &buf)
|
||||||
|
FfiConverterSequenceTypeMobilePasswordRow.write(value.rows, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePasswordPage_lift(_ buf: RustBuffer) throws -> MobilePasswordPage {
|
||||||
|
return try FfiConverterTypeMobilePasswordPage.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePasswordPage_lower(_ value: MobilePasswordPage) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobilePasswordPage.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobilePasswordRow: Equatable, Hashable {
|
||||||
|
public var id: String
|
||||||
|
public var path: String
|
||||||
|
public var title: String
|
||||||
|
public var detail: String
|
||||||
|
public var systemImage: String
|
||||||
|
public var kind: MobilePasswordRowKind
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(id: String, path: String, title: String, detail: String, systemImage: String, kind: MobilePasswordRowKind) {
|
||||||
|
self.id = id
|
||||||
|
self.path = path
|
||||||
|
self.title = title
|
||||||
|
self.detail = detail
|
||||||
|
self.systemImage = systemImage
|
||||||
|
self.kind = kind
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobilePasswordRow: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobilePasswordRow: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordRow {
|
||||||
|
return
|
||||||
|
try MobilePasswordRow(
|
||||||
|
id: FfiConverterString.read(from: &buf),
|
||||||
|
path: FfiConverterString.read(from: &buf),
|
||||||
|
title: FfiConverterString.read(from: &buf),
|
||||||
|
detail: FfiConverterString.read(from: &buf),
|
||||||
|
systemImage: FfiConverterString.read(from: &buf),
|
||||||
|
kind: FfiConverterTypeMobilePasswordRowKind.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobilePasswordRow, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.id, into: &buf)
|
||||||
|
FfiConverterString.write(value.path, into: &buf)
|
||||||
|
FfiConverterString.write(value.title, into: &buf)
|
||||||
|
FfiConverterString.write(value.detail, into: &buf)
|
||||||
|
FfiConverterString.write(value.systemImage, into: &buf)
|
||||||
|
FfiConverterTypeMobilePasswordRowKind.write(value.kind, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePasswordRow_lift(_ buf: RustBuffer) throws -> MobilePasswordRow {
|
||||||
|
return try FfiConverterTypeMobilePasswordRow.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePasswordRow_lower(_ value: MobilePasswordRow) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobilePasswordRow.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct MobileShell: Equatable, Hashable {
|
public struct MobileShell: Equatable, Hashable {
|
||||||
public var selectedTab: MobileTab
|
public var selectedTab: MobileTab
|
||||||
public var pages: [MobilePage]
|
public var pages: [MobilePage]
|
||||||
@@ -2372,6 +2504,238 @@ public func FfiConverterTypeMobileOnboardingPhase_lower(_ value: MobileOnboardin
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public enum MobilePasswordErrorKind: Equatable, Hashable {
|
||||||
|
|
||||||
|
case missingConfiguration
|
||||||
|
case configuration
|
||||||
|
case invalidPath
|
||||||
|
case directoryMissing
|
||||||
|
case repository
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobilePasswordErrorKind: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobilePasswordErrorKind: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = MobilePasswordErrorKind
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordErrorKind {
|
||||||
|
let variant: Int32 = try readInt(&buf)
|
||||||
|
switch variant {
|
||||||
|
|
||||||
|
case 1: return .missingConfiguration
|
||||||
|
|
||||||
|
case 2: return .configuration
|
||||||
|
|
||||||
|
case 3: return .invalidPath
|
||||||
|
|
||||||
|
case 4: return .directoryMissing
|
||||||
|
|
||||||
|
case 5: return .repository
|
||||||
|
|
||||||
|
default: throw UniffiInternalError.unexpectedEnumCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobilePasswordErrorKind, into buf: inout [UInt8]) {
|
||||||
|
switch value {
|
||||||
|
|
||||||
|
|
||||||
|
case .missingConfiguration:
|
||||||
|
writeInt(&buf, Int32(1))
|
||||||
|
|
||||||
|
|
||||||
|
case .configuration:
|
||||||
|
writeInt(&buf, Int32(2))
|
||||||
|
|
||||||
|
|
||||||
|
case .invalidPath:
|
||||||
|
writeInt(&buf, Int32(3))
|
||||||
|
|
||||||
|
|
||||||
|
case .directoryMissing:
|
||||||
|
writeInt(&buf, Int32(4))
|
||||||
|
|
||||||
|
|
||||||
|
case .repository:
|
||||||
|
writeInt(&buf, Int32(5))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePasswordErrorKind_lift(_ buf: RustBuffer) throws -> MobilePasswordErrorKind {
|
||||||
|
return try FfiConverterTypeMobilePasswordErrorKind.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePasswordErrorKind_lower(_ value: MobilePasswordErrorKind) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobilePasswordErrorKind.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public
|
||||||
|
enum MobilePasswordFfiError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
case Failed(kind: MobilePasswordErrorKind, title: String, detail: String
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
String(reflecting: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobilePasswordFfiError: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobilePasswordFfiError: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = MobilePasswordFfiError
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordFfiError {
|
||||||
|
let variant: Int32 = try readInt(&buf)
|
||||||
|
switch variant {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
case 1: return .Failed(
|
||||||
|
kind: try FfiConverterTypeMobilePasswordErrorKind.read(from: &buf),
|
||||||
|
title: try FfiConverterString.read(from: &buf),
|
||||||
|
detail: try FfiConverterString.read(from: &buf)
|
||||||
|
)
|
||||||
|
|
||||||
|
default: throw UniffiInternalError.unexpectedEnumCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobilePasswordFfiError, into buf: inout [UInt8]) {
|
||||||
|
switch value {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
case let .Failed(kind,title,detail):
|
||||||
|
writeInt(&buf, Int32(1))
|
||||||
|
FfiConverterTypeMobilePasswordErrorKind.write(kind, into: &buf)
|
||||||
|
FfiConverterString.write(title, into: &buf)
|
||||||
|
FfiConverterString.write(detail, into: &buf)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePasswordFfiError_lift(_ buf: RustBuffer) throws -> MobilePasswordFfiError {
|
||||||
|
return try FfiConverterTypeMobilePasswordFfiError.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePasswordFfiError_lower(_ value: MobilePasswordFfiError) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobilePasswordFfiError.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public enum MobilePasswordRowKind: Equatable, Hashable {
|
||||||
|
|
||||||
|
case directory
|
||||||
|
case entry
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobilePasswordRowKind: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobilePasswordRowKind: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = MobilePasswordRowKind
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePasswordRowKind {
|
||||||
|
let variant: Int32 = try readInt(&buf)
|
||||||
|
switch variant {
|
||||||
|
|
||||||
|
case 1: return .directory
|
||||||
|
|
||||||
|
case 2: return .entry
|
||||||
|
|
||||||
|
default: throw UniffiInternalError.unexpectedEnumCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobilePasswordRowKind, into buf: inout [UInt8]) {
|
||||||
|
switch value {
|
||||||
|
|
||||||
|
|
||||||
|
case .directory:
|
||||||
|
writeInt(&buf, Int32(1))
|
||||||
|
|
||||||
|
|
||||||
|
case .entry:
|
||||||
|
writeInt(&buf, Int32(2))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePasswordRowKind_lift(_ buf: RustBuffer) throws -> MobilePasswordRowKind {
|
||||||
|
return try FfiConverterTypeMobilePasswordRowKind.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePasswordRowKind_lower(_ value: MobilePasswordRowKind) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobilePasswordRowKind.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public
|
public
|
||||||
enum MobilePreferenceError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
|
enum MobilePreferenceError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
|
||||||
|
|
||||||
@@ -2637,6 +3001,30 @@ fileprivate struct FfiConverterOptionInt64: FfiConverterRustBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = String?
|
||||||
|
|
||||||
|
public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
|
||||||
|
guard let value = value else {
|
||||||
|
writeInt(&buf, Int8(0))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeInt(&buf, Int8(1))
|
||||||
|
FfiConverterString.write(value, into: &buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
|
||||||
|
switch try readInt(&buf) as Int8 {
|
||||||
|
case 0: return nil
|
||||||
|
case 1: return try FfiConverterString.read(from: &buf)
|
||||||
|
default: throw UniffiInternalError.unexpectedOptionalTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if swift(>=5.8)
|
#if swift(>=5.8)
|
||||||
@_documentation(visibility: private)
|
@_documentation(visibility: private)
|
||||||
#endif
|
#endif
|
||||||
@@ -2785,6 +3173,31 @@ fileprivate struct FfiConverterSequenceTypeMobilePage: FfiConverterRustBuffer {
|
|||||||
return seq
|
return seq
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
fileprivate struct FfiConverterSequenceTypeMobilePasswordRow: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = [MobilePasswordRow]
|
||||||
|
|
||||||
|
public static func write(_ value: [MobilePasswordRow], into buf: inout [UInt8]) {
|
||||||
|
let len = Int32(value.count)
|
||||||
|
writeInt(&buf, len)
|
||||||
|
for item in value {
|
||||||
|
FfiConverterTypeMobilePasswordRow.write(item, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [MobilePasswordRow] {
|
||||||
|
let len: Int32 = try readInt(&buf)
|
||||||
|
var seq = [MobilePasswordRow]()
|
||||||
|
seq.reserveCapacity(Int(len))
|
||||||
|
for _ in 0 ..< len {
|
||||||
|
seq.append(try FfiConverterTypeMobilePasswordRow.read(from: &buf))
|
||||||
|
}
|
||||||
|
return seq
|
||||||
|
}
|
||||||
|
}
|
||||||
public func mobileHomeOperation() -> MobileHomeOperation {
|
public func mobileHomeOperation() -> MobileHomeOperation {
|
||||||
return try! FfiConverterTypeMobileHomeOperation_lift(try! rustCall() {
|
return try! FfiConverterTypeMobileHomeOperation_lift(try! rustCall() {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -2803,6 +3216,14 @@ public func mobileOnboardingOperation(serverUrl: String, account: String, reposi
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
public func mobilePasswordPage(path: String?)throws -> MobilePasswordPage {
|
||||||
|
return try FfiConverterTypeMobilePasswordPage_lift(try rustCallWithError(FfiConverterTypeMobilePasswordFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_func_mobile_password_page(
|
||||||
|
FfiConverterOptionString.lower(path),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
public func mobileShell() -> MobileShell {
|
public func mobileShell() -> MobileShell {
|
||||||
return try! FfiConverterTypeMobileShell_lift(try! rustCall() {
|
return try! FfiConverterTypeMobileShell_lift(try! rustCall() {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -2862,6 +3283,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation() != 8354) {
|
if (uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation() != 8354) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_func_mobile_password_page() != 64312) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_func_mobile_shell() != 42687) {
|
if (uniffi_ironstorage_apple_checksum_func_mobile_shell() != 42687) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,6 +324,11 @@ uint64_t uniffi_ironstorage_apple_fn_func_mobile_home_operation(RustCallStatus *
|
|||||||
uint64_t uniffi_ironstorage_apple_fn_func_mobile_onboarding_operation(RustBuffer server_url, RustBuffer account, RustBuffer repository_path, RustBuffer application_token, RustCallStatus *_Nonnull out_status
|
uint64_t uniffi_ironstorage_apple_fn_func_mobile_onboarding_operation(RustBuffer server_url, RustBuffer account, RustBuffer repository_path, RustBuffer application_token, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_PASSWORD_PAGE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_PASSWORD_PAGE
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_func_mobile_password_page(RustBuffer path, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_SHELL
|
||||||
RustBuffer uniffi_ironstorage_apple_fn_func_mobile_shell(RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_func_mobile_shell(RustCallStatus *_Nonnull out_status
|
||||||
@@ -621,6 +626,12 @@ uint16_t uniffi_ironstorage_apple_checksum_func_mobile_home_operation(void
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_ONBOARDING_OPERATION
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_ONBOARDING_OPERATION
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation(void
|
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_PASSWORD_PAGE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_PASSWORD_PAGE
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_password_page(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_SHELL
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_SHELL
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private protocol MobileTabRoot: AnyObject {
|
||||||
|
var shellTab: MobileTab { get }
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private final class AppContext: NSObject, UITabBarControllerDelegate {
|
private final class AppContext: NSObject, UITabBarControllerDelegate {
|
||||||
private let tabs = UITabBarController()
|
private let tabs = UITabBarController()
|
||||||
@@ -34,7 +39,9 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
|||||||
func makeRootController() -> UIViewController {
|
func makeRootController() -> UIViewController {
|
||||||
let shell = mobileShellFixture(state: .loading)
|
let shell = mobileShellFixture(state: .loading)
|
||||||
navigationControllers = shell.pages.map { page in
|
navigationControllers = shell.pages.map { page in
|
||||||
let root = ShellViewController(page: page)
|
let root: UIViewController = page.tab == .passwords
|
||||||
|
? PasswordDirectoryViewController(shellPage: page)
|
||||||
|
: ShellViewController(page: page)
|
||||||
let navigation = UINavigationController(rootViewController: root)
|
let navigation = UINavigationController(rootViewController: root)
|
||||||
navigation.navigationBar.prefersLargeTitles = true
|
navigation.navigationBar.prefersLargeTitles = true
|
||||||
navigation.tabBarItem = UITabBarItem(
|
navigation.tabBarItem = UITabBarItem(
|
||||||
@@ -56,7 +63,7 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
|||||||
) {
|
) {
|
||||||
guard
|
guard
|
||||||
let navigation = viewController as? UINavigationController,
|
let navigation = viewController as? UINavigationController,
|
||||||
let root = navigation.viewControllers.first as? ShellViewController
|
let root = navigation.viewControllers.first as? MobileTabRoot
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -86,7 +93,7 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private final class ShellViewController: UITableViewController {
|
private final class ShellViewController: UITableViewController, MobileTabRoot {
|
||||||
fileprivate let shellTab: MobileTab
|
fileprivate let shellTab: MobileTab
|
||||||
private var page: MobilePage
|
private var page: MobilePage
|
||||||
private var loadTask: Task<Void, Never>?
|
private var loadTask: Task<Void, Never>?
|
||||||
@@ -607,6 +614,289 @@ private final class ShellViewController: UITableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class PasswordDirectoryViewController: UITableViewController, MobileTabRoot {
|
||||||
|
fileprivate let shellTab = MobileTab.passwords
|
||||||
|
private let path: String?
|
||||||
|
private var shellPage: MobilePage?
|
||||||
|
private var directoryPage: MobilePasswordPage?
|
||||||
|
private var loadTask: Task<Void, Never>?
|
||||||
|
private var loadGeneration = 0
|
||||||
|
|
||||||
|
init(shellPage: MobilePage, path: String? = nil) {
|
||||||
|
self.shellPage = shellPage
|
||||||
|
self.path = path
|
||||||
|
super.init(style: .insetGrouped)
|
||||||
|
title = shellPage.title
|
||||||
|
navigationItem.largeTitleDisplayMode = path == nil ? .always : .never
|
||||||
|
refreshControl = UIRefreshControl()
|
||||||
|
refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(localStoreDidChange),
|
||||||
|
name: .ironStorageLocalStoreDidChange,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(path: String, title: String) {
|
||||||
|
self.path = path
|
||||||
|
shellPage = nil
|
||||||
|
super.init(style: .insetGrouped)
|
||||||
|
self.title = title
|
||||||
|
navigationItem.largeTitleDisplayMode = .never
|
||||||
|
refreshControl = UIRefreshControl()
|
||||||
|
refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(localStoreDidChange),
|
||||||
|
name: .ironStorageLocalStoreDidChange,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
loadTask?.cancel()
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
if let shellPage {
|
||||||
|
apply(shellPage)
|
||||||
|
} else {
|
||||||
|
showLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
loadGeneration += 1
|
||||||
|
loadTask?.cancel()
|
||||||
|
refreshControl?.endRefreshing()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
directoryPage?.rows.isEmpty == false ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
directoryPage?.rows.count ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(
|
||||||
|
_ tableView: UITableView,
|
||||||
|
cellForRowAt indexPath: IndexPath
|
||||||
|
) -> UITableViewCell {
|
||||||
|
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||||
|
guard let row = directoryPage?.rows[indexPath.row] else { return cell }
|
||||||
|
var content = cell.defaultContentConfiguration()
|
||||||
|
content.image = UIImage(systemName: row.systemImage)
|
||||||
|
content.text = row.title
|
||||||
|
content.secondaryText = row.detail
|
||||||
|
content.textProperties.numberOfLines = 2
|
||||||
|
content.secondaryTextProperties.numberOfLines = 1
|
||||||
|
cell.contentConfiguration = content
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
cell.accessibilityLabel = "\(row.title), \(row.detail)"
|
||||||
|
cell.accessibilityHint = row.kind == .directory
|
||||||
|
? "Opens this password folder."
|
||||||
|
: "Opens the locked password viewer."
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
guard let row = directoryPage?.rows[indexPath.row] else { return }
|
||||||
|
let controller: UIViewController = switch row.kind {
|
||||||
|
case .directory:
|
||||||
|
PasswordDirectoryViewController(path: row.path, title: row.title)
|
||||||
|
case .entry:
|
||||||
|
LockedPasswordViewController(entry: row)
|
||||||
|
}
|
||||||
|
navigationController?.pushViewController(controller, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func refreshRequested() {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func localStoreDidChange() {
|
||||||
|
guard viewIfLoaded?.window != nil else { return }
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reload() {
|
||||||
|
loadGeneration += 1
|
||||||
|
let generation = loadGeneration
|
||||||
|
let selectedID = tableView.indexPathForSelectedRow.flatMap {
|
||||||
|
directoryPage?.rows[$0.row].id
|
||||||
|
}
|
||||||
|
loadTask?.cancel()
|
||||||
|
loadTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
if path == nil {
|
||||||
|
let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
|
||||||
|
guard
|
||||||
|
!Task.isCancelled,
|
||||||
|
generation == loadGeneration,
|
||||||
|
let page = shell.pages.first(where: { $0.tab == .passwords })
|
||||||
|
else { return }
|
||||||
|
apply(page)
|
||||||
|
guard page.state == .ready else { return }
|
||||||
|
} else {
|
||||||
|
showLoading()
|
||||||
|
}
|
||||||
|
let path = self.path
|
||||||
|
let result = await Task.detached(priority: .userInitiated) {
|
||||||
|
do {
|
||||||
|
return Result<MobilePasswordPage, PasswordFailure>.success(
|
||||||
|
try mobilePasswordPage(path: path)
|
||||||
|
)
|
||||||
|
} catch let error as MobilePasswordFfiError {
|
||||||
|
return .failure(PasswordFailure(error))
|
||||||
|
} catch {
|
||||||
|
return .failure(.unexpected)
|
||||||
|
}
|
||||||
|
}.value
|
||||||
|
guard !Task.isCancelled, generation == loadGeneration else { return }
|
||||||
|
finish(result, selectedID: selectedID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func apply(_ page: MobilePage) {
|
||||||
|
shellPage = page
|
||||||
|
title = page.title
|
||||||
|
guard page.state == .ready else {
|
||||||
|
directoryPage = nil
|
||||||
|
tableView.reloadData()
|
||||||
|
refreshControl?.endRefreshing()
|
||||||
|
var configuration = page.state == .loading
|
||||||
|
? UIContentUnavailableConfiguration.loading()
|
||||||
|
: UIContentUnavailableConfiguration.empty()
|
||||||
|
configuration.image = page.state == .loading
|
||||||
|
? nil
|
||||||
|
: UIImage(systemName: page.state == .error ? "exclamationmark.triangle" : "lock.shield")
|
||||||
|
configuration.text = page.stateTitle
|
||||||
|
configuration.secondaryText = page.stateDetail
|
||||||
|
contentUnavailableConfiguration = configuration
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finish(
|
||||||
|
_ result: Result<MobilePasswordPage, PasswordFailure>,
|
||||||
|
selectedID: String?
|
||||||
|
) {
|
||||||
|
refreshControl?.endRefreshing()
|
||||||
|
switch result {
|
||||||
|
case let .success(page):
|
||||||
|
directoryPage = page
|
||||||
|
title = page.title
|
||||||
|
contentUnavailableConfiguration = nil
|
||||||
|
tableView.reloadData()
|
||||||
|
if page.rows.isEmpty {
|
||||||
|
var configuration = UIContentUnavailableConfiguration.empty()
|
||||||
|
configuration.image = UIImage(systemName: "folder")
|
||||||
|
configuration.text = "Empty Folder"
|
||||||
|
configuration.secondaryText = "This password folder contains no entries."
|
||||||
|
contentUnavailableConfiguration = configuration
|
||||||
|
} else if let selectedID,
|
||||||
|
let row = page.rows.firstIndex(where: { $0.id == selectedID }) {
|
||||||
|
tableView.selectRow(at: IndexPath(row: row, section: 0), animated: false, scrollPosition: .none)
|
||||||
|
}
|
||||||
|
case let .failure(failure):
|
||||||
|
if failure.kind == .directoryMissing, path != nil {
|
||||||
|
UIAccessibility.post(
|
||||||
|
notification: .announcement,
|
||||||
|
argument: "The password folder no longer exists."
|
||||||
|
)
|
||||||
|
navigationController?.popViewController(animated: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
directoryPage = nil
|
||||||
|
tableView.reloadData()
|
||||||
|
var configuration = UIContentUnavailableConfiguration.empty()
|
||||||
|
configuration.image = UIImage(systemName: "exclamationmark.triangle")
|
||||||
|
configuration.text = failure.title
|
||||||
|
configuration.secondaryText = failure.detail
|
||||||
|
contentUnavailableConfiguration = configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showLoading() {
|
||||||
|
var configuration = UIContentUnavailableConfiguration.loading()
|
||||||
|
configuration.text = "Loading Passwords"
|
||||||
|
configuration.secondaryText = "Reading the locked folder model from storage."
|
||||||
|
contentUnavailableConfiguration = configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PasswordFailure: Error, Sendable {
|
||||||
|
let kind: MobilePasswordErrorKind
|
||||||
|
let title: String
|
||||||
|
let detail: String
|
||||||
|
|
||||||
|
init(_ error: MobilePasswordFfiError) {
|
||||||
|
switch error {
|
||||||
|
case let .Failed(kind, title, detail):
|
||||||
|
self.kind = kind
|
||||||
|
self.title = title
|
||||||
|
self.detail = detail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static let unexpected = PasswordFailure(
|
||||||
|
kind: .repository,
|
||||||
|
title: "Passwords Are Unavailable",
|
||||||
|
detail: "IronStorage could not load the storage-provided folder."
|
||||||
|
)
|
||||||
|
|
||||||
|
private init(kind: MobilePasswordErrorKind, title: String, detail: String) {
|
||||||
|
self.kind = kind
|
||||||
|
self.title = title
|
||||||
|
self.detail = detail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class LockedPasswordViewController: UIViewController {
|
||||||
|
private let entry: MobilePasswordRow
|
||||||
|
|
||||||
|
init(entry: MobilePasswordRow) {
|
||||||
|
self.entry = entry
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
title = entry.title
|
||||||
|
navigationItem.largeTitleDisplayMode = .never
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
view.backgroundColor = .systemGroupedBackground
|
||||||
|
var configuration = UIContentUnavailableConfiguration.empty()
|
||||||
|
configuration.image = UIImage(systemName: "lock.fill")
|
||||||
|
configuration.text = "Locked Password"
|
||||||
|
configuration.secondaryText = "Authenticate to reveal this password entry."
|
||||||
|
contentUnavailableConfiguration = configuration
|
||||||
|
view.accessibilityIdentifier = entry.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct HomeFailure: Error, Sendable {
|
private struct HomeFailure: Error, Sendable {
|
||||||
let kind: MobileHomeErrorKind
|
let kind: MobileHomeErrorKind
|
||||||
let title: String
|
let title: String
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ use ironstorage::{
|
|||||||
MobileOnboardingErrorKind as StorageOnboardingErrorKind,
|
MobileOnboardingErrorKind as StorageOnboardingErrorKind,
|
||||||
MobileOnboardingPhase as StorageOnboardingPhase,
|
MobileOnboardingPhase as StorageOnboardingPhase,
|
||||||
},
|
},
|
||||||
|
mobile_passwords::{
|
||||||
|
MobilePasswordError as StoragePasswordError,
|
||||||
|
MobilePasswordErrorKind as StoragePasswordErrorKind,
|
||||||
|
MobilePasswordRowKind as StoragePasswordRowKind,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
uniffi::setup_scaffolding!();
|
uniffi::setup_scaffolding!();
|
||||||
@@ -381,6 +386,111 @@ impl MobileHomeOperation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||||
|
pub enum MobilePasswordRowKind {
|
||||||
|
Directory,
|
||||||
|
Entry,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StoragePasswordRowKind> for MobilePasswordRowKind {
|
||||||
|
fn from(kind: StoragePasswordRowKind) -> Self {
|
||||||
|
match kind {
|
||||||
|
StoragePasswordRowKind::Directory => Self::Directory,
|
||||||
|
StoragePasswordRowKind::Entry => Self::Entry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobilePasswordRow {
|
||||||
|
pub id: String,
|
||||||
|
pub path: String,
|
||||||
|
pub title: String,
|
||||||
|
pub detail: String,
|
||||||
|
pub system_image: String,
|
||||||
|
pub kind: MobilePasswordRowKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobilePasswordPage {
|
||||||
|
pub id: String,
|
||||||
|
pub path: String,
|
||||||
|
pub title: String,
|
||||||
|
pub rows: Vec<MobilePasswordRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ironstorage::mobile_passwords::MobilePasswordPage> for MobilePasswordPage {
|
||||||
|
fn from(page: ironstorage::mobile_passwords::MobilePasswordPage) -> Self {
|
||||||
|
Self {
|
||||||
|
id: page.id().to_owned(),
|
||||||
|
path: page.path().to_owned(),
|
||||||
|
title: page.title().to_owned(),
|
||||||
|
rows: page
|
||||||
|
.rows()
|
||||||
|
.iter()
|
||||||
|
.map(|row| MobilePasswordRow {
|
||||||
|
id: row.id().to_owned(),
|
||||||
|
path: row.path().to_owned(),
|
||||||
|
title: row.title().to_owned(),
|
||||||
|
detail: row.detail().to_owned(),
|
||||||
|
system_image: row.system_image().to_owned(),
|
||||||
|
kind: row.kind().into(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||||
|
pub enum MobilePasswordErrorKind {
|
||||||
|
MissingConfiguration,
|
||||||
|
Configuration,
|
||||||
|
InvalidPath,
|
||||||
|
DirectoryMissing,
|
||||||
|
Repository,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StoragePasswordErrorKind> for MobilePasswordErrorKind {
|
||||||
|
fn from(kind: StoragePasswordErrorKind) -> Self {
|
||||||
|
match kind {
|
||||||
|
StoragePasswordErrorKind::MissingConfiguration => Self::MissingConfiguration,
|
||||||
|
StoragePasswordErrorKind::Configuration => Self::Configuration,
|
||||||
|
StoragePasswordErrorKind::InvalidPath => Self::InvalidPath,
|
||||||
|
StoragePasswordErrorKind::DirectoryMissing => Self::DirectoryMissing,
|
||||||
|
StoragePasswordErrorKind::Repository => Self::Repository,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, uniffi::Error)]
|
||||||
|
pub enum MobilePasswordFfiError {
|
||||||
|
Failed {
|
||||||
|
kind: MobilePasswordErrorKind,
|
||||||
|
title: String,
|
||||||
|
detail: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MobilePasswordFfiError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Failed { title, detail, .. } => write!(formatter, "{title}: {detail}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for MobilePasswordFfiError {}
|
||||||
|
|
||||||
|
impl From<StoragePasswordError> for MobilePasswordFfiError {
|
||||||
|
fn from(error: StoragePasswordError) -> Self {
|
||||||
|
Self::Failed {
|
||||||
|
kind: error.kind().into(),
|
||||||
|
title: error.title().to_owned(),
|
||||||
|
detail: error.detail().to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||||
pub enum MobileOnboardingPhase {
|
pub enum MobileOnboardingPhase {
|
||||||
Validating,
|
Validating,
|
||||||
@@ -592,6 +702,15 @@ pub fn mobile_home_operation() -> Arc<MobileHomeOperation> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
pub fn mobile_password_page(
|
||||||
|
path: Option<String>,
|
||||||
|
) -> Result<MobilePasswordPage, MobilePasswordFfiError> {
|
||||||
|
ironstorage::mobile_passwords::MobilePasswordPage::load(path.as_deref())
|
||||||
|
.map(Into::into)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
pub fn mobile_onboarding_operation(
|
pub fn mobile_onboarding_operation(
|
||||||
server_url: String,
|
server_url: String,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub mod kdbx;
|
|||||||
pub mod mobile;
|
pub mod mobile;
|
||||||
pub mod mobile_home;
|
pub mod mobile_home;
|
||||||
pub mod mobile_onboarding;
|
pub mod mobile_onboarding;
|
||||||
|
pub mod mobile_passwords;
|
||||||
pub mod mutation;
|
pub mod mutation;
|
||||||
pub mod otp;
|
pub mod otp;
|
||||||
pub mod presentation;
|
pub mod presentation;
|
||||||
|
|||||||
286
crates/storage/src/mobile_passwords.rs
Normal file
286
crates/storage/src/mobile_passwords.rs
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
//! Storage-owned, locked password navigation for native mobile frontends.
|
||||||
|
|
||||||
|
use std::{error::Error, fmt};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{Config, ConfigError},
|
||||||
|
read::{ReadError, TreeNode, TreeNodeKind, list_tree},
|
||||||
|
repository::{DirectoryPath, Repository, RepositoryError},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MobilePasswordRowKind {
|
||||||
|
Directory,
|
||||||
|
Entry,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MobilePasswordRow {
|
||||||
|
id: String,
|
||||||
|
path: String,
|
||||||
|
title: String,
|
||||||
|
detail: String,
|
||||||
|
system_image: String,
|
||||||
|
kind: MobilePasswordRowKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobilePasswordRow {
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &str {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detail(&self) -> &str {
|
||||||
|
&self.detail
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn system_image(&self) -> &str {
|
||||||
|
&self.system_image
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> MobilePasswordRowKind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MobilePasswordPage {
|
||||||
|
id: String,
|
||||||
|
path: String,
|
||||||
|
title: String,
|
||||||
|
rows: Vec<MobilePasswordRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobilePasswordPage {
|
||||||
|
pub fn load(path: Option<&str>) -> Result<Self, MobilePasswordError> {
|
||||||
|
let config = Config::load(None).map_err(MobilePasswordError::from_config)?;
|
||||||
|
let repository =
|
||||||
|
Repository::open(config.vault()).map_err(MobilePasswordError::repository)?;
|
||||||
|
Self::from_repository(&repository, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &str {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rows(&self) -> &[MobilePasswordRow] {
|
||||||
|
&self.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_repository(
|
||||||
|
repository: &Repository,
|
||||||
|
path: Option<&str>,
|
||||||
|
) -> Result<Self, MobilePasswordError> {
|
||||||
|
let directory = match path {
|
||||||
|
Some(path) => DirectoryPath::parse(path).map_err(MobilePasswordError::repository)?,
|
||||||
|
None => DirectoryPath::root(),
|
||||||
|
};
|
||||||
|
let tree = list_tree(repository, &directory).map_err(MobilePasswordError::from_read)?;
|
||||||
|
let path = directory
|
||||||
|
.as_path()
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(MobilePasswordError::invalid_path)?
|
||||||
|
.to_owned();
|
||||||
|
let title = directory
|
||||||
|
.as_path()
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("Passwords")
|
||||||
|
.to_owned();
|
||||||
|
Ok(Self {
|
||||||
|
id: format!("directory:{path}"),
|
||||||
|
path,
|
||||||
|
title,
|
||||||
|
rows: tree.children().iter().map(mobile_row).collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mobile_row(node: &TreeNode) -> MobilePasswordRow {
|
||||||
|
let path = node.path().to_owned();
|
||||||
|
let (kind, prefix, detail, system_image) = match node.kind() {
|
||||||
|
TreeNodeKind::Directory => (
|
||||||
|
MobilePasswordRowKind::Directory,
|
||||||
|
"directory",
|
||||||
|
"Folder",
|
||||||
|
"folder",
|
||||||
|
),
|
||||||
|
TreeNodeKind::Entry => (
|
||||||
|
MobilePasswordRowKind::Entry,
|
||||||
|
"entry",
|
||||||
|
"Locked password",
|
||||||
|
"key.fill",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
MobilePasswordRow {
|
||||||
|
id: format!("{prefix}:{path}"),
|
||||||
|
path,
|
||||||
|
title: node.name().to_owned(),
|
||||||
|
detail: detail.to_owned(),
|
||||||
|
system_image: system_image.to_owned(),
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MobilePasswordErrorKind {
|
||||||
|
MissingConfiguration,
|
||||||
|
Configuration,
|
||||||
|
InvalidPath,
|
||||||
|
DirectoryMissing,
|
||||||
|
Repository,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MobilePasswordError {
|
||||||
|
kind: MobilePasswordErrorKind,
|
||||||
|
title: String,
|
||||||
|
detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobilePasswordError {
|
||||||
|
pub fn kind(&self) -> MobilePasswordErrorKind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detail(&self) -> &str {
|
||||||
|
&self.detail
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_config(error: ConfigError) -> Self {
|
||||||
|
let kind = if matches!(error, ConfigError::NotFound { .. }) {
|
||||||
|
MobilePasswordErrorKind::MissingConfiguration
|
||||||
|
} else {
|
||||||
|
MobilePasswordErrorKind::Configuration
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
title: "Passwords Are Unavailable".to_owned(),
|
||||||
|
detail: error.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repository(error: RepositoryError) -> Self {
|
||||||
|
let kind = match error {
|
||||||
|
RepositoryError::InvalidPath { .. } => MobilePasswordErrorKind::InvalidPath,
|
||||||
|
RepositoryError::NotFound { .. } => MobilePasswordErrorKind::DirectoryMissing,
|
||||||
|
_ => MobilePasswordErrorKind::Repository,
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
title: match kind {
|
||||||
|
MobilePasswordErrorKind::DirectoryMissing => "Folder No Longer Exists",
|
||||||
|
MobilePasswordErrorKind::InvalidPath => "Invalid Password Folder",
|
||||||
|
_ => "Passwords Are Unavailable",
|
||||||
|
}
|
||||||
|
.to_owned(),
|
||||||
|
detail: error.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_read(error: ReadError) -> Self {
|
||||||
|
match error {
|
||||||
|
ReadError::Repository(error) => Self::repository(error),
|
||||||
|
_ => Self {
|
||||||
|
kind: MobilePasswordErrorKind::Repository,
|
||||||
|
title: "Passwords Are Unavailable".to_owned(),
|
||||||
|
detail: error.to_string(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_path() -> Self {
|
||||||
|
Self {
|
||||||
|
kind: MobilePasswordErrorKind::InvalidPath,
|
||||||
|
title: "Invalid Password Folder".to_owned(),
|
||||||
|
detail: "The password-store path is not valid UTF-8.".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MobilePasswordError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(formatter, "{}: {}", self.title, self.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for MobilePasswordError {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
use super::{MobilePasswordErrorKind, MobilePasswordPage, MobilePasswordRowKind};
|
||||||
|
use crate::repository::Repository;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn locked_pages_preserve_storage_identity_across_nested_refreshes()
|
||||||
|
-> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let temporary = tempdir()?;
|
||||||
|
fs::create_dir_all(temporary.path().join("Personal/Finance/Very Deep/Empty"))?;
|
||||||
|
fs::write(
|
||||||
|
temporary.path().join("Personal/Finance/bank.gpg"),
|
||||||
|
b"ciphertext",
|
||||||
|
)?;
|
||||||
|
fs::write(
|
||||||
|
temporary
|
||||||
|
.path()
|
||||||
|
.join("Personal/a very long password entry name.gpg"),
|
||||||
|
b"ciphertext",
|
||||||
|
)?;
|
||||||
|
let repository = Repository::open(temporary.path())?;
|
||||||
|
|
||||||
|
let root = MobilePasswordPage::from_repository(&repository, None)?;
|
||||||
|
assert_eq!(root.title(), "Passwords");
|
||||||
|
assert_eq!(root.rows()[0].id(), "directory:Personal");
|
||||||
|
assert_eq!(root.rows()[0].kind(), MobilePasswordRowKind::Directory);
|
||||||
|
|
||||||
|
let finance = MobilePasswordPage::from_repository(&repository, Some("Personal/Finance"))?;
|
||||||
|
assert_eq!(finance.id(), "directory:Personal/Finance");
|
||||||
|
assert_eq!(finance.title(), "Finance");
|
||||||
|
assert_eq!(
|
||||||
|
finance.rows()[0].id(),
|
||||||
|
"directory:Personal/Finance/Very Deep"
|
||||||
|
);
|
||||||
|
assert_eq!(finance.rows()[1].id(), "entry:Personal/Finance/bank");
|
||||||
|
assert_eq!(finance.rows()[1].detail(), "Locked password");
|
||||||
|
|
||||||
|
let refreshed = MobilePasswordPage::from_repository(&repository, Some("Personal/Finance"))?;
|
||||||
|
assert_eq!(finance, refreshed);
|
||||||
|
let empty = MobilePasswordPage::from_repository(
|
||||||
|
&repository,
|
||||||
|
Some("Personal/Finance/Very Deep/Empty"),
|
||||||
|
)?;
|
||||||
|
assert!(empty.rows().is_empty());
|
||||||
|
|
||||||
|
fs::remove_dir(temporary.path().join("Personal/Finance/Very Deep/Empty"))?;
|
||||||
|
let error = MobilePasswordPage::from_repository(
|
||||||
|
&repository,
|
||||||
|
Some("Personal/Finance/Very Deep/Empty"),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(error.kind(), MobilePasswordErrorKind::DirectoryMissing);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -386,8 +386,7 @@ impl<'a> VaultReader<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn list(&self, directory: &DirectoryPath) -> Result<TreeModel, ReadError> {
|
pub fn list(&self, directory: &DirectoryPath) -> Result<TreeModel, ReadError> {
|
||||||
let snapshot = self.repository.snapshot()?;
|
list_tree(self.repository, directory)
|
||||||
build_tree(&snapshot, directory, None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implement explicit or implicit show dispatch. No path means the root tree; a directory
|
/// Implement explicit or implicit show dispatch. No path means the root tree; a directory
|
||||||
@@ -565,6 +564,15 @@ impl<'a> VaultReader<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a locked navigation model without loading key material or decrypting entries.
|
||||||
|
pub(crate) fn list_tree(
|
||||||
|
repository: &Repository,
|
||||||
|
directory: &DirectoryPath,
|
||||||
|
) -> Result<TreeModel, ReadError> {
|
||||||
|
let snapshot = repository.snapshot()?;
|
||||||
|
build_tree(&snapshot, directory, None)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_regex(request: &GrepRequest) -> Result<Regex, ReadError> {
|
fn build_regex(request: &GrepRequest) -> Result<Regex, ReadError> {
|
||||||
let pattern = if request.fixed_strings {
|
let pattern = if request.fixed_strings {
|
||||||
regex::escape(&request.pattern)
|
regex::escape(&request.pattern)
|
||||||
|
|||||||
Reference in New Issue
Block a user