Implement iPhone password search
This commit is contained in:
@@ -5290,6 +5290,7 @@ public enum MobileTab: Equatable, Hashable {
|
||||
|
||||
case home
|
||||
case passwords
|
||||
case search
|
||||
case totp
|
||||
case preferences
|
||||
|
||||
@@ -5317,9 +5318,11 @@ public struct FfiConverterTypeMobileTab: FfiConverterRustBuffer {
|
||||
|
||||
case 2: return .passwords
|
||||
|
||||
case 3: return .totp
|
||||
case 3: return .search
|
||||
|
||||
case 4: return .preferences
|
||||
case 4: return .totp
|
||||
|
||||
case 5: return .preferences
|
||||
|
||||
default: throw UniffiInternalError.unexpectedEnumCase
|
||||
}
|
||||
@@ -5337,13 +5340,17 @@ public struct FfiConverterTypeMobileTab: FfiConverterRustBuffer {
|
||||
writeInt(&buf, Int32(2))
|
||||
|
||||
|
||||
case .totp:
|
||||
case .search:
|
||||
writeInt(&buf, Int32(3))
|
||||
|
||||
|
||||
case .preferences:
|
||||
case .totp:
|
||||
writeInt(&buf, Int32(4))
|
||||
|
||||
|
||||
case .preferences:
|
||||
writeInt(&buf, Int32(5))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5914,6 +5921,14 @@ public func mobilePasswordPage(path: String?)throws -> MobilePasswordPage {
|
||||
)
|
||||
})
|
||||
}
|
||||
public func mobilePasswordSearch(query: String)throws -> MobilePasswordPage {
|
||||
return try FfiConverterTypeMobilePasswordPage_lift(try rustCallWithError(FfiConverterTypeMobilePasswordFfiError_lift) {
|
||||
uniffiCallStatus in
|
||||
uniffi_ironstorage_apple_fn_func_mobile_password_search(
|
||||
FfiConverterString.lower(query),uniffiCallStatus
|
||||
)
|
||||
})
|
||||
}
|
||||
public func mobileShell() -> MobileShell {
|
||||
return try! FfiConverterTypeMobileShell_lift(try! rustCall() {
|
||||
uniffiCallStatus in
|
||||
@@ -5982,6 +5997,9 @@ private let initializationResult: InitializationResult = {
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_password_page() != 64312) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_password_search() != 23429) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
if (uniffi_ironstorage_apple_checksum_func_mobile_shell() != 42687) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
|
||||
@@ -521,6 +521,11 @@ uint64_t uniffi_ironstorage_apple_fn_func_mobile_onboarding_operation(RustBuffer
|
||||
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_PASSWORD_SEARCH
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_PASSWORD_SEARCH
|
||||
RustBuffer uniffi_ironstorage_apple_fn_func_mobile_password_search(RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||
);
|
||||
#endif
|
||||
#ifndef 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
|
||||
@@ -836,6 +841,12 @@ uint16_t uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation(void
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_PASSWORD_PAGE
|
||||
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_password_page(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_PASSWORD_SEARCH
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_PASSWORD_SEARCH
|
||||
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_password_search(void
|
||||
|
||||
);
|
||||
#endif
|
||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_SHELL
|
||||
|
||||
@@ -87,6 +87,8 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
||||
let root: UIViewController = switch page.tab {
|
||||
case .passwords:
|
||||
PasswordDirectoryViewController(shellPage: page, authentication: authentication)
|
||||
case .search:
|
||||
PasswordSearchViewController(shellPage: page, authentication: authentication)
|
||||
case .totp:
|
||||
TotpListViewController(shellPage: page, authentication: authentication)
|
||||
case .preferences:
|
||||
@@ -2307,6 +2309,221 @@ private extension UInt64 {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class PasswordSearchViewController: UITableViewController, MobileTabRoot,
|
||||
UISearchResultsUpdating
|
||||
{
|
||||
fileprivate let shellTab = MobileTab.search
|
||||
private let authentication: MobileAuthentication?
|
||||
private let searchController = UISearchController(searchResultsController: nil)
|
||||
private var shellPage: MobilePage
|
||||
private var searchPage: MobilePasswordPage?
|
||||
private var searchTask: Task<Void, Never>?
|
||||
private var shellTask: Task<Void, Never>?
|
||||
private var generation = 0
|
||||
|
||||
init(shellPage: MobilePage, authentication: MobileAuthentication?) {
|
||||
self.shellPage = shellPage
|
||||
self.authentication = authentication
|
||||
super.init(style: .insetGrouped)
|
||||
title = shellPage.title
|
||||
navigationItem.largeTitleDisplayMode = .always
|
||||
searchController.searchResultsUpdater = self
|
||||
searchController.obscuresBackgroundDuringPresentation = false
|
||||
searchController.searchBar.placeholder = "Entry names and folders"
|
||||
searchController.searchBar.autocapitalizationType = .none
|
||||
searchController.searchBar.autocorrectionType = .no
|
||||
searchController.searchBar.spellCheckingType = .no
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = false
|
||||
definesPresentationContext = true
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(localStoreDidChange),
|
||||
name: .ironStorageLocalStoreDidChange,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported")
|
||||
}
|
||||
|
||||
deinit {
|
||||
searchTask?.cancel()
|
||||
shellTask?.cancel()
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
apply(shellPage)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
reloadShell()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if searchController.searchBar.text?.isEmpty != false {
|
||||
searchController.isActive = true
|
||||
searchController.searchBar.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
searchPage?.rows.isEmpty == false ? 1 : 0
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
searchPage?.rows.count ?? 0
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||
guard let row = searchPage?.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 = 2
|
||||
cell.contentConfiguration = content
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
cell.accessibilityLabel = "\(row.title), in \(row.detail)"
|
||||
cell.accessibilityHint = "Opens the locked password viewer."
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard let row = searchPage?.rows[indexPath.row] else { return }
|
||||
navigationController?.pushViewController(
|
||||
LockedPasswordViewController(entry: row, authentication: authentication),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
|
||||
func updateSearchResults(for searchController: UISearchController) {
|
||||
search(searchController.searchBar.text ?? "")
|
||||
}
|
||||
|
||||
@objc private func localStoreDidChange() {
|
||||
guard viewIfLoaded?.window != nil else { return }
|
||||
search(searchController.searchBar.text ?? "")
|
||||
}
|
||||
|
||||
private func reloadShell() {
|
||||
shellTask?.cancel()
|
||||
shellTask = Task { [weak self] in
|
||||
let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
|
||||
guard
|
||||
!Task.isCancelled,
|
||||
let self,
|
||||
let page = shell.pages.first(where: { $0.tab == .search })
|
||||
else { return }
|
||||
apply(page)
|
||||
}
|
||||
}
|
||||
|
||||
private func apply(_ page: MobilePage) {
|
||||
shellPage = page
|
||||
title = page.title
|
||||
guard page.state == .ready else {
|
||||
searchTask?.cancel()
|
||||
searchPage = nil
|
||||
tableView.reloadData()
|
||||
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
|
||||
}
|
||||
search(searchController.searchBar.text ?? "")
|
||||
}
|
||||
|
||||
private func search(_ query: String) {
|
||||
generation += 1
|
||||
let currentGeneration = generation
|
||||
searchTask?.cancel()
|
||||
guard shellPage.state == .ready else { return }
|
||||
guard !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
searchPage = nil
|
||||
tableView.reloadData()
|
||||
var configuration = UIContentUnavailableConfiguration.empty()
|
||||
configuration.image = UIImage(systemName: "magnifyingglass")
|
||||
configuration.text = "Search Passwords"
|
||||
configuration.secondaryText = "Search by entry name or folder."
|
||||
contentUnavailableConfiguration = configuration
|
||||
return
|
||||
}
|
||||
|
||||
searchPage = nil
|
||||
tableView.reloadData()
|
||||
var loading = UIContentUnavailableConfiguration.loading()
|
||||
loading.text = "Searching Passwords"
|
||||
contentUnavailableConfiguration = loading
|
||||
searchTask = Task { [weak self] in
|
||||
let result = await Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
return Result<MobilePasswordPage, PasswordFailure>.success(
|
||||
try mobilePasswordSearch(query: query)
|
||||
)
|
||||
} catch let error as MobilePasswordFfiError {
|
||||
return .failure(PasswordFailure(error))
|
||||
} catch {
|
||||
return .failure(.unexpected)
|
||||
}
|
||||
}.value
|
||||
guard !Task.isCancelled, let self, currentGeneration == generation else { return }
|
||||
finish(result, query: query)
|
||||
}
|
||||
}
|
||||
|
||||
private func finish(
|
||||
_ result: Result<MobilePasswordPage, PasswordFailure>,
|
||||
query: String
|
||||
) {
|
||||
switch result {
|
||||
case let .success(page):
|
||||
searchPage = page
|
||||
tableView.reloadData()
|
||||
if page.rows.isEmpty {
|
||||
var configuration = UIContentUnavailableConfiguration.empty()
|
||||
configuration.image = UIImage(systemName: "magnifyingglass")
|
||||
configuration.text = "No Results"
|
||||
configuration.secondaryText = "No password entries match “\(query)”."
|
||||
contentUnavailableConfiguration = configuration
|
||||
} else {
|
||||
contentUnavailableConfiguration = nil
|
||||
UIAccessibility.post(
|
||||
notification: .announcement,
|
||||
argument: "\(page.rows.count) password results"
|
||||
)
|
||||
}
|
||||
case let .failure(failure):
|
||||
searchPage = nil
|
||||
tableView.reloadData()
|
||||
var configuration = UIContentUnavailableConfiguration.empty()
|
||||
configuration.image = UIImage(systemName: "exclamationmark.triangle")
|
||||
configuration.text = failure.title
|
||||
configuration.secondaryText = failure.detail
|
||||
contentUnavailableConfiguration = configuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class PasswordDirectoryViewController: UITableViewController, MobileTabRoot {
|
||||
fileprivate let shellTab = MobileTab.passwords
|
||||
|
||||
@@ -60,6 +60,7 @@ uniffi::setup_scaffolding!();
|
||||
pub enum MobileTab {
|
||||
Home,
|
||||
Passwords,
|
||||
Search,
|
||||
Totp,
|
||||
Preferences,
|
||||
}
|
||||
@@ -69,6 +70,7 @@ impl From<StorageTab> for MobileTab {
|
||||
match tab {
|
||||
StorageTab::Home => Self::Home,
|
||||
StorageTab::Passwords => Self::Passwords,
|
||||
StorageTab::Search => Self::Search,
|
||||
StorageTab::Totp => Self::Totp,
|
||||
StorageTab::Preferences => Self::Preferences,
|
||||
}
|
||||
@@ -80,6 +82,7 @@ impl From<MobileTab> for StorageTab {
|
||||
match tab {
|
||||
MobileTab::Home => Self::Home,
|
||||
MobileTab::Passwords => Self::Passwords,
|
||||
MobileTab::Search => Self::Search,
|
||||
MobileTab::Totp => Self::Totp,
|
||||
MobileTab::Preferences => Self::Preferences,
|
||||
}
|
||||
@@ -1598,6 +1601,13 @@ pub fn mobile_password_page(
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn mobile_password_search(query: String) -> Result<MobilePasswordPage, MobilePasswordFfiError> {
|
||||
ironstorage::mobile_passwords::MobilePasswordPage::search(&query)
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn mobile_authentication() -> Result<Arc<MobileAuthentication>, MobileAuthenticationFfiError> {
|
||||
Ok(Arc::new(MobileAuthentication {
|
||||
@@ -1665,7 +1675,7 @@ mod tests {
|
||||
] {
|
||||
let shell = super::mobile_shell_fixture(state);
|
||||
assert_eq!(shell.selected_tab, MobileTab::Home);
|
||||
assert_eq!(shell.pages.len(), 4);
|
||||
assert_eq!(shell.pages.len(), 5);
|
||||
assert!(shell.pages.iter().all(|page| page.state == state));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,17 +10,25 @@ pub enum MobileTab {
|
||||
#[default]
|
||||
Home,
|
||||
Passwords,
|
||||
Search,
|
||||
Totp,
|
||||
Preferences,
|
||||
}
|
||||
|
||||
impl MobileTab {
|
||||
pub const ALL: [Self; 4] = [Self::Home, Self::Passwords, Self::Totp, Self::Preferences];
|
||||
pub const ALL: [Self; 5] = [
|
||||
Self::Home,
|
||||
Self::Passwords,
|
||||
Self::Search,
|
||||
Self::Totp,
|
||||
Self::Preferences,
|
||||
];
|
||||
|
||||
pub const fn title(self) -> &'static str {
|
||||
match self {
|
||||
Self::Home => "Home",
|
||||
Self::Passwords => "Passwords",
|
||||
Self::Search => "Search",
|
||||
Self::Totp => "TOTP",
|
||||
Self::Preferences => "Preferences",
|
||||
}
|
||||
@@ -30,6 +38,7 @@ impl MobileTab {
|
||||
match self {
|
||||
Self::Home => "house",
|
||||
Self::Passwords => "key",
|
||||
Self::Search => "magnifyingglass",
|
||||
Self::Totp => "timer",
|
||||
Self::Preferences => "gearshape",
|
||||
}
|
||||
@@ -39,6 +48,7 @@ impl MobileTab {
|
||||
match self {
|
||||
Self::Home => "house.fill",
|
||||
Self::Passwords => "key.fill",
|
||||
Self::Search => "magnifyingglass",
|
||||
Self::Totp => "timer",
|
||||
Self::Preferences => "gearshape.fill",
|
||||
}
|
||||
@@ -48,6 +58,7 @@ impl MobileTab {
|
||||
match self {
|
||||
Self::Home => "home",
|
||||
Self::Passwords => "passwords",
|
||||
Self::Search => "search",
|
||||
Self::Totp => "totp",
|
||||
Self::Preferences => "preferences",
|
||||
}
|
||||
@@ -57,6 +68,7 @@ impl MobileTab {
|
||||
match value {
|
||||
"home" => Ok(Self::Home),
|
||||
"passwords" => Ok(Self::Passwords),
|
||||
"search" => Ok(Self::Search),
|
||||
"totp" => Ok(Self::Totp),
|
||||
"preferences" => Ok(Self::Preferences),
|
||||
_ => Err(ConfigError::InvalidField {
|
||||
@@ -234,7 +246,7 @@ mod tests {
|
||||
use super::{MobilePage, MobileShell, MobileShellState, MobileTab, store_selected_tab_from};
|
||||
|
||||
#[test]
|
||||
fn every_fixture_is_view_ready_for_all_four_tabs() {
|
||||
fn every_fixture_is_view_ready_for_all_five_tabs() {
|
||||
for state in [
|
||||
MobileShellState::Loading,
|
||||
MobileShellState::Empty,
|
||||
@@ -244,7 +256,7 @@ mod tests {
|
||||
] {
|
||||
let shell = MobileShell::fixture(state);
|
||||
assert_eq!(shell.selected_tab(), MobileTab::Home);
|
||||
assert_eq!(shell.pages().len(), 4);
|
||||
assert_eq!(shell.pages().len(), 5);
|
||||
assert_eq!(
|
||||
shell
|
||||
.pages()
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{error::Error, fmt};
|
||||
|
||||
use crate::{
|
||||
config::{Config, ConfigError},
|
||||
read::{ReadError, TreeNode, TreeNodeKind, list_tree},
|
||||
read::{ReadError, TreeNode, TreeNodeKind, hidden_path, list_tree},
|
||||
repository::{DirectoryPath, Repository, RepositoryError},
|
||||
};
|
||||
|
||||
@@ -66,6 +66,13 @@ impl MobilePasswordPage {
|
||||
Self::from_repository(&repository, path)
|
||||
}
|
||||
|
||||
pub fn search(query: &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::search_repository(&repository, query)
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
@@ -109,6 +116,61 @@ impl MobilePasswordPage {
|
||||
rows: tree.children().iter().map(mobile_row).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn search_repository(
|
||||
repository: &Repository,
|
||||
query: &str,
|
||||
) -> Result<Self, MobilePasswordError> {
|
||||
let query = query.trim();
|
||||
let folded = query.to_lowercase();
|
||||
let snapshot = repository
|
||||
.snapshot()
|
||||
.map_err(MobilePasswordError::repository)?;
|
||||
let rows = if folded.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
snapshot
|
||||
.entries()
|
||||
.filter(|entry| !hidden_path(entry.path().as_path()))
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path().as_path().to_str()?;
|
||||
path.to_lowercase()
|
||||
.contains(&folded)
|
||||
.then(|| search_row(entry.path(), path))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
Ok(Self {
|
||||
id: format!("search:{query}"),
|
||||
path: String::new(),
|
||||
title: "Search".to_owned(),
|
||||
rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn search_row(path: &crate::repository::EntryPath, text: &str) -> MobilePasswordRow {
|
||||
let title = path
|
||||
.as_path()
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or(text)
|
||||
.to_owned();
|
||||
let detail = path
|
||||
.as_path()
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
.and_then(|parent| parent.to_str())
|
||||
.unwrap_or("Password Store")
|
||||
.to_owned();
|
||||
MobilePasswordRow {
|
||||
id: format!("entry:{text}"),
|
||||
path: text.to_owned(),
|
||||
title,
|
||||
detail,
|
||||
system_image: "key.fill".to_owned(),
|
||||
kind: MobilePasswordRowKind::Entry,
|
||||
}
|
||||
}
|
||||
|
||||
fn mobile_row(node: &TreeNode) -> MobilePasswordRow {
|
||||
@@ -283,4 +345,57 @@ mod tests {
|
||||
assert_eq!(error.kind(), MobilePasswordErrorKind::DirectoryMissing);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_matches_entry_and_folder_names_and_preserves_location()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let temporary = tempdir()?;
|
||||
fs::create_dir_all(temporary.path().join("Personal/Finance"))?;
|
||||
fs::create_dir_all(temporary.path().join("Work/Finance"))?;
|
||||
fs::create_dir_all(temporary.path().join(".extensions"))?;
|
||||
fs::write(
|
||||
temporary.path().join("Personal/Finance/bank.gpg"),
|
||||
b"ciphertext",
|
||||
)?;
|
||||
fs::write(
|
||||
temporary.path().join("Work/Finance/bank.gpg"),
|
||||
b"ciphertext",
|
||||
)?;
|
||||
fs::write(temporary.path().join("Personal/mail.gpg"), b"ciphertext")?;
|
||||
fs::write(
|
||||
temporary.path().join(".extensions/leaked.gpg"),
|
||||
b"ciphertext",
|
||||
)?;
|
||||
let repository = Repository::open(temporary.path())?;
|
||||
|
||||
let folder = MobilePasswordPage::search_repository(&repository, "FINANCE")?;
|
||||
assert_eq!(
|
||||
folder
|
||||
.rows()
|
||||
.iter()
|
||||
.map(|row| (row.title(), row.detail(), row.path()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
("bank", "Personal/Finance", "Personal/Finance/bank"),
|
||||
("bank", "Work/Finance", "Work/Finance/bank"),
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
MobilePasswordPage::search_repository(&repository, "mail")?
|
||||
.rows()
|
||||
.iter()
|
||||
.any(|row| row.path() == "Personal/mail")
|
||||
);
|
||||
assert!(
|
||||
MobilePasswordPage::search_repository(&repository, " ")?
|
||||
.rows()
|
||||
.is_empty()
|
||||
);
|
||||
assert!(
|
||||
MobilePasswordPage::search_repository(&repository, "leaked")?
|
||||
.rows()
|
||||
.is_empty()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,7 +715,7 @@ fn is_included_path(path: &Path, included: &[&str], directory: bool) -> bool {
|
||||
}) || included.iter().any(|candidate| *candidate == text)
|
||||
}
|
||||
|
||||
fn hidden_path(path: &Path) -> bool {
|
||||
pub(crate) fn hidden_path(path: &Path) -> bool {
|
||||
path.components()
|
||||
.any(|component| component.as_os_str() == EXTENSIONS_DIRECTORY)
|
||||
}
|
||||
|
||||
@@ -221,6 +221,10 @@ fn mobile_tab_defaults_and_persists_through_storage_configuration() -> TestResul
|
||||
fs::read_to_string(fixture.explicit_path())?.contains("selected_mobile_tab = \"totp\"")
|
||||
);
|
||||
|
||||
reloaded.update_mobile_tab(MobileTab::Search)?;
|
||||
let reloaded = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||
assert_eq!(reloaded.mobile_tab(), MobileTab::Search);
|
||||
|
||||
fixture.write_explicit(&format!(
|
||||
"{}\n[ui]\nselected_mobile_tab = \"unknown\"\n",
|
||||
fixture.valid_contents()
|
||||
|
||||
Reference in New Issue
Block a user