Skip to content

Commit 9775b7d

Browse files
authored
feat: add ip address validation rule (#89)
1 parent cbf8d2e commit 9775b7d

File tree

5 files changed

+446
-0
lines changed

5 files changed

+446
-0
lines changed

.swiftlint.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ identifier_name:
9797
excluded:
9898
- id
9999
- URL
100+
- ip
101+
- v6
102+
- v4
100103

101104
analyzer_rules:
102105
- unused_import

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ struct RegistrationView: View {
315315
| `EqualityValidationRule`| Validates that the input is equal to a given reference value | `EqualityValidationRule(compareTo: password, error: "Passwords do not match")`
316316
| `ComparisonValidationRule` | Validates that input against a comparison constraint | `ComparisonValidationRule(greaterThan: 0, error: "Must be greater than 0")`
317317
| `IBANValidationRule` | Validates that a string is a valid IBAN (International Bank Account Number) | `IBANValidationRule(error: "Invalid IBAN")`
318+
| `IPAddressValidationRule` | Validates that a string is a valid IPv4 or IPv6 address | `IPAddressValidationRule(version: .v4, error: ValidationError("Invalid IPv4"))`
318319

319320
## Custom Validators
320321

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//
2+
// Validator
3+
// Copyright © 2025 Space Code. All rights reserved.
4+
//
5+
6+
import Foundation
7+
8+
/// A validation rule for checking whether the input string is a valid IPv4 or IPv6 address.
9+
///
10+
/// This rule supports:
11+
/// - Standard IPv4 addresses (e.g., `192.168.1.1`)
12+
/// - Standard and compressed IPv6 addresses (e.g., `2001:db8::1`)
13+
///
14+
/// The rule fails validation if:
15+
/// - The input is empty
16+
/// - The input contains spaces, tabs, or newlines
17+
/// - The format does not match the expected IPv4/IPv6 specification
18+
///
19+
/// Example:
20+
/// ```swift
21+
/// let rule = IPAddressValidationRule(version: .v4, error: ValidationError("Invalid IPv4"))
22+
/// rule.validate(input: "192.168.1.1") // true
23+
/// ```
24+
public struct IPAddressValidationRule: IValidationRule {
25+
// MARK: Types
26+
27+
public typealias Input = String
28+
29+
/// Specifies which IP protocol version the rule should validate.
30+
public enum Version {
31+
case v4
32+
case v6
33+
}
34+
35+
// MARK: Properties
36+
37+
/// The expected IP standard (IPv4 or IPv6).
38+
public let version: Version
39+
40+
/// The validation error returned when validation fails.
41+
public let error: IValidationError
42+
43+
// MARK: Initialization
44+
45+
/// Creates an IP address validation rule.
46+
///
47+
/// - Parameters:
48+
/// - version: The expected IP protocol version (.v4 or .v6).
49+
/// - error: The validation error returned if validation fails.
50+
public init(version: Version, error: IValidationError) {
51+
self.version = version
52+
self.error = error
53+
}
54+
55+
// MARK: IValidationRule
56+
57+
public func validate(input: String) -> Bool {
58+
if input.isEmpty { return false }
59+
60+
if input.contains(" ") || input.contains("\t") || input.contains("\n") {
61+
return false
62+
}
63+
64+
switch version {
65+
case .v4:
66+
return validateIPv4(input)
67+
case .v6:
68+
return validateIPv6(input)
69+
}
70+
}
71+
72+
// MARK: Private
73+
74+
/// Validates an IPv4 address using a strict regex pattern that ensures each octet is between 0–255.
75+
///
76+
/// - Parameter input: The IPv4 string.
77+
///
78+
/// - Returns: `true` if valid, otherwise `false`.
79+
private func validateIPv4(_ input: String) -> Bool {
80+
let pattern = #"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\."#
81+
+ #"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\."#
82+
+ #"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\."#
83+
+ #"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"#
84+
85+
if let regex = try? NSRegularExpression(pattern: pattern) {
86+
let range = NSRange(location: 0, length: input.utf16.count)
87+
return regex.firstMatch(in: input, options: [], range: range) != nil
88+
}
89+
90+
return false
91+
}
92+
93+
/// Validates an IPv6 address using system-level parsing (`inet_pton`) and basic structural checks.
94+
///
95+
/// Supports:
96+
/// - Full IPv6 addresses
97+
/// - Compressed forms (e.g., `::1`)
98+
/// - IPv4-mapped IPv6 forms (e.g., `::ffff:192.168.0.1`)
99+
///
100+
/// - Parameter input: The IPv6 string.
101+
///
102+
/// - Returns: `true` if valid, otherwise `false`.
103+
private func validateIPv6(_ input: String) -> Bool {
104+
let components = input.components(separatedBy: ":")
105+
106+
for component in components where !component.isEmpty {
107+
if component.contains(".") {
108+
continue
109+
}
110+
111+
if component.count > 4 {
112+
return false
113+
}
114+
115+
if !component.allSatisfy(\.isHexDigit) {
116+
return false
117+
}
118+
}
119+
120+
var addr = sockaddr_in6()
121+
return input.withCString { cString in
122+
inet_pton(AF_INET6, cString, &addr.sin6_addr) == 1
123+
}
124+
}
125+
}

Sources/ValidatorCore/Validator.docc/Overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ ValidatorCore contains all core validation rules, utilities, and mechanisms for
3838
- ``EqualityValidationRule``
3939
- ``ComparisonValidationRule``
4040
- ``IBANValidationRule``
41+
- ``IPAddressValidationRule``
4142

4243
### Articles
4344

0 commit comments

Comments
 (0)