Skip to content

Commit df8eedf

Browse files
authored
feat: add base64 validation rule (#115)
1 parent b89b1c9 commit df8eedf

File tree

4 files changed

+243
-0
lines changed

4 files changed

+243
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ struct RegistrationView: View {
317317
| `IBANValidationRule` | Validates that a string is a valid IBAN (International Bank Account Number) | `IBANValidationRule(error: "Invalid IBAN")`
318318
| `IPAddressValidationRule` | Validates that a string is a valid IPv4 or IPv6 address | `IPAddressValidationRule(version: .v4, error: ValidationError("Invalid IPv4"))`
319319
| `PostalCodeValidationRule` | Validates postal/ZIP codes for different countries | `PostalCodeValidationRule(country: .uk, error: "Invalid post code")`
320+
| `Base64ValidationRule` | | `Base64ValidationRule(error: "The input is not valid Base64.")`
320321

321322
## Custom Validators
322323

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// Validator
3+
// Copyright © 2026 Space Code. All rights reserved.
4+
//
5+
6+
import Foundation
7+
8+
/// Validates that a string represents valid Base64-encoded data.
9+
///
10+
/// # Example:
11+
/// ```swift
12+
/// let rule = Base64ValidationRule(error: "Invalid Base64")
13+
/// rule.validate(input: "SGVsbG8gV29ybGQ=") // true
14+
/// rule.validate(input: "not_base64!") // false
15+
/// ```
16+
public struct Base64ValidationRule: IValidationRule {
17+
// MARK: Types
18+
19+
public typealias Input = String
20+
21+
// MARK: Properties
22+
23+
/// The validation error returned if the input is not valid Base64.
24+
public let error: IValidationError
25+
26+
// MARK: Initialization
27+
28+
/// Initializes a Base64 validation rule.
29+
///
30+
/// - Parameter error: The validation error returned if input fails validation.
31+
public init(error: IValidationError) {
32+
self.error = error
33+
}
34+
35+
// MARK: IValidationRule
36+
37+
public func validate(input: String) -> Bool {
38+
guard !input.isEmpty else { return false }
39+
40+
let cleanedInput = input.replacingOccurrences(of: "\\s", with: "", options: .regularExpression)
41+
42+
guard !cleanedInput.isEmpty else { return false }
43+
44+
guard cleanedInput.count.isMultiple(of: 4) else { return false }
45+
46+
guard Data(base64Encoded: cleanedInput) != nil else { return false }
47+
48+
let base64Pattern = "^[A-Za-z0-9+/]*={0,2}$"
49+
let predicate = NSPredicate(format: "SELF MATCHES %@", base64Pattern)
50+
return predicate.evaluate(with: cleanedInput)
51+
}
52+
}

Sources/ValidatorCore/Validator.docc/Overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ ValidatorCore contains all core validation rules, utilities, and mechanisms for
4040
- ``IBANValidationRule``
4141
- ``IPAddressValidationRule``
4242
- ``PostalCodeValidationRule``
43+
- ``Base64ValidationRule``
4344

4445
### Articles
4546

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//
2+
// Validator
3+
// Copyright © 2026 Space Code. All rights reserved.
4+
//
5+
6+
import ValidatorCore
7+
import XCTest
8+
9+
// MARK: - Base64ValidationRuleTests
10+
11+
final class Base64ValidationRuleTests: XCTestCase {
12+
// MARK: - Properties
13+
14+
private var sut: Base64ValidationRule!
15+
16+
// MARK: - Setup
17+
18+
override func setUp() {
19+
super.setUp()
20+
sut = Base64ValidationRule(error: String.error)
21+
}
22+
23+
override func tearDown() {
24+
sut = nil
25+
super.tearDown()
26+
}
27+
28+
// MARK: - Tests
29+
30+
func test_validate_validBase64_shouldReturnTrue() {
31+
// given
32+
let base64 = "SGVsbG8gV29ybGQ="
33+
34+
// when
35+
let result = sut.validate(input: base64)
36+
37+
// then
38+
XCTAssertTrue(result)
39+
}
40+
41+
func test_validate_validBase64WithoutPadding_shouldReturnTrue() {
42+
// given
43+
let base64 = "SGVsbG8="
44+
45+
// when
46+
let result = sut.validate(input: base64)
47+
48+
// then
49+
XCTAssertTrue(result)
50+
}
51+
52+
func test_validate_validBase64NoPadding_shouldReturnTrue() {
53+
// given
54+
let base64 = "YWJj"
55+
56+
// when
57+
let result = sut.validate(input: base64)
58+
59+
// then
60+
XCTAssertTrue(result)
61+
}
62+
63+
func test_validate_validBase64WithPlus_shouldReturnTrue() {
64+
// given
65+
let base64 = "YWJjZGVmZ2hpamts+A=="
66+
67+
// when
68+
let result = sut.validate(input: base64)
69+
70+
// then
71+
XCTAssertTrue(result)
72+
}
73+
74+
func test_validate_validBase64WithSlash_shouldReturnTrue() {
75+
// given
76+
let base64 = "YWJjZGVmZ2hpamts/w=="
77+
78+
// when
79+
let result = sut.validate(input: base64)
80+
81+
// then
82+
XCTAssertTrue(result)
83+
}
84+
85+
func test_validate_invalidCharacters_shouldReturnFalse() {
86+
// given
87+
let base64 = "Hello@World!"
88+
89+
// when
90+
let result = sut.validate(input: base64)
91+
92+
// then
93+
XCTAssertFalse(result)
94+
}
95+
96+
func test_validate_invalidLength_shouldReturnFalse() {
97+
// given
98+
let base64 = "SGVsbG"
99+
100+
// when
101+
let result = sut.validate(input: base64)
102+
103+
// then
104+
XCTAssertFalse(result)
105+
}
106+
107+
func test_validate_tooManyPaddingCharacters_shouldReturnFalse() {
108+
// given
109+
let base64 = "SGVsbG8==="
110+
111+
// when
112+
let result = sut.validate(input: base64)
113+
114+
// then
115+
XCTAssertFalse(result)
116+
}
117+
118+
func test_validate_emptyString_shouldReturnFalse() {
119+
// given
120+
let base64 = ""
121+
122+
// when
123+
let result = sut.validate(input: base64)
124+
125+
// then
126+
XCTAssertFalse(result)
127+
}
128+
129+
func test_validate_whitespaceString_shouldReturnFalse() {
130+
// given
131+
let base64 = " "
132+
133+
// when
134+
let result = sut.validate(input: base64)
135+
136+
// then
137+
XCTAssertFalse(result)
138+
}
139+
140+
func test_validate_plainText_shouldReturnFalse() {
141+
// given
142+
let base64 = "not base64"
143+
144+
// when
145+
let result = sut.validate(input: base64)
146+
147+
// then
148+
XCTAssertFalse(result)
149+
}
150+
151+
func test_validate_base64WithWhitespace_shouldReturnTrue() {
152+
// given
153+
let base64 = "SGVs bG8g V29y bGQ="
154+
155+
// when
156+
let result = sut.validate(input: base64)
157+
158+
// then
159+
XCTAssertTrue(result)
160+
}
161+
162+
func test_validate_base64WithNewlines_shouldReturnTrue() {
163+
// given
164+
let base64 = "SGVs\nbG8g\nV29y\nbGQ="
165+
166+
// when
167+
let result = sut.validate(input: base64)
168+
169+
// then
170+
XCTAssertTrue(result)
171+
}
172+
173+
func test_validate_paddingInMiddle_shouldReturnFalse() {
174+
// given
175+
let base64 = "SGVs=bG8="
176+
177+
// when
178+
let result = sut.validate(input: base64)
179+
180+
// then
181+
XCTAssertFalse(result)
182+
}
183+
}
184+
185+
// MARK: Constants
186+
187+
private extension String {
188+
static let error = "Base64 string is invalid"
189+
}

0 commit comments

Comments
 (0)