react-native-nitro-logger 0.1.0

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.
Files changed (205) hide show
  1. package/LICENSE +20 -0
  2. package/NitroLogger.podspec +29 -0
  3. package/README.md +214 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +124 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/nitrologger/AndroidPlatformIo.kt +100 -0
  9. package/android/src/main/java/com/margelo/nitro/nitrologger/HybridFileSink.kt +292 -0
  10. package/android/src/main/java/com/margelo/nitro/nitrologger/HybridNativeConsoleSink.kt +23 -0
  11. package/android/src/main/java/com/margelo/nitro/nitrologger/LogFileWriter.kt +1328 -0
  12. package/android/src/main/java/com/margelo/nitro/nitrologger/LogRotationPolicy.kt +186 -0
  13. package/android/src/main/java/com/margelo/nitro/nitrologger/LogSecureFile.kt +80 -0
  14. package/android/src/main/java/com/margelo/nitro/nitrologger/LogWriterRegistry.kt +420 -0
  15. package/android/src/main/java/com/margelo/nitro/nitrologger/NativeConsoleWriter.kt +237 -0
  16. package/android/src/main/java/com/margelo/nitro/nitrologger/NitroLoggerPackage.kt +22 -0
  17. package/android/src/main/java/com/margelo/nitro/nitrologger/PlatformIo.kt +165 -0
  18. package/docs/PARITY.md +330 -0
  19. package/docs/PRIVACY.md +341 -0
  20. package/eslint-plugin/README.md +238 -0
  21. package/eslint-plugin/index.js +62 -0
  22. package/eslint-plugin/rules/literal-subsystem.js +102 -0
  23. package/eslint-plugin/rules/no-computed-metadata-key.js +174 -0
  24. package/eslint-plugin/rules/no-derived-correlation.js +171 -0
  25. package/eslint-plugin/rules/no-dynamic-message.js +60 -0
  26. package/eslint-plugin/shared.js +1948 -0
  27. package/ios/Gzip.swift +203 -0
  28. package/ios/HybridFileSink.swift +291 -0
  29. package/ios/HybridNativeConsoleSink.swift +20 -0
  30. package/ios/LogFileWriter.swift +1280 -0
  31. package/ios/LogSecureFile.swift +296 -0
  32. package/ios/LogWriterRegistry.swift +444 -0
  33. package/ios/NativeConsoleWriter.swift +180 -0
  34. package/lib/module/Logger.js +381 -0
  35. package/lib/module/Logger.js.map +1 -0
  36. package/lib/module/ScopedLogger.js +70 -0
  37. package/lib/module/ScopedLogger.js.map +1 -0
  38. package/lib/module/config.js +23 -0
  39. package/lib/module/config.js.map +1 -0
  40. package/lib/module/destinations/Batcher.js +647 -0
  41. package/lib/module/destinations/Batcher.js.map +1 -0
  42. package/lib/module/destinations/ConsoleDestination.js +51 -0
  43. package/lib/module/destinations/ConsoleDestination.js.map +1 -0
  44. package/lib/module/destinations/FileDestination.js +360 -0
  45. package/lib/module/destinations/FileDestination.js.map +1 -0
  46. package/lib/module/destinations/NativeConsoleDestination.js +170 -0
  47. package/lib/module/destinations/NativeConsoleDestination.js.map +1 -0
  48. package/lib/module/destinations/types.js +4 -0
  49. package/lib/module/destinations/types.js.map +1 -0
  50. package/lib/module/formatters/DefaultFormatter.js +136 -0
  51. package/lib/module/formatters/DefaultFormatter.js.map +1 -0
  52. package/lib/module/formatters/JsonLinesFormatter.js +162 -0
  53. package/lib/module/formatters/JsonLinesFormatter.js.map +1 -0
  54. package/lib/module/formatters/timestamp.js +15 -0
  55. package/lib/module/formatters/timestamp.js.map +1 -0
  56. package/lib/module/formatters/types.js +4 -0
  57. package/lib/module/formatters/types.js.map +1 -0
  58. package/lib/module/index.js +31 -0
  59. package/lib/module/index.js.map +1 -0
  60. package/lib/module/integrations/appState.js +81 -0
  61. package/lib/module/integrations/appState.js.map +1 -0
  62. package/lib/module/integrations/errorHandler.js +166 -0
  63. package/lib/module/integrations/errorHandler.js.map +1 -0
  64. package/lib/module/integrations/sanitizeError.js +238 -0
  65. package/lib/module/integrations/sanitizeError.js.map +1 -0
  66. package/lib/module/levels.js +35 -0
  67. package/lib/module/levels.js.map +1 -0
  68. package/lib/module/metadata.js +52 -0
  69. package/lib/module/metadata.js.map +1 -0
  70. package/lib/module/package.json +1 -0
  71. package/lib/module/privacy.js +349 -0
  72. package/lib/module/privacy.js.map +1 -0
  73. package/lib/module/specs/FileSink.nitro.js +4 -0
  74. package/lib/module/specs/FileSink.nitro.js.map +1 -0
  75. package/lib/module/specs/NativeConsoleSink.nitro.js +4 -0
  76. package/lib/module/specs/NativeConsoleSink.nitro.js.map +1 -0
  77. package/lib/module/types.js +4 -0
  78. package/lib/module/types.js.map +1 -0
  79. package/lib/module/utf8.js +42 -0
  80. package/lib/module/utf8.js.map +1 -0
  81. package/lib/typescript/package.json +1 -0
  82. package/lib/typescript/src/Logger.d.ts +122 -0
  83. package/lib/typescript/src/Logger.d.ts.map +1 -0
  84. package/lib/typescript/src/ScopedLogger.d.ts +36 -0
  85. package/lib/typescript/src/ScopedLogger.d.ts.map +1 -0
  86. package/lib/typescript/src/config.d.ts +12 -0
  87. package/lib/typescript/src/config.d.ts.map +1 -0
  88. package/lib/typescript/src/destinations/Batcher.d.ts +244 -0
  89. package/lib/typescript/src/destinations/Batcher.d.ts.map +1 -0
  90. package/lib/typescript/src/destinations/ConsoleDestination.d.ts +27 -0
  91. package/lib/typescript/src/destinations/ConsoleDestination.d.ts.map +1 -0
  92. package/lib/typescript/src/destinations/FileDestination.d.ts +200 -0
  93. package/lib/typescript/src/destinations/FileDestination.d.ts.map +1 -0
  94. package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts +86 -0
  95. package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts.map +1 -0
  96. package/lib/typescript/src/destinations/types.d.ts +19 -0
  97. package/lib/typescript/src/destinations/types.d.ts.map +1 -0
  98. package/lib/typescript/src/formatters/DefaultFormatter.d.ts +22 -0
  99. package/lib/typescript/src/formatters/DefaultFormatter.d.ts.map +1 -0
  100. package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts +73 -0
  101. package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts.map +1 -0
  102. package/lib/typescript/src/formatters/timestamp.d.ts +6 -0
  103. package/lib/typescript/src/formatters/timestamp.d.ts.map +1 -0
  104. package/lib/typescript/src/formatters/types.d.ts +27 -0
  105. package/lib/typescript/src/formatters/types.d.ts.map +1 -0
  106. package/lib/typescript/src/index.d.ts +34 -0
  107. package/lib/typescript/src/index.d.ts.map +1 -0
  108. package/lib/typescript/src/integrations/appState.d.ts +33 -0
  109. package/lib/typescript/src/integrations/appState.d.ts.map +1 -0
  110. package/lib/typescript/src/integrations/errorHandler.d.ts +55 -0
  111. package/lib/typescript/src/integrations/errorHandler.d.ts.map +1 -0
  112. package/lib/typescript/src/integrations/sanitizeError.d.ts +89 -0
  113. package/lib/typescript/src/integrations/sanitizeError.d.ts.map +1 -0
  114. package/lib/typescript/src/levels.d.ts +9 -0
  115. package/lib/typescript/src/levels.d.ts.map +1 -0
  116. package/lib/typescript/src/metadata.d.ts +23 -0
  117. package/lib/typescript/src/metadata.d.ts.map +1 -0
  118. package/lib/typescript/src/privacy.d.ts +103 -0
  119. package/lib/typescript/src/privacy.d.ts.map +1 -0
  120. package/lib/typescript/src/specs/FileSink.nitro.d.ts +131 -0
  121. package/lib/typescript/src/specs/FileSink.nitro.d.ts.map +1 -0
  122. package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts +21 -0
  123. package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts.map +1 -0
  124. package/lib/typescript/src/types.d.ts +37 -0
  125. package/lib/typescript/src/types.d.ts.map +1 -0
  126. package/lib/typescript/src/utf8.d.ts +15 -0
  127. package/lib/typescript/src/utf8.d.ts.map +1 -0
  128. package/nitro.json +33 -0
  129. package/nitrogen/generated/android/c++/JAppendResult.hpp +79 -0
  130. package/nitrogen/generated/android/c++/JClearOutcome.hpp +88 -0
  131. package/nitrogen/generated/android/c++/JFlushOutcome.hpp +81 -0
  132. package/nitrogen/generated/android/c++/JHybridFileSinkSpec.cpp +120 -0
  133. package/nitrogen/generated/android/c++/JHybridFileSinkSpec.hpp +69 -0
  134. package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.cpp +71 -0
  135. package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.hpp +64 -0
  136. package/nitrogen/generated/android/c++/JRejectReason.hpp +64 -0
  137. package/nitrogen/generated/android/c++/JRotationConfig.hpp +77 -0
  138. package/nitrogen/generated/android/c++/JSinkStatus.hpp +69 -0
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/AppendResult.kt +76 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/ClearOutcome.kt +66 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/FlushOutcome.kt +81 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridFileSinkSpec.kt +80 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridNativeConsoleSinkSpec.kt +58 -0
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RejectReason.kt +25 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RotationConfig.kt +76 -0
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/SinkStatus.kt +66 -0
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/nitrologgerOnLoad.kt +35 -0
  148. package/nitrogen/generated/android/nitrologger+autolinking.cmake +83 -0
  149. package/nitrogen/generated/android/nitrologger+autolinking.gradle +27 -0
  150. package/nitrogen/generated/android/nitrologgerOnLoad.cpp +70 -0
  151. package/nitrogen/generated/android/nitrologgerOnLoad.hpp +34 -0
  152. package/nitrogen/generated/ios/NitroLogger+autolinking.rb +62 -0
  153. package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.cpp +50 -0
  154. package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.hpp +216 -0
  155. package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Umbrella.hpp +69 -0
  156. package/nitrogen/generated/ios/NitroLoggerAutolinking.mm +41 -0
  157. package/nitrogen/generated/ios/NitroLoggerAutolinking.swift +38 -0
  158. package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.cpp +11 -0
  159. package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.hpp +150 -0
  160. package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.cpp +11 -0
  161. package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.hpp +87 -0
  162. package/nitrogen/generated/ios/swift/AppendResult.swift +60 -0
  163. package/nitrogen/generated/ios/swift/ClearOutcome.swift +50 -0
  164. package/nitrogen/generated/ios/swift/FlushOutcome.swift +59 -0
  165. package/nitrogen/generated/ios/swift/HybridFileSinkSpec.swift +61 -0
  166. package/nitrogen/generated/ios/swift/HybridFileSinkSpec_cxx.swift +227 -0
  167. package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec.swift +56 -0
  168. package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec_cxx.swift +148 -0
  169. package/nitrogen/generated/ios/swift/RejectReason.swift +48 -0
  170. package/nitrogen/generated/ios/swift/RotationConfig.swift +93 -0
  171. package/nitrogen/generated/ios/swift/SinkStatus.swift +44 -0
  172. package/nitrogen/generated/shared/c++/AppendResult.hpp +105 -0
  173. package/nitrogen/generated/shared/c++/ClearOutcome.hpp +96 -0
  174. package/nitrogen/generated/shared/c++/FlushOutcome.hpp +107 -0
  175. package/nitrogen/generated/shared/c++/HybridFileSinkSpec.cpp +28 -0
  176. package/nitrogen/generated/shared/c++/HybridFileSinkSpec.hpp +84 -0
  177. package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.cpp +22 -0
  178. package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.hpp +64 -0
  179. package/nitrogen/generated/shared/c++/RejectReason.hpp +84 -0
  180. package/nitrogen/generated/shared/c++/RotationConfig.hpp +103 -0
  181. package/nitrogen/generated/shared/c++/SinkStatus.hpp +95 -0
  182. package/package.json +151 -0
  183. package/src/Logger.ts +417 -0
  184. package/src/ScopedLogger.ts +94 -0
  185. package/src/config.ts +25 -0
  186. package/src/destinations/Batcher.ts +805 -0
  187. package/src/destinations/ConsoleDestination.ts +60 -0
  188. package/src/destinations/FileDestination.ts +473 -0
  189. package/src/destinations/NativeConsoleDestination.ts +220 -0
  190. package/src/destinations/types.ts +19 -0
  191. package/src/formatters/DefaultFormatter.ts +150 -0
  192. package/src/formatters/JsonLinesFormatter.ts +195 -0
  193. package/src/formatters/timestamp.ts +12 -0
  194. package/src/formatters/types.ts +29 -0
  195. package/src/index.tsx +106 -0
  196. package/src/integrations/appState.ts +111 -0
  197. package/src/integrations/errorHandler.ts +223 -0
  198. package/src/integrations/sanitizeError.ts +287 -0
  199. package/src/levels.ts +35 -0
  200. package/src/metadata.ts +52 -0
  201. package/src/privacy.ts +389 -0
  202. package/src/specs/FileSink.nitro.ts +144 -0
  203. package/src/specs/NativeConsoleSink.nitro.ts +21 -0
  204. package/src/types.ts +45 -0
  205. package/src/utf8.ts +39 -0
package/ios/Gzip.swift ADDED
@@ -0,0 +1,203 @@
1
+ #if canImport(Compression)
2
+ import Foundation
3
+ import Compression
4
+
5
+ /// Produces gzip (RFC 1952) containers for rotated log archives.
6
+ ///
7
+ /// Apple's Compression framework emits a *raw* DEFLATE stream for
8
+ /// `COMPRESSION_ZLIB` (RFC 1951) with no container around it, so writing a file
9
+ /// that `gunzip` and every other standard tool can open means supplying the
10
+ /// gzip header and the trailing CRC-32 and length ourselves.
11
+ ///
12
+ /// Vendored unchanged from SwiftLogger (`Sources/Logger/Gzip.swift`). Kept
13
+ /// byte-identical on purpose: it is the piece with the least reason to diverge
14
+ /// and the most reason to stay diffable against the original.
15
+ internal enum Gzip {
16
+
17
+ /// Header: magic, DEFLATE method, no flags, no mtime, no extra flags, and
18
+ /// an unknown OS (255) so the output is reproducible.
19
+ private static let header: [UInt8] = [0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff]
20
+
21
+ /// Returns `data` as a gzip stream, or `nil` if compression fails.
22
+ static func compress(_ data: Data) -> Data? {
23
+ guard let deflated = deflate(data) else { return nil }
24
+
25
+ var output = Data(capacity: deflated.count + 18)
26
+ output.append(contentsOf: header)
27
+ output.append(deflated)
28
+
29
+ // Trailer: CRC-32 of the *uncompressed* data, then its length mod 2^32,
30
+ // both little-endian.
31
+ appendLittleEndian(&output, crc32(data))
32
+ appendLittleEndian(&output, UInt32(truncatingIfNeeded: data.count))
33
+
34
+ return output
35
+ }
36
+
37
+ /// Size of each read from the source file. Two of these are live at a time,
38
+ /// so peak memory is bounded regardless of how large the archive is.
39
+ private static let chunkSize = 64 * 1024
40
+
41
+ /// Compresses the file at `source` into a gzip file at `destination`,
42
+ /// streaming in fixed-size chunks.
43
+ ///
44
+ /// ``compress(_:)`` holds the whole input, the whole DEFLATE output, and
45
+ /// the assembled container in memory at once — roughly 2.5× a 10 MB archive
46
+ /// on the write queue. This reads and deflates incrementally instead, so a
47
+ /// rotation costs two 64 KB buffers no matter how big the log grew.
48
+ ///
49
+ /// Returns `false` and removes any partial output if anything fails, so the
50
+ /// caller can keep the uncompressed original.
51
+ static func compressFile(at source: URL, to destination: URL) -> Bool {
52
+ guard let input = try? FileHandle(forReadingFrom: source) else { return false }
53
+ defer { try? input.close() }
54
+
55
+ guard FileManager.default.createFile(atPath: destination.path, contents: nil),
56
+ let output = try? FileHandle(forWritingTo: destination) else {
57
+ try? FileManager.default.removeItem(at: destination)
58
+ return false
59
+ }
60
+
61
+ var succeeded = false
62
+ defer {
63
+ try? output.close()
64
+ if !succeeded {
65
+ try? FileManager.default.removeItem(at: destination)
66
+ }
67
+ }
68
+
69
+ guard (try? output.write(contentsOf: Data(header))) != nil else { return false }
70
+
71
+ var stream = compression_stream(
72
+ dst_ptr: UnsafeMutablePointer<UInt8>(bitPattern: -1)!, dst_size: 0,
73
+ src_ptr: UnsafePointer<UInt8>(bitPattern: -1)!, src_size: 0,
74
+ state: nil
75
+ )
76
+ guard compression_stream_init(&stream, COMPRESSION_STREAM_ENCODE, COMPRESSION_ZLIB)
77
+ == COMPRESSION_STATUS_OK else { return false }
78
+ defer { compression_stream_destroy(&stream) }
79
+
80
+ let outputBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: chunkSize)
81
+ defer { outputBuffer.deallocate() }
82
+
83
+ var crc: UInt32 = 0xFFFF_FFFF
84
+ var totalRead = 0
85
+
86
+ while true {
87
+ // A read error must NOT be treated as end-of-input. Doing so would
88
+ // finalize the stream over a partial file, write a CRC and length
89
+ // matching what was read, and return success — producing a .gz that
90
+ // gunzip happily verifies while the caller deletes the complete
91
+ // original. Silent truncation is far worse than a failed rotation.
92
+ //
93
+ // `read(upToCount:)` returns nil at EOF and throws on error, so the
94
+ // two must be distinguished rather than collapsed with `try?`.
95
+ let chunk: Data
96
+ do {
97
+ chunk = try input.read(upToCount: chunkSize) ?? Data()
98
+ } catch {
99
+ return false
100
+ }
101
+ let isLast = chunk.isEmpty
102
+ totalRead += chunk.count
103
+ crc = crc32Update(crc, chunk)
104
+
105
+ let ok = chunk.withUnsafeBytes { raw -> Bool in
106
+ // An empty Data has no base address; the encoder accepts a
107
+ // zero-length source as long as the pointer is non-null.
108
+ let base = raw.bindMemory(to: UInt8.self).baseAddress
109
+ ?? UnsafePointer<UInt8>(bitPattern: -1)!
110
+ stream.src_ptr = base
111
+ stream.src_size = chunk.count
112
+ let flags = isLast ? Int32(COMPRESSION_STREAM_FINALIZE.rawValue) : 0
113
+
114
+ // One source chunk can expand into several output chunks, so
115
+ // keep draining until the encoder stops filling the buffer.
116
+ repeat {
117
+ stream.dst_ptr = outputBuffer
118
+ stream.dst_size = chunkSize
119
+ let status = compression_stream_process(&stream, flags)
120
+ guard status != COMPRESSION_STATUS_ERROR else { return false }
121
+
122
+ let produced = chunkSize - stream.dst_size
123
+ if produced > 0 {
124
+ let out = Data(bytes: outputBuffer, count: produced)
125
+ guard (try? output.write(contentsOf: out)) != nil else { return false }
126
+ }
127
+ if status == COMPRESSION_STATUS_END { break }
128
+ } while stream.src_size > 0 || (isLast && stream.dst_size == 0)
129
+
130
+ return true
131
+ }
132
+ guard ok else { return false }
133
+ if isLast { break }
134
+ }
135
+
136
+ // Trailer: CRC-32 of the uncompressed bytes, then their length mod 2^32.
137
+ var trailer = Data(capacity: 8)
138
+ appendLittleEndian(&trailer, crc ^ 0xFFFF_FFFF)
139
+ appendLittleEndian(&trailer, UInt32(truncatingIfNeeded: totalRead))
140
+ guard (try? output.write(contentsOf: trailer)) != nil else { return false }
141
+
142
+ succeeded = true
143
+ return true
144
+ }
145
+
146
+ private static func appendLittleEndian(_ data: inout Data, _ value: UInt32) {
147
+ withUnsafeBytes(of: value.littleEndian) { data.append(contentsOf: $0) }
148
+ }
149
+
150
+ private static func deflate(_ data: Data) -> Data? {
151
+ guard !data.isEmpty else { return Data() }
152
+
153
+ // Incompressible input can grow slightly, so leave headroom rather than
154
+ // letting the encode fail on a tight buffer.
155
+ let capacity = data.count + (data.count / 2) + 64
156
+ let destination = UnsafeMutablePointer<UInt8>.allocate(capacity: capacity)
157
+ defer { destination.deallocate() }
158
+
159
+ let written = data.withUnsafeBytes { raw -> Int in
160
+ guard let base = raw.bindMemory(to: UInt8.self).baseAddress else { return 0 }
161
+ return compression_encode_buffer(
162
+ destination, capacity,
163
+ base, data.count,
164
+ nil,
165
+ COMPRESSION_ZLIB
166
+ )
167
+ }
168
+
169
+ guard written > 0 else { return nil }
170
+ return Data(bytes: destination, count: written)
171
+ }
172
+
173
+ // MARK: - CRC-32
174
+
175
+ private static let crcTable: [UInt32] = {
176
+ (0..<256).map { index -> UInt32 in
177
+ var value = UInt32(index)
178
+ for _ in 0..<8 {
179
+ value = (value & 1 == 1) ? (0xEDB8_8320 ^ (value >> 1)) : (value >> 1)
180
+ }
181
+ return value
182
+ }
183
+ }()
184
+
185
+ static func crc32(_ data: Data) -> UInt32 {
186
+ crc32Update(0xFFFF_FFFF, data) ^ 0xFFFF_FFFF
187
+ }
188
+
189
+ /// Folds `data` into a running CRC-32. The caller supplies the initial
190
+ /// `0xFFFF_FFFF` and applies the final inversion, so a checksum can be
191
+ /// accumulated across streamed chunks.
192
+ private static func crc32Update(_ crc: UInt32, _ data: Data) -> UInt32 {
193
+ guard !data.isEmpty else { return crc }
194
+ var value = crc
195
+ data.withUnsafeBytes { raw in
196
+ for byte in raw.bindMemory(to: UInt8.self) {
197
+ value = crcTable[Int((value ^ UInt32(byte)) & 0xFF)] ^ (value >> 8)
198
+ }
199
+ }
200
+ return value
201
+ }
202
+ }
203
+ #endif
@@ -0,0 +1,291 @@
1
+ import Foundation
2
+ import NitroModules
3
+
4
+ /// The Nitro end of the file sink: marshalling, and nothing else.
5
+ ///
6
+ /// Every decision — batching, backpressure, drop accounting, loss notices —
7
+ /// lives in TypeScript, and every byte that touches the disk lives in
8
+ /// `LogWriter`. What is left here is converting `Double` to `Int`, one enum to
9
+ /// another, and holding exactly one `LogFileHandle`.
10
+ ///
11
+ /// That split is the point, not an accident of it: `LogWriter` imports nothing
12
+ /// from Nitro, so rotation, recovery, the registry, and every injected fault run
13
+ /// under XCTest in about a second instead of by hand on a simulator. Anything
14
+ /// that grows logic in this file has been put in the wrong place.
15
+ final class HybridFileSink: HybridFileSinkSpec {
16
+ private let lock = NSLock()
17
+ private var handle: LogFileHandle?
18
+
19
+ /// Whether this sink may have put anything on disk.
20
+ ///
21
+ /// `handle == nil` conflates two states that owe the caller opposite answers.
22
+ /// A sink that created nothing can say "everything is deleted" vacuously. A
23
+ /// sink whose files are on disk but out of reach cannot — and reporting a
24
+ /// durable purge over those is the worst lie in the API, because the only
25
+ /// caller who asks is the one deleting patient data on request.
26
+ ///
27
+ /// Set *before* the acquisition attempt rather than after it succeeds.
28
+ /// Acquiring creates the log directory and can then fail on the file itself,
29
+ /// so a throw out of `acquire` is not evidence that nothing was written.
30
+ ///
31
+ /// Never cleared once set: `close` releases the handle, it does not unmake
32
+ /// the files.
33
+ private var mayHaveArtifacts = false
34
+
35
+ /// An `open` past its check and inside `acquire`.
36
+ ///
37
+ /// Published so a second `open` is refused instead of racing to install a
38
+ /// handle over the first one's, and so `close` can tell "nothing here" from
39
+ /// "something is on its way".
40
+ private var opening = false
41
+
42
+ /// A `close` that arrived while `opening` was true.
43
+ ///
44
+ /// The close had no handle to flush and has already returned; this tells the
45
+ /// in-flight `open` to throw away what it acquired rather than install it.
46
+ private var closePending = false
47
+
48
+ /// The native finalizer, which is the whole reason the refcount is native.
49
+ ///
50
+ /// An abrupt runtime teardown never runs JavaScript, so a JS `dispose()` is
51
+ /// not a guarantee — but this deinit is. Releasing here hands the descriptor
52
+ /// and the registry slot back even when nothing on the JS side got to run.
53
+ deinit {
54
+ lock.lock()
55
+ let live = handle
56
+ handle = nil
57
+ lock.unlock()
58
+ // Zero deadline: a teardown deinit must not wait on a wedged disk.
59
+ _ = live?.close(deadlineMs: 0)
60
+ }
61
+
62
+ var defaultLogDirectory: String {
63
+ let base = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first
64
+ ?? FileManager.default.temporaryDirectory
65
+ return base.appendingPathComponent("Logs").path
66
+ }
67
+
68
+ func open(path: String, rotation: RotationConfig?, lineFramed: Bool?) throws {
69
+ lock.lock()
70
+ // `opening` counts as open. Acquisition does real I/O — it creates the
71
+ // directory, opens the file, and may scan a torn tail — so the lock cannot
72
+ // be held across it without putting every `getStatus` behind disk latency,
73
+ // which is the one thing that call is documented not to do. Instead the
74
+ // in-flight attempt is published, and a second one is refused rather than
75
+ // being allowed to acquire a writer that would overwrite the first: the
76
+ // loser's handle would be unreachable, and unreachable means a later purge
77
+ // never deletes its files.
78
+ let busy = handle != nil || opening
79
+ if !busy {
80
+ opening = true
81
+ closePending = false
82
+ // Recorded before the attempt: see `mayHaveArtifacts`.
83
+ mayHaveArtifacts = true
84
+ }
85
+ lock.unlock()
86
+ guard !busy else {
87
+ throw RuntimeError.error(withMessage: "FileSink: already open")
88
+ }
89
+
90
+ // However this attempt ends, it is no longer in flight afterwards.
91
+ defer {
92
+ lock.lock()
93
+ opening = false
94
+ lock.unlock()
95
+ }
96
+
97
+ let acquired: LogFileHandle
98
+ do {
99
+ acquired = try LogWriterRegistry.shared.acquire(
100
+ path: path,
101
+ policy: Self.policy(from: rotation),
102
+ // Absent means absent. Without a declared one-record-per-line contract
103
+ // the startup scan must not trim a trailing record, because it cannot
104
+ // tell a torn one from an intentional newline.
105
+ lineFramed: lineFramed ?? false
106
+ )
107
+ } catch LogWriterError.configConflict {
108
+ throw RuntimeError.error(
109
+ withMessage: "FileSink: another destination already opened this file with a different configuration")
110
+ } catch LogWriterError.symlinkEscape {
111
+ throw RuntimeError.error(withMessage: "FileSink: the log path is a symbolic link")
112
+ } catch LogWriterError.stillClosing {
113
+ // Distinct from the others because it is the one worth retrying: a
114
+ // previous destination on this file has not finished shutting down, and
115
+ // opening a second writer alongside it is exactly what must not happen.
116
+ throw RuntimeError.error(
117
+ withMessage: "FileSink: the previous destination for this file is still closing")
118
+ } catch {
119
+ // Deliberately payload-free. An `errno` description or a path is exactly
120
+ // the kind of string that carries a username, and this message crosses
121
+ // into JavaScript where something will eventually log it.
122
+ throw RuntimeError.error(withMessage: "FileSink: could not open the log file")
123
+ }
124
+
125
+ lock.lock()
126
+ // A close that arrived mid-acquisition found nothing to hand back and has
127
+ // already returned. Installing now would leave a live writer holding a
128
+ // descriptor that nothing can reach or release.
129
+ let abandon = closePending
130
+ closePending = false
131
+ if !abandon { handle = acquired }
132
+ lock.unlock()
133
+
134
+ if abandon {
135
+ // Zero deadline: the caller has already been told this sink is closed.
136
+ _ = acquired.close(deadlineMs: 0)
137
+ }
138
+ }
139
+
140
+ func appendBatch(batch: String, entryCount: Double) throws -> AppendResult {
141
+ guard let handle = current() else {
142
+ return AppendResult(accepted: false, rejectReason: .closed,
143
+ queuedBytes: 0, lostBytes: 0, lostEntries: 0, degraded: 0)
144
+ }
145
+ // Refused rather than rounded: an unchecked cast of NaN or 1e30 traps, and
146
+ // a count that does not survive the round trip cannot be trusted to
147
+ // describe the batch it arrived with.
148
+ guard entryCount.isFinite, let count = Int(exactly: entryCount) else {
149
+ return Self.appendResult(
150
+ LogAppendResult(accepted: false, rejectReason: .failed, status: handle.status()))
151
+ }
152
+ return Self.appendResult(handle.appendBatch(batch, entryCount: count))
153
+ }
154
+
155
+ func getStatus() throws -> SinkStatus {
156
+ guard let handle = current() else {
157
+ return SinkStatus(queuedBytes: 0, lostBytes: 0, lostEntries: 0, degraded: 0)
158
+ }
159
+ return Self.status(handle.status())
160
+ }
161
+
162
+ func flush(deadlineMs: Double) throws -> FlushOutcome {
163
+ guard let handle = current() else {
164
+ // Nothing was ever accepted through this sink, so "every accepted byte
165
+ // reached storage" holds vacuously. Reporting otherwise would have the
166
+ // JavaScript side retry a flush that can never have anything to do.
167
+ return Self.emptyOutcome()
168
+ }
169
+ return Self.flushOutcome(handle.flush(deadlineMs: deadlineMs))
170
+ }
171
+
172
+ func close(deadlineMs: Double) throws -> FlushOutcome {
173
+ lock.lock()
174
+ let live = handle
175
+ handle = nil
176
+ // Nothing to close, but an acquisition is in flight and will finish after
177
+ // this returns. Recording the intent is what keeps that writer from being
178
+ // installed into a sink the caller has already closed.
179
+ if live == nil && opening { closePending = true }
180
+ lock.unlock()
181
+
182
+ guard let live else { return Self.emptyOutcome() }
183
+ return Self.flushOutcome(live.close(deadlineMs: deadlineMs))
184
+ }
185
+
186
+ func getLogFilePaths() throws -> [String] {
187
+ current()?.logFilePaths() ?? []
188
+ }
189
+
190
+ func clearLogs(deadlineMs: Double) throws -> ClearOutcome {
191
+ // Both fields read in ONE critical section. Reading them separately lets a
192
+ // close land in between and produce "no handle, nothing created" — the one
193
+ // combination that is never true, and the one that lies in the direction
194
+ // that matters.
195
+ lock.lock()
196
+ let live = handle
197
+ let created = mayHaveArtifacts
198
+ lock.unlock()
199
+
200
+ guard let handle = live else {
201
+ // Nothing created: "every artifact is gone" holds vacuously. `rebound`
202
+ // stays false regardless — there is nothing to rebind onto, and saying
203
+ // otherwise would have the JS destination resume against a sink it never
204
+ // opened.
205
+ //
206
+ // Otherwise the files are still there and this object cannot reach them.
207
+ // The registry has the right answer for a released handle
208
+ // (`durable: false`, pinned by `testAReleasedHandleCannotPurge`) but
209
+ // `close` nils the handle above before that branch is reachable, so the
210
+ // honest answer has to be produced here.
211
+ return ClearOutcome(
212
+ deletedCount: 0, failedPaths: [], durable: !created, rebound: false)
213
+ }
214
+ let outcome = handle.clearLogs(deadlineMs: deadlineMs)
215
+ return ClearOutcome(
216
+ deletedCount: Double(outcome.deletedCount),
217
+ failedPaths: outcome.failedPaths,
218
+ durable: outcome.durable,
219
+ rebound: outcome.rebound
220
+ )
221
+ }
222
+
223
+ private func current() -> LogFileHandle? {
224
+ lock.lock()
225
+ defer { lock.unlock() }
226
+ return handle
227
+ }
228
+
229
+ // MARK: - Marshalling
230
+
231
+ /// Numbers arrive as `Double` because that is what JavaScript has. The
232
+ /// clamping lives in `LogRotationPolicy.init`, which is also where the Kotlin
233
+ /// side's equivalent has to agree with it.
234
+ private static func policy(from config: RotationConfig?) -> LogRotationPolicy {
235
+ guard let config else { return LogRotationPolicy() }
236
+ return LogRotationPolicy(
237
+ maxFileSizeBytes: config.maxFileSizeBytes,
238
+ maxArchivedFilesCount: config.maxArchivedFilesCount,
239
+ maxFileAgeSeconds: config.maxFileAgeSeconds,
240
+ compressArchives: config.compressArchives,
241
+ maxArchiveAgeSeconds: config.maxArchiveAgeSeconds,
242
+ maxTotalLogBytes: config.maxTotalLogBytes
243
+ )
244
+ }
245
+
246
+ private static func status(_ status: LogSinkStatus) -> SinkStatus {
247
+ SinkStatus(
248
+ queuedBytes: Double(status.queuedBytes),
249
+ lostBytes: Double(status.lostBytes),
250
+ lostEntries: Double(status.lostEntries),
251
+ degraded: Double(status.degraded)
252
+ )
253
+ }
254
+
255
+ private static func appendResult(_ result: LogAppendResult) -> AppendResult {
256
+ AppendResult(
257
+ accepted: result.accepted,
258
+ rejectReason: result.rejectReason.map(reason),
259
+ queuedBytes: Double(result.status.queuedBytes),
260
+ lostBytes: Double(result.status.lostBytes),
261
+ lostEntries: Double(result.status.lostEntries),
262
+ degraded: Double(result.status.degraded)
263
+ )
264
+ }
265
+
266
+ private static func flushOutcome(_ outcome: LogFlushOutcome) -> FlushOutcome {
267
+ FlushOutcome(
268
+ durable: outcome.durable,
269
+ timedOut: outcome.timedOut,
270
+ pendingBytes: Double(outcome.pendingBytes),
271
+ queuedBytes: Double(outcome.status.queuedBytes),
272
+ lostBytes: Double(outcome.status.lostBytes),
273
+ lostEntries: Double(outcome.status.lostEntries),
274
+ degraded: Double(outcome.status.degraded)
275
+ )
276
+ }
277
+
278
+ private static func emptyOutcome() -> FlushOutcome {
279
+ FlushOutcome(durable: true, timedOut: false, pendingBytes: 0,
280
+ queuedBytes: 0, lostBytes: 0, lostEntries: 0, degraded: 0)
281
+ }
282
+
283
+ private static func reason(_ reason: LogRejectReason) -> RejectReason {
284
+ switch reason {
285
+ case .full: return .full
286
+ case .staleGeneration: return .stalegeneration
287
+ case .closed: return .closed
288
+ case .failed: return .failed
289
+ }
290
+ }
291
+ }
@@ -0,0 +1,20 @@
1
+ import Foundation
2
+ import NitroModules
3
+
4
+ /// The Nitro end of the os_log sink: marshalling, and nothing else.
5
+ ///
6
+ /// The level map and the chunking live in `NativeConsoleWriter`, which imports
7
+ /// nothing from Nitro and therefore runs under XCTest. Anything that grows a
8
+ /// decision in this file has been put in the wrong place — the same split the
9
+ /// file sink uses, for the same reason.
10
+ final class HybridNativeConsoleSink: HybridNativeConsoleSinkSpec {
11
+ private let writer = NativeConsoleWriter()
12
+
13
+ func install(subsystem: String, category: String) throws {
14
+ writer.install(subsystem: subsystem, category: category)
15
+ }
16
+
17
+ func logBatch(levels: [Double], messages: [String]) throws {
18
+ writer.logBatch(levels: levels, messages: messages)
19
+ }
20
+ }