react-native-kookit 0.2.2 → 0.2.3

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.
@@ -68,9 +68,152 @@ enum FtpError: Error, LocalizedError {
68
68
  }
69
69
  }
70
70
 
71
- class FtpClient {
71
+ // Helper class to safely manage continuation resumption
72
+ private class ConnectionBox<T>: @unchecked Sendable {
73
+ private let continuation: CheckedContinuation<T, Error>
74
+ private var hasResumed = false
75
+ private let queue = DispatchQueue(label: "ConnectionBox")
76
+
77
+ init(continuation: CheckedContinuation<T, Error>) {
78
+ self.continuation = continuation
79
+ }
80
+
81
+ func resume(returning value: T) {
82
+ queue.sync {
83
+ guard !hasResumed else { return }
84
+ hasResumed = true
85
+ continuation.resume(returning: value)
86
+ }
87
+ }
88
+
89
+ func resume(throwing error: Error) {
90
+ queue.sync {
91
+ guard !hasResumed else { return }
92
+ hasResumed = true
93
+ continuation.resume(throwing: error)
94
+ }
95
+ }
96
+
97
+ func resume() where T == Void {
98
+ queue.sync {
99
+ guard !hasResumed else { return }
100
+ hasResumed = true
101
+ continuation.resume()
102
+ }
103
+ }
104
+ }
105
+
106
+ // Helper class specifically for NWConnection state handling
107
+ private class NWConnectionBox: @unchecked Sendable {
108
+ private let connection: NWConnection
109
+ private let continuation: CheckedContinuation<NWConnection, Error>
110
+ private var hasResumed = false
111
+ private let queue = DispatchQueue(label: "NWConnectionBox")
112
+
113
+ init(connection: NWConnection, continuation: CheckedContinuation<NWConnection, Error>) {
114
+ self.connection = connection
115
+ self.continuation = continuation
116
+ }
117
+
118
+ func handleStateUpdate(_ state: NWConnection.State) {
119
+ queue.sync {
120
+ guard !hasResumed else { return }
121
+
122
+ switch state {
123
+ case .ready:
124
+ hasResumed = true
125
+ continuation.resume(returning: connection)
126
+ case .failed(let error):
127
+ hasResumed = true
128
+ continuation.resume(throwing: error)
129
+ case .cancelled:
130
+ hasResumed = true
131
+ continuation.resume(throwing: FtpError.connectionCancelled)
132
+ default:
133
+ break
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ // Helper class for FTP connection authentication
140
+ private class FtpConnectionBox: @unchecked Sendable {
141
+ private let ftpClient: FtpClient
142
+ private let continuation: CheckedContinuation<Void, Error>
143
+ private var hasResumed = false
144
+ private let queue = DispatchQueue(label: "FtpConnectionBox")
145
+ private var timeoutTask: DispatchWorkItem?
146
+
147
+ init(ftpClient: FtpClient, continuation: CheckedContinuation<Void, Error>) {
148
+ self.ftpClient = ftpClient
149
+ self.continuation = continuation
150
+ }
151
+
152
+ func setTimeoutTask(_ task: DispatchWorkItem) {
153
+ self.timeoutTask = task
154
+ }
155
+
156
+ func handleStateUpdate(_ state: NWConnection.State) {
157
+ queue.async {
158
+ guard !self.hasResumed else { return }
159
+
160
+ switch state {
161
+ case .ready:
162
+ self.timeoutTask?.cancel()
163
+ Task {
164
+ do {
165
+ try await self.ftpClient.performAuthentication()
166
+ self.ftpClient.isConnected = true
167
+ self.queue.sync {
168
+ if !self.hasResumed {
169
+ self.hasResumed = true
170
+ self.continuation.resume()
171
+ }
172
+ }
173
+ } catch {
174
+ self.queue.sync {
175
+ if !self.hasResumed {
176
+ self.hasResumed = true
177
+ self.continuation.resume(throwing: error)
178
+ }
179
+ }
180
+ }
181
+ }
182
+ case .failed(let error):
183
+ self.timeoutTask?.cancel()
184
+ self.queue.sync {
185
+ if !self.hasResumed {
186
+ self.hasResumed = true
187
+ self.continuation.resume(throwing: error)
188
+ }
189
+ }
190
+ case .cancelled:
191
+ self.timeoutTask?.cancel()
192
+ self.queue.sync {
193
+ if !self.hasResumed {
194
+ self.hasResumed = true
195
+ self.continuation.resume(throwing: FtpError.connectionCancelled)
196
+ }
197
+ }
198
+ default:
199
+ break
200
+ }
201
+ }
202
+ }
203
+
204
+ func handleTimeout() {
205
+ queue.sync {
206
+ if !hasResumed {
207
+ hasResumed = true
208
+ continuation.resume(throwing: FtpError.connectionTimeout)
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ class FtpClient: @unchecked Sendable {
72
215
  private var controlConnection: NWConnection?
73
- private var isConnected = false
216
+ var isConnected = false
74
217
  private var config: FtpConnectionConfig?
75
218
  private weak var progressDelegate: FtpProgressDelegate?
76
219
 
@@ -84,36 +227,24 @@ class FtpClient {
84
227
  controlConnection = NWConnection(to: endpoint, using: .tcp)
85
228
 
86
229
  return try await withCheckedThrowingContinuation { continuation in
87
- controlConnection?.stateUpdateHandler = { state in
88
- switch state {
89
- case .ready:
90
- Task {
91
- do {
92
- try await self.performAuthentication()
93
- self.isConnected = true
94
- continuation.resume()
95
- } catch {
96
- continuation.resume(throwing: error)
97
- }
98
- }
99
- case .failed(let error):
100
- continuation.resume(throwing: error)
101
- case .cancelled:
102
- continuation.resume(throwing: FtpError.connectionCancelled)
103
- default:
104
- break
230
+ let connectionBox = FtpConnectionBox(ftpClient: self, continuation: continuation)
231
+
232
+ let timeoutTask = DispatchWorkItem {
233
+ if !self.isConnected {
234
+ self.controlConnection?.cancel()
105
235
  }
106
236
  }
107
237
 
238
+ connectionBox.setTimeoutTask(timeoutTask)
239
+
240
+ controlConnection?.stateUpdateHandler = { state in
241
+ connectionBox.handleStateUpdate(state)
242
+ }
243
+
108
244
  controlConnection?.start(queue: .global())
109
245
 
110
246
  // Set timeout
111
- DispatchQueue.global().asyncAfter(deadline: .now() + config.timeout) {
112
- if !self.isConnected {
113
- self.controlConnection?.cancel()
114
- continuation.resume(throwing: FtpError.connectionTimeout)
115
- }
116
- }
247
+ DispatchQueue.global().asyncAfter(deadline: .now() + config.timeout, execute: timeoutTask)
117
248
  }
118
249
  }
119
250
 
@@ -133,26 +264,48 @@ class FtpClient {
133
264
 
134
265
  func listFiles(path: String? = nil) async throws -> [FtpFileInfo] {
135
266
  guard isConnected else { throw FtpError.notConnected }
136
-
267
+
268
+ // Set ASCII mode for directory listings
269
+ print("FTP DEBUG: Setting ASCII mode for directory listing")
270
+ let asciiResponse = try await sendCommand("TYPE A")
271
+ print("FTP DEBUG: ASCII mode response: \(asciiResponse)")
272
+ if !asciiResponse.hasPrefix("200") {
273
+ print("FTP DEBUG: Warning - Failed to set ASCII mode: \(asciiResponse)")
274
+ }
275
+
137
276
  let dataConnection = try await enterPassiveMode()
138
-
277
+
139
278
  let command = path != nil ? "LIST \(path!)" : "LIST"
279
+ print("FTP DEBUG: Sending command: \(command)")
140
280
  let response = try await sendCommand(command)
141
-
281
+ print("FTP DEBUG: LIST command response: \(response)")
282
+
142
283
  guard response.hasPrefix("150") || response.hasPrefix("125") else {
143
284
  dataConnection.cancel()
144
285
  throw FtpError.commandFailed("LIST failed: \(response)")
145
286
  }
146
-
287
+
147
288
  let data = try await receiveData(from: dataConnection)
289
+ print("FTP DEBUG: Received data length: \(data.count) bytes")
290
+ if let dataString = String(data: data, encoding: .utf8) {
291
+ print("FTP DEBUG: Received data content: '\(dataString)'")
292
+ }
148
293
  dataConnection.cancel()
149
-
294
+
150
295
  let finalResponse = try await readResponse()
296
+ print("FTP DEBUG: Final response: \(finalResponse)")
151
297
  guard finalResponse.hasPrefix("226") else {
152
298
  throw FtpError.commandFailed("LIST completion failed: \(finalResponse)")
153
299
  }
300
+
301
+ let files = parseListingData(data)
302
+ print("FTP DEBUG: Parsed \(files.count) files")
303
+
304
+ // Reset to binary mode for file transfers
305
+ let binaryResponse = try await sendCommand("TYPE I")
306
+ print("FTP DEBUG: Reset to binary mode response: \(binaryResponse)")
154
307
 
155
- return parseListingData(data)
308
+ return files
156
309
  }
157
310
 
158
311
  func downloadFile(remotePath: String, localPath: String) async throws {
@@ -268,7 +421,7 @@ class FtpClient {
268
421
 
269
422
  // MARK: - Private Methods
270
423
 
271
- private func performAuthentication() async throws {
424
+ func performAuthentication() async throws {
272
425
  // Read welcome message
273
426
  let welcome = try await readResponse()
274
427
  guard welcome.hasPrefix("220") else {
@@ -304,16 +457,18 @@ class FtpClient {
304
457
  let commandData = "\(command)\r\n".data(using: .utf8)!
305
458
 
306
459
  return try await withCheckedThrowingContinuation { continuation in
460
+ let connectionBox = ConnectionBox<String>(continuation: continuation)
461
+
307
462
  connection.send(content: commandData, completion: .contentProcessed { error in
308
463
  if let error = error {
309
- continuation.resume(throwing: error)
464
+ connectionBox.resume(throwing: error)
310
465
  } else {
311
466
  Task {
312
467
  do {
313
468
  let response = try await self.readResponse()
314
- continuation.resume(returning: response)
469
+ connectionBox.resume(returning: response)
315
470
  } catch {
316
- continuation.resume(throwing: error)
471
+ connectionBox.resume(throwing: error)
317
472
  }
318
473
  }
319
474
  }
@@ -327,13 +482,35 @@ class FtpClient {
327
482
  }
328
483
 
329
484
  return try await withCheckedThrowingContinuation { continuation in
485
+ let connectionBox = ConnectionBox<String>(continuation: continuation)
486
+
330
487
  connection.receive(minimumIncompleteLength: 1, maximumLength: 4096) { data, _, isComplete, error in
331
488
  if let error = error {
332
- continuation.resume(throwing: error)
333
- } else if let data = data, let response = String(data: data, encoding: .utf8) {
334
- continuation.resume(returning: response.trimmingCharacters(in: .whitespacesAndNewlines))
489
+ connectionBox.resume(throwing: error)
490
+ } else if let data = data {
491
+ // Try multiple encodings for FTP responses
492
+ var response: String?
493
+
494
+ // Try UTF-8 first
495
+ if let utf8Response = String(data: data, encoding: .utf8) {
496
+ response = utf8Response
497
+ }
498
+ // Try GBK for Chinese FTP servers
499
+ else if let gbkResponse = String(data: data, encoding: String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)))) {
500
+ response = gbkResponse
501
+ }
502
+ // Try ASCII as fallback
503
+ else if let asciiResponse = String(data: data, encoding: .ascii) {
504
+ response = asciiResponse
505
+ }
506
+
507
+ if let validResponse = response {
508
+ connectionBox.resume(returning: validResponse.trimmingCharacters(in: .whitespacesAndNewlines))
509
+ } else {
510
+ connectionBox.resume(throwing: FtpError.invalidResponse)
511
+ }
335
512
  } else {
336
- continuation.resume(throwing: FtpError.invalidResponse)
513
+ connectionBox.resume(throwing: FtpError.invalidResponse)
337
514
  }
338
515
  }
339
516
  }
@@ -371,17 +548,10 @@ class FtpClient {
371
548
  let dataConnection = NWConnection(to: dataEndpoint, using: .tcp)
372
549
 
373
550
  return try await withCheckedThrowingContinuation { continuation in
551
+ let connectionBox = NWConnectionBox(connection: dataConnection, continuation: continuation)
552
+
374
553
  dataConnection.stateUpdateHandler = { state in
375
- switch state {
376
- case .ready:
377
- continuation.resume(returning: dataConnection)
378
- case .failed(let error):
379
- continuation.resume(throwing: error)
380
- case .cancelled:
381
- continuation.resume(throwing: FtpError.connectionCancelled)
382
- default:
383
- break
384
- }
554
+ connectionBox.handleStateUpdate(state)
385
555
  }
386
556
 
387
557
  dataConnection.start(queue: .global())
@@ -392,22 +562,26 @@ class FtpClient {
392
562
  var receivedData = Data()
393
563
 
394
564
  return try await withCheckedThrowingContinuation { continuation in
565
+ let connectionBox = ConnectionBox<Data>(continuation: continuation)
566
+
395
567
  func receiveMore() {
396
568
  connection.receive(minimumIncompleteLength: 1, maximumLength: 8192) { data, _, isComplete, error in
397
569
  if let error = error {
398
- continuation.resume(throwing: error)
570
+ connectionBox.resume(throwing: error)
399
571
  return
400
572
  }
401
573
 
402
574
  if let data = data {
403
575
  receivedData.append(data)
404
576
  if reportProgress {
405
- self.progressDelegate?.onProgress(transferred: Int64(receivedData.count), total: -1)
577
+ DispatchQueue.main.async {
578
+ self.progressDelegate?.onProgress(transferred: Int64(receivedData.count), total: -1)
579
+ }
406
580
  }
407
581
  }
408
582
 
409
583
  if isComplete {
410
- continuation.resume(returning: receivedData)
584
+ connectionBox.resume(returning: receivedData)
411
585
  } else {
412
586
  receiveMore()
413
587
  }
@@ -427,15 +601,19 @@ class FtpClient {
427
601
  let chunk = data.subdata(in: i..<endIndex)
428
602
 
429
603
  try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
604
+ let connectionBox = ConnectionBox<Void>(continuation: continuation)
605
+
430
606
  connection.send(content: chunk, completion: .contentProcessed { error in
431
607
  if let error = error {
432
- continuation.resume(throwing: error)
608
+ connectionBox.resume(throwing: error)
433
609
  } else {
434
610
  sentBytes += chunk.count
435
611
  if reportProgress {
436
- self.progressDelegate?.onProgress(transferred: Int64(sentBytes), total: Int64(totalSize))
612
+ DispatchQueue.main.async {
613
+ self.progressDelegate?.onProgress(transferred: Int64(sentBytes), total: Int64(totalSize))
614
+ }
437
615
  }
438
- continuation.resume()
616
+ connectionBox.resume()
439
617
  }
440
618
  })
441
619
  }
@@ -443,48 +621,122 @@ class FtpClient {
443
621
  }
444
622
 
445
623
  private func parseListingData(_ data: Data) -> [FtpFileInfo] {
446
- guard let string = String(data: data, encoding: .utf8) else { return [] }
624
+ print("FTP DEBUG: Raw data length: \(data.count) bytes")
625
+ print("FTP DEBUG: Raw data hex: \(data.map { String(format: "%02x", $0) }.joined())")
447
626
 
448
- let lines = string.components(separatedBy: .newlines)
449
- var files: [FtpFileInfo] = []
627
+ // Try different encodings, prioritizing Chinese encodings
628
+ var string: String?
629
+ var encoding: String.Encoding = .utf8
630
+
631
+ // Try UTF-8 first
632
+ if let utf8String = String(data: data, encoding: .utf8) {
633
+ string = utf8String
634
+ encoding = .utf8
635
+ print("FTP DEBUG: Successfully decoded with UTF-8")
636
+ }
637
+ // Try GBK/GB2312 for Chinese
638
+ else if let gbkString = String(data: data, encoding: String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)))) {
639
+ string = gbkString
640
+ encoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)))
641
+ print("FTP DEBUG: Successfully decoded with GBK/GB18030")
642
+ }
643
+ // Try Big5 for Traditional Chinese
644
+ else if let big5String = String(data: data, encoding: String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.big5.rawValue)))) {
645
+ string = big5String
646
+ encoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.big5.rawValue)))
647
+ print("FTP DEBUG: Successfully decoded with Big5")
648
+ }
649
+ // Try ASCII
650
+ else if let asciiString = String(data: data, encoding: .ascii) {
651
+ string = asciiString
652
+ encoding = .ascii
653
+ print("FTP DEBUG: Successfully decoded with ASCII")
654
+ }
655
+ // Try Latin1 (ISO-8859-1)
656
+ else if let latin1String = String(data: data, encoding: .isoLatin1) {
657
+ string = latin1String
658
+ encoding = .isoLatin1
659
+ print("FTP DEBUG: Successfully decoded with Latin1")
660
+ }
661
+ // Try Windows-1252
662
+ else if let windowsString = String(data: data, encoding: .windowsCP1252) {
663
+ string = windowsString
664
+ encoding = .windowsCP1252
665
+ print("FTP DEBUG: Successfully decoded with Windows-1252")
666
+ }
667
+ else {
668
+ print("FTP DEBUG: Failed to decode data with any encoding")
669
+ print("FTP DEBUG: Raw bytes: \(Array(data))")
670
+ return []
671
+ }
672
+
673
+ guard let decodedString = string else {
674
+ print("FTP DEBUG: No valid string found")
675
+ return []
676
+ }
677
+
678
+ print("FTP DEBUG: Decoded string with \(encoding): '\(decodedString)'")
679
+ print("FTP DEBUG: String length: \(decodedString.count) characters")
450
680
 
451
- for line in lines {
681
+ let lines = decodedString.components(separatedBy: .newlines)
682
+ print("FTP DEBUG: Found \(lines.count) lines")
683
+ var files: [FtpFileInfo] = []
684
+
685
+ for (index, line) in lines.enumerated() {
686
+ print("FTP DEBUG: Processing line \(index): '\(line)'")
452
687
  if let fileInfo = parseListing(line) {
453
688
  files.append(fileInfo)
689
+ print("FTP DEBUG: Parsed file: \(fileInfo.name), isDirectory: \(fileInfo.isDirectory)")
690
+ } else {
691
+ print("FTP DEBUG: Failed to parse line \(index)")
454
692
  }
455
693
  }
456
-
694
+
695
+ print("FTP DEBUG: Successfully parsed \(files.count) files")
457
696
  return files
458
697
  }
459
698
 
460
699
  private func parseListing(_ line: String) -> FtpFileInfo? {
461
700
  let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
462
- guard !trimmedLine.isEmpty else { return nil }
463
-
701
+ guard !trimmedLine.isEmpty else {
702
+ print("FTP DEBUG: Line is empty after trimming")
703
+ return nil
704
+ }
705
+
706
+ print("FTP DEBUG: Parsing line: '\(trimmedLine)'")
707
+
464
708
  // Parse Unix-style listing: drwxr-xr-x 3 user group 4096 Mar 15 10:30 dirname
465
709
  let components = trimmedLine.components(separatedBy: .whitespaces).filter { !$0.isEmpty }
466
- guard components.count >= 9 else { return nil }
467
-
710
+ print("FTP DEBUG: Components count: \(components.count), components: \(components)")
711
+
712
+ guard components.count >= 9 else {
713
+ print("FTP DEBUG: Not enough components (\(components.count)), expected at least 9")
714
+ return nil
715
+ }
716
+
468
717
  let permissions = components[0]
469
718
  let isDirectory = permissions.hasPrefix("d")
470
719
  let size = isDirectory ? 0 : Int64(components[4]) ?? 0
471
-
720
+
472
721
  // Reconstruct filename (can contain spaces)
473
722
  let name = components[8...].joined(separator: " ")
474
-
723
+
475
724
  // Parse date
476
725
  let month = components[5]
477
726
  let day = components[6]
478
727
  let yearOrTime = components[7]
479
728
  let lastModified = "\(month) \(day) \(yearOrTime)"
480
-
481
- return FtpFileInfo(
729
+
730
+ let fileInfo = FtpFileInfo(
482
731
  name: name,
483
732
  isDirectory: isDirectory,
484
733
  size: size,
485
734
  lastModified: lastModified,
486
735
  permissions: permissions
487
736
  )
737
+
738
+ print("FTP DEBUG: Successfully parsed file: \(fileInfo)")
739
+ return fileInfo
488
740
  }
489
741
  }
490
742
 
@@ -565,13 +817,9 @@ public class ReactNativeKookitModule: Module {
565
817
 
566
818
  AsyncFunction("ftpDisconnect") { (promise: Promise) in
567
819
  Task {
568
- do {
569
- await self.ftpClient?.disconnect()
570
- self.ftpClient = nil
571
- promise.resolve()
572
- } catch {
573
- promise.reject("FTP_DISCONNECT_ERROR", error.localizedDescription)
574
- }
820
+ await self.ftpClient?.disconnect()
821
+ self.ftpClient = nil
822
+ promise.resolve()
575
823
  }
576
824
  }
577
825
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-kookit",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "React Native module for intercepting volume button presses on iOS and Android, with FTP client functionality",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",