Skip to content

Commit

Permalink
Merge pull request #22 from frzi/fix/invalid-regex-patterns
Browse files Browse the repository at this point in the history
Fix/invalid regex patterns
  • Loading branch information
frzi authored Aug 7, 2021
2 parents 1f57344 + 1c0bad1 commit dfad2c7
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 19 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ Route(path: "user/:id?") { info in
A view that will only render its contents if its path matches that of the environment. Use `/*` to also match deeper paths. E.g.: the path `news/*` will match the following environment paths: `/news`, `/news/latest`, `/news/article/1` etc.

#### Parameters
Paths can contain parameters (aka variables) that can be read individually. A parameter's name is prefixed with a colon (`:`). Additionally, a parameter can be considered optional by suffixing it with a question mark (`?`). The parameters are passed down as a `[String : String]` in an `RouteInformation` object to a `Route`'s contents.
Paths can contain parameters (aka placeholders) that can be read individually. A parameter's name is prefixed with a colon (`:`). Additionally, a parameter can be considered optional by suffixing it with a question mark (`?`). The parameters are passed down as a `[String : String]` in an `RouteInformation` object to a `Route`'s contents.
**Note**: Parameters may only exist of alphanumeric characters (A-Z, a-z and 0-9) and *must* start with a letter.

#### Parameter validation
```swift
Expand Down Expand Up @@ -141,4 +142,4 @@ This object is passed down by default in a `Route` to its contents. It's also ac
<br>

## License 📄
[MIT License](LICENSE).
[MIT License](LICENSE).
64 changes: 48 additions & 16 deletions Sources/Route.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ import SwiftUI
/// }
/// ```
///
/// ## Path parameters (aka placeholders)
/// Paths may contain one or several parameters. Parameters are placeholders that will be replaced by the
/// corresponding component of the matching path. Parameters are prefixed with a colon (:). The values of the
/// parameters are provided via the `RouteInformation` object passed to the contents of the `Route`.
/// Parameters can be marked as optional by postfixing them with a question mark (?).
///
/// **Note:** Only alphanumeric characters (A-Z, a-z, 0-9) are valid for parameters.
/// ```swift
/// Route("/news/:id") { routeInfo in
/// NewsItemView(id: routeInfo.parameters["id"]!)
/// }
/// ```
///
/// ## Validation and parameter transform
/// `Route`s are given the opportunity to add an extra layer of validation. Use the `validator` argument to pass
/// down a validator function. This function is given a `RouteInformation` object, containing the path parameters.
Expand Down Expand Up @@ -82,16 +95,21 @@ public struct Route<ValidatedData, Content: View>: View {
var routeInformation: RouteInformation?

if !switchEnvironment.isActive || (switchEnvironment.isActive && !switchEnvironment.isResolved) {
if let matchInformation = try? pathMatcher.match(glob: resolvedGlob, with: navigator.path),
let validated = validator(matchInformation)
{
validatedData = validated
routeInformation = matchInformation

if switchEnvironment.isActive {
switchEnvironment.isResolved = true
do {
if let matchInformation = try pathMatcher.match(glob: resolvedGlob, with: navigator.path),
let validated = validator(matchInformation)
{
validatedData = validated
routeInformation = matchInformation

if switchEnvironment.isActive {
switchEnvironment.isResolved = true
}
}
}
catch {
fatalError("Unable to compile path glob '\(path)' to Regex. Error: \(error)")
}
}

return Group {
Expand Down Expand Up @@ -155,6 +173,14 @@ final class PathMatcher: ObservableObject {
let matchRegex: NSRegularExpression
let parameters: Set<String>
}

private enum CompileError: Error {
case badParameter(String, culprit: String)
}

private static let variablesRegex = try! NSRegularExpression(pattern: #":([^\/\?]+)"#, options: [])

//

private var cached: CompiledRegex?

Expand All @@ -169,12 +195,21 @@ final class PathMatcher: ObservableObject {
var variables = Set<String>()

let nsrange = NSRange(glob.startIndex..<glob.endIndex, in: glob)
let variablesRegex = try NSRegularExpression(pattern: #":([^\/\?]+)"#, options: [])
let variableMatches = variablesRegex.matches(in: glob, options: [], range: nsrange)
let variableMatches = Self.variablesRegex.matches(in: glob, options: [], range: nsrange)

for match in variableMatches where match.numberOfRanges > 1 {
if let range = Range(match.range(at: 1), in: glob) {
variables.insert(String(glob[range]))
let variable = String(glob[range])

#if DEBUG
// In debug mode perform an extra check whether parameters contain invalid characters or
// whether the parameters starts with something besides a letter.
if let r = variable.range(of: "(^[^a-z]|[^a-z0-9])", options: [.regularExpression, .caseInsensitive]) {
throw CompileError.badParameter(variable, culprit: String(variable[r]))
}
#endif

variables.insert(variable)
}
}

Expand All @@ -201,7 +236,7 @@ final class PathMatcher: ObservableObject {

return cached!
}

func match(glob: String, with path: String) throws -> RouteInformation? {
let compiled = try compileRegex(glob)

Expand Down Expand Up @@ -236,9 +271,6 @@ final class PathMatcher: ObservableObject {

let resolvedGlob = String(path[range])

return RouteInformation(
path: resolvedGlob,
parameters: parameterValues
)
return RouteInformation(path: resolvedGlob, parameters: parameterValues)
}
}
18 changes: 17 additions & 1 deletion Tests/SwiftUIRouterTests/SwiftUIRouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ final class SwiftUIRouterTests: XCTestCase {
"/:id?",
"/:id1/:id2",
"/:id1/:id2?",
"/:id/*",
"/:Movie/*",
"/:i", // Single character.
]

for glob in goodGlobs {
Expand All @@ -157,6 +158,21 @@ final class SwiftUIRouterTests: XCTestCase {
"Glob \(glob) causes bad Regex."
)
}

// These bad globs should throw at Regex compilation.
let badGlobs: [String] = [
"/:0abc", // Starting with numerics.
"/:user-id", // Illegal characters.
"/:foo_bar",
"/:😀"
]

for glob in badGlobs {
XCTAssertThrowsError(
try pathMatcher.match(glob: glob, with: ""),
"Glob \(glob) should've thrown an error, but didn't."
)
}
}

/// Test the `Navigator.navigate()` method.
Expand Down

0 comments on commit dfad2c7

Please sign in to comment.