expo-tvos-search 1.5.0 → 1.5.2

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 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 React Native tvOS (Expo)
1
+ # expo-tvos-search
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/expo-tvos-search.svg)](https://www.npmjs.com/package/expo-tvos-search)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'ExpoTvosSearch'
3
- s.version = '1.5.0'
3
+ s.version = '1.5.2'
4
4
  s.summary = 'Native tvOS search view with SwiftUI .searchable modifier'
5
5
  s.description = 'Provides a native tvOS search experience using SwiftUI searchable modifier for proper focus and keyboard navigation'
6
6
  s.author = 'Keiver Hernandez'
@@ -148,11 +148,27 @@ public class ExpoTvosSearchModule: Module {
148
148
  }
149
149
 
150
150
  Prop("cardWidth") { (view: ExpoTvosSearchView, width: Double) in
151
- view.cardWidth = CGFloat(max(50, min(1000, width))) // Clamp to reasonable range
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
- view.cardHeight = CGFloat(max(50, min(1000, height))) // Clamp to reasonable range
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
- view.cardMargin = CGFloat(max(0, min(200, margin))) // Clamp to reasonable range
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
- view.cardPadding = CGFloat(max(0, min(100, padding))) // Clamp to reasonable range
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
- view.overlayTitleSize = CGFloat(max(8, min(72, size))) // Clamp to reasonable font size range
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 number of results to process to prevent memory exhaustion.
65
- private static let maxResultsCount = 500
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
- #if DEBUG
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
- validatedImageUrl = imageUrl
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",
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-tvos-search",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Native tvOS search view using SwiftUI .searchable modifier for Expo/React Native",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",