expo-tvos-search 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/ios/ExpoTvosSearchModule.swift +45 -5
- package/ios/ExpoTvosSearchView.swift +12 -19
- package/ios/MarqueeText.swift +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.1] - 2026-02-01
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `cardWidth`, `cardHeight`, `cardMargin`, `cardPadding`, and `overlayTitleSize` now emit `onValidationWarning` when values are clamped, consistent with `columns`, `topInset`, and `marqueeDelay`
|
|
12
|
+
- Data URIs exceeding 1 MB are now rejected with a validation warning to prevent memory exhaustion
|
|
13
|
+
- Removed redundant results array truncation in the view layer (module already enforces the 500-item limit)
|
|
14
|
+
- Added defensive overflow protection in marquee animation sleep
|
|
15
|
+
|
|
8
16
|
## [1.5.0] - 2026-02-01
|
|
9
17
|
|
|
10
18
|
### Added
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# expo-tvos-search — Native tvOS Search for
|
|
1
|
+
# expo-tvos-search — Native tvOS Search Component for Expo
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/expo-tvos-search)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -148,11 +148,27 @@ public class ExpoTvosSearchModule: Module {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
Prop("cardWidth") { (view: ExpoTvosSearchView, width: Double) in
|
|
151
|
-
|
|
151
|
+
let clampedValue = min(max(50, width), 1000)
|
|
152
|
+
if clampedValue != width {
|
|
153
|
+
view.onValidationWarning([
|
|
154
|
+
"type": "value_clamped",
|
|
155
|
+
"message": "cardWidth value \(width) was clamped to range [50, 1000]",
|
|
156
|
+
"context": "cardWidth=\(clampedValue)"
|
|
157
|
+
])
|
|
158
|
+
}
|
|
159
|
+
view.cardWidth = CGFloat(clampedValue)
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
Prop("cardHeight") { (view: ExpoTvosSearchView, height: Double) in
|
|
155
|
-
|
|
163
|
+
let clampedValue = min(max(50, height), 1000)
|
|
164
|
+
if clampedValue != height {
|
|
165
|
+
view.onValidationWarning([
|
|
166
|
+
"type": "value_clamped",
|
|
167
|
+
"message": "cardHeight value \(height) was clamped to range [50, 1000]",
|
|
168
|
+
"context": "cardHeight=\(clampedValue)"
|
|
169
|
+
])
|
|
170
|
+
}
|
|
171
|
+
view.cardHeight = CGFloat(clampedValue)
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
Prop("imageContentMode") { (view: ExpoTvosSearchView, mode: String) in
|
|
@@ -160,15 +176,39 @@ public class ExpoTvosSearchModule: Module {
|
|
|
160
176
|
}
|
|
161
177
|
|
|
162
178
|
Prop("cardMargin") { (view: ExpoTvosSearchView, margin: Double) in
|
|
163
|
-
|
|
179
|
+
let clampedValue = min(max(0, margin), 200)
|
|
180
|
+
if clampedValue != margin {
|
|
181
|
+
view.onValidationWarning([
|
|
182
|
+
"type": "value_clamped",
|
|
183
|
+
"message": "cardMargin value \(margin) was clamped to range [0, 200]",
|
|
184
|
+
"context": "cardMargin=\(clampedValue)"
|
|
185
|
+
])
|
|
186
|
+
}
|
|
187
|
+
view.cardMargin = CGFloat(clampedValue)
|
|
164
188
|
}
|
|
165
189
|
|
|
166
190
|
Prop("cardPadding") { (view: ExpoTvosSearchView, padding: Double) in
|
|
167
|
-
|
|
191
|
+
let clampedValue = min(max(0, padding), 100)
|
|
192
|
+
if clampedValue != padding {
|
|
193
|
+
view.onValidationWarning([
|
|
194
|
+
"type": "value_clamped",
|
|
195
|
+
"message": "cardPadding value \(padding) was clamped to range [0, 100]",
|
|
196
|
+
"context": "cardPadding=\(clampedValue)"
|
|
197
|
+
])
|
|
198
|
+
}
|
|
199
|
+
view.cardPadding = CGFloat(clampedValue)
|
|
168
200
|
}
|
|
169
201
|
|
|
170
202
|
Prop("overlayTitleSize") { (view: ExpoTvosSearchView, size: Double) in
|
|
171
|
-
|
|
203
|
+
let clampedValue = min(max(8, size), 72)
|
|
204
|
+
if clampedValue != size {
|
|
205
|
+
view.onValidationWarning([
|
|
206
|
+
"type": "value_clamped",
|
|
207
|
+
"message": "overlayTitleSize value \(size) was clamped to range [8, 72]",
|
|
208
|
+
"context": "overlayTitleSize=\(clampedValue)"
|
|
209
|
+
])
|
|
210
|
+
}
|
|
211
|
+
view.overlayTitleSize = CGFloat(clampedValue)
|
|
172
212
|
}
|
|
173
213
|
}
|
|
174
214
|
}
|
|
@@ -61,8 +61,8 @@ class ExpoTvosSearchView: ExpoView {
|
|
|
61
61
|
/// Maximum length for string fields (id, title, subtitle) to prevent memory issues.
|
|
62
62
|
private static let maxStringFieldLength = 500
|
|
63
63
|
|
|
64
|
-
/// Maximum
|
|
65
|
-
private static let
|
|
64
|
+
/// Maximum length for data: URIs to prevent memory exhaustion (~750KB decoded).
|
|
65
|
+
private static let maxDataUrlLength = 1_000_000
|
|
66
66
|
|
|
67
67
|
// Track if we've disabled RN gesture handlers for keyboard input
|
|
68
68
|
private var gestureHandlersDisabled = false
|
|
@@ -410,23 +410,13 @@ class ExpoTvosSearchView: ExpoView {
|
|
|
410
410
|
}
|
|
411
411
|
|
|
412
412
|
func updateResults(_ results: [[String: Any]]) {
|
|
413
|
-
// Limit results to prevent memory exhaustion from malicious/buggy input
|
|
414
|
-
let limitedResults = results.prefix(Self.maxResultsCount)
|
|
415
|
-
let truncatedResultsCount = results.count - limitedResults.count
|
|
416
|
-
|
|
417
413
|
var validResults: [SearchResultItem] = []
|
|
418
414
|
var skippedCount = 0
|
|
419
415
|
var urlValidationFailures = 0
|
|
420
416
|
var httpUrlCount = 0
|
|
421
417
|
var truncatedFields = 0
|
|
422
418
|
|
|
423
|
-
|
|
424
|
-
if truncatedResultsCount > 0 {
|
|
425
|
-
print("[expo-tvos-search] Truncated \(truncatedResultsCount) results (max \(Self.maxResultsCount) allowed)")
|
|
426
|
-
}
|
|
427
|
-
#endif
|
|
428
|
-
|
|
429
|
-
for (index, dict) in limitedResults.enumerated() {
|
|
419
|
+
for (index, dict) in results.enumerated() {
|
|
430
420
|
// Validate required fields
|
|
431
421
|
guard let id = dict["id"] as? String, !id.isEmpty else {
|
|
432
422
|
skippedCount += 1
|
|
@@ -451,7 +441,15 @@ class ExpoTvosSearchView: ExpoView {
|
|
|
451
441
|
if let url = URL(string: imageUrl),
|
|
452
442
|
let scheme = url.scheme?.lowercased(),
|
|
453
443
|
scheme == "http" || scheme == "https" || scheme == "data" {
|
|
454
|
-
|
|
444
|
+
// Reject oversized data URIs to prevent memory exhaustion
|
|
445
|
+
if scheme == "data" && imageUrl.count > Self.maxDataUrlLength {
|
|
446
|
+
urlValidationFailures += 1
|
|
447
|
+
#if DEBUG
|
|
448
|
+
print("[expo-tvos-search] Result '\(title)' (id: '\(id)'): data URL too large (\(imageUrl.count) chars, max \(Self.maxDataUrlLength)). Skipped.")
|
|
449
|
+
#endif
|
|
450
|
+
} else {
|
|
451
|
+
validatedImageUrl = imageUrl
|
|
452
|
+
}
|
|
455
453
|
// Warn about insecure HTTP URLs (HTTPS recommended)
|
|
456
454
|
if scheme == "http" {
|
|
457
455
|
httpUrlCount += 1
|
|
@@ -513,11 +511,6 @@ class ExpoTvosSearchView: ExpoView {
|
|
|
513
511
|
#endif
|
|
514
512
|
|
|
515
513
|
// Emit validation warnings for production monitoring
|
|
516
|
-
if truncatedResultsCount > 0 {
|
|
517
|
-
emitWarning(type: "results_truncated",
|
|
518
|
-
message: "Truncated \(truncatedResultsCount) result(s) exceeding maximum of \(Self.maxResultsCount)",
|
|
519
|
-
context: "Consider implementing pagination")
|
|
520
|
-
}
|
|
521
514
|
if skippedCount > 0 {
|
|
522
515
|
emitWarning(type: "validation_failed",
|
|
523
516
|
message: "Skipped \(skippedCount) result(s) due to missing required fields",
|
package/ios/MarqueeText.swift
CHANGED
|
@@ -87,7 +87,7 @@ struct MarqueeText: View {
|
|
|
87
87
|
.task(id: shouldAnimate) {
|
|
88
88
|
if shouldAnimate {
|
|
89
89
|
do {
|
|
90
|
-
try await Task.sleep(nanoseconds: UInt64(startDelay * 1_000_000_000))
|
|
90
|
+
try await Task.sleep(nanoseconds: UInt64(min(startDelay, 60.0) * 1_000_000_000))
|
|
91
91
|
} catch {
|
|
92
92
|
return
|
|
93
93
|
}
|