Match any path component in StrictDomain FilterMode
This commit is contained in:
parent
b633cc1f3d
commit
d6db10e089
5 changed files with 86 additions and 16 deletions
|
@ -121,6 +121,7 @@ dependencies {
|
||||||
|
|
||||||
// Testing-only dependencies
|
// Testing-only dependencies
|
||||||
androidTestImplementation deps.testing.junit
|
androidTestImplementation deps.testing.junit
|
||||||
|
androidTestImplementation deps.testing.kotlin_test_junit
|
||||||
androidTestImplementation deps.testing.androidx.runner
|
androidTestImplementation deps.testing.androidx.runner
|
||||||
androidTestImplementation deps.testing.androidx.rules
|
androidTestImplementation deps.testing.androidx.rules
|
||||||
androidTestImplementation deps.testing.androidx.junit
|
androidTestImplementation deps.testing.androidx.junit
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
package com.zeapo.pwdstore
|
||||||
|
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import org.junit.Test as test
|
||||||
|
|
||||||
|
private infix fun String.matchedForDomain(domain: String) =
|
||||||
|
SearchableRepositoryViewModel.generateStrictDomainRegex(domain)?.containsMatchIn(this) == true
|
||||||
|
|
||||||
|
class StrictDomainRegexTest {
|
||||||
|
@test fun acceptsLiteralDomain() {
|
||||||
|
assertTrue("work/example.org/john.doe@example.org.gpg" matchedForDomain "example.org")
|
||||||
|
assertTrue("example.org/john.doe@example.org.gpg" matchedForDomain "example.org")
|
||||||
|
assertTrue("example.org.gpg" matchedForDomain "example.org")
|
||||||
|
}
|
||||||
|
|
||||||
|
@test fun acceptsSubdomains() {
|
||||||
|
assertTrue("work/www.example.org/john.doe@example.org.gpg" matchedForDomain "example.org")
|
||||||
|
assertTrue("www2.example.org/john.doe@example.org.gpg" matchedForDomain "example.org")
|
||||||
|
assertTrue("www.login.example.org.gpg" matchedForDomain "example.org")
|
||||||
|
}
|
||||||
|
|
||||||
|
@test fun rejectsPhishingAttempts() {
|
||||||
|
assertFalse("example.org.gpg" matchedForDomain "xample.org")
|
||||||
|
assertFalse("login.example.org.gpg" matchedForDomain "xample.org")
|
||||||
|
assertFalse("example.org/john.doe@exmple.org.gpg" matchedForDomain "xample.org")
|
||||||
|
assertFalse("example.org.gpg" matchedForDomain "e/xample.org")
|
||||||
|
}
|
||||||
|
|
||||||
|
@test fun rejectNonGpgComponentMatches() {
|
||||||
|
assertFalse("work/example.org" matchedForDomain "example.org")
|
||||||
|
}
|
||||||
|
|
||||||
|
@test fun rejectsEmailAddresses() {
|
||||||
|
assertFalse("work/notexample.org/john.doe@example.org.gpg" matchedForDomain "example.org")
|
||||||
|
assertFalse("work/notexample.org/john.doe@www.example.org.gpg" matchedForDomain "example.org")
|
||||||
|
assertFalse("work/john.doe@www.example.org/foo.org" matchedForDomain "example.org")
|
||||||
|
}
|
||||||
|
|
||||||
|
@test fun rejectsPathSeparators() {
|
||||||
|
assertNull(SearchableRepositoryViewModel.generateStrictDomainRegex("ex/ample.org"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -126,6 +126,28 @@ enum class ListMode {
|
||||||
@FlowPreview
|
@FlowPreview
|
||||||
class SearchableRepositoryViewModel(application: Application) : AndroidViewModel(application) {
|
class SearchableRepositoryViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun generateStrictDomainRegex(domain: String): Regex? {
|
||||||
|
// Valid domains do not contain path separators.
|
||||||
|
if (domain.contains('/'))
|
||||||
|
return null
|
||||||
|
// Matches the start of a path component, which is either the start of the
|
||||||
|
// string or a path separator.
|
||||||
|
val prefix = """(?:^|/)"""
|
||||||
|
val escapedFilter = Regex.escape(domain.replace("/", ""))
|
||||||
|
// Matches either the filter literally or a strict subdomain of the filter term.
|
||||||
|
// We allow a lot of freedom in what a subdomain is, as long as it is not an
|
||||||
|
// email address.
|
||||||
|
val subdomain = """(?:(?:[^/@]+\.)?$escapedFilter)"""
|
||||||
|
// Matches the end of a path component, which is either the literal ".gpg" or a
|
||||||
|
// path separator.
|
||||||
|
val suffix = """(?:\.gpg|/)"""
|
||||||
|
// Match any relative path with a component that is a subdomain of the filter.
|
||||||
|
return Regex(prefix + subdomain + suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var _updateCounter = 0
|
private var _updateCounter = 0
|
||||||
private val updateCounter: Int
|
private val updateCounter: Int
|
||||||
get() = _updateCounter
|
get() = _updateCounter
|
||||||
|
@ -219,22 +241,18 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||||
}
|
}
|
||||||
FilterMode.StrictDomain -> {
|
FilterMode.StrictDomain -> {
|
||||||
check(searchAction.listMode == ListMode.FilesOnly) { "Searches with StrictDomain search mode can only list files" }
|
check(searchAction.listMode == ListMode.FilesOnly) { "Searches with StrictDomain search mode can only list files" }
|
||||||
|
val regex = generateStrictDomainRegex(searchAction.filter)
|
||||||
|
if (regex != null) {
|
||||||
prefilteredResultFlow
|
prefilteredResultFlow
|
||||||
.filter { absoluteFile ->
|
.filter { absoluteFile ->
|
||||||
val file = absoluteFile.relativeTo(root)
|
regex.containsMatchIn(absoluteFile.relativeTo(root).path)
|
||||||
val toMatch =
|
|
||||||
directoryStructure.getIdentifierFor(file) ?: return@filter false
|
|
||||||
// In strict domain mode, we match
|
|
||||||
// * the search term exactly,
|
|
||||||
// * subdomains of the search term,
|
|
||||||
// * or the search term plus an arbitrary protocol.
|
|
||||||
toMatch == searchAction.filter ||
|
|
||||||
toMatch.endsWith(".${searchAction.filter}") ||
|
|
||||||
toMatch.endsWith("://${searchAction.filter}")
|
|
||||||
}
|
}
|
||||||
.map { it.toPasswordItem(root) }
|
.map { it.toPasswordItem(root) }
|
||||||
.toList()
|
.toList()
|
||||||
.sortedWith(itemComparator)
|
.sortedWith(itemComparator)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FilterMode.Fuzzy -> {
|
FilterMode.Fuzzy -> {
|
||||||
prefilteredResultFlow
|
prefilteredResultFlow
|
||||||
|
|
|
@ -50,6 +50,8 @@ subprojects {
|
||||||
targetSdkVersion versions.targetSdk
|
targetSdkVersion versions.targetSdk
|
||||||
versionCode versions.versionCode
|
versionCode versions.versionCode
|
||||||
versionName versions.versionName
|
versionName versions.versionName
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
|
|
@ -67,6 +67,7 @@ ext.deps = [
|
||||||
|
|
||||||
testing: [
|
testing: [
|
||||||
junit: 'junit:junit:4.13',
|
junit: 'junit:junit:4.13',
|
||||||
|
kotlin_test_junit: 'org.jetbrains.kotlin:kotlin-test-junit:1.3.71',
|
||||||
androidx: [
|
androidx: [
|
||||||
runner: 'androidx.test:runner:1.3.0-alpha05',
|
runner: 'androidx.test:runner:1.3.0-alpha05',
|
||||||
rules: 'androidx.test:rules:1.3.0-alpha05',
|
rules: 'androidx.test:rules:1.3.0-alpha05',
|
||||||
|
|
Loading…
Reference in a new issue