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
@@ -0,0 +1,237 @@
1
+ package com.margelo.nitro.nitrologger
2
+
3
+ import android.util.Log
4
+
5
+ /**
6
+ * The logcat bridge, minus Nitro.
7
+ *
8
+ * Everything worth getting wrong here — the level map, and what happens to a
9
+ * line longer than logcat will store — is a pure function of its input, so it
10
+ * lives where JUnit can reach it. [HybridNativeConsoleSink] is the adapter
11
+ * around this and holds no logic of its own. The iOS twin,
12
+ * `NativeConsoleWriter.swift`, is split the same way for the same reason.
13
+ *
14
+ * [emit] exists so the tests can watch what would have gone to logcat.
15
+ * `android.util.Log` is a stub under unit tests that swallows everything and
16
+ * returns zero, so a writer that called it directly could only ever be tested
17
+ * by not testing it.
18
+ */
19
+ class NativeConsoleWriter(private val emit: (Int, String, String) -> Unit = LOGCAT) {
20
+
21
+ @Volatile
22
+ private var tag: String = FALLBACK_TAG
23
+
24
+ /** Binds the tag. Calling it again rebinds. */
25
+ fun install(subsystem: String, category: String) {
26
+ // logcat has no subsystem dimension, so the category becomes the tag and
27
+ // the subsystem is deliberately unused — it is already in the rendered
28
+ // line. An empty category would produce entries nobody can filter for.
29
+ tag = category.ifEmpty { FALLBACK_TAG }
30
+ }
31
+
32
+ /**
33
+ * Writes one drained batch.
34
+ *
35
+ * `levels` and `messages` are parallel arrays rather than an array of
36
+ * structs, which keeps a batch to two bridge crossings instead of one per
37
+ * entry. `messages` is authoritative on count: a short `levels` means the
38
+ * caller has a bug, and dropping the messages it does have would turn that
39
+ * bug into missing logs.
40
+ */
41
+ fun logBatch(levels: DoubleArray, messages: Array<String>) {
42
+ if (messages.isEmpty()) return
43
+ val tag = this.tag
44
+ for (i in messages.indices) {
45
+ val priority = priorityOf(if (i < levels.size) levels[i] else 2.0)
46
+ for (chunk in chunks(messages[i])) emit(priority, tag, chunk)
47
+ }
48
+ }
49
+
50
+ companion object {
51
+ /**
52
+ * Bytes of message text per logcat entry.
53
+ *
54
+ * logcat's per-entry payload is about 4 KB and the rest is dropped in
55
+ * silence — no ellipsis, no diagnostic, the tail simply is not there. For a
56
+ * logger that is the worst kind of failure: the console shows a complete-
57
+ * looking line that is missing the half you needed. 3800 leaves room under
58
+ * the limit for the tag, the priority byte and logcat's own framing.
59
+ *
60
+ * Lower than the iOS 900 because the two platforms' limits genuinely
61
+ * differ; the *behaviour* around the limit is what is kept identical.
62
+ */
63
+ const val CHUNK_BYTES = 3800
64
+
65
+ /**
66
+ * At most this many chunks per line. A stack trace should arrive whole; a
67
+ * runaway 4 MB string should not become a thousand logcat entries.
68
+ */
69
+ const val MAX_CHUNKS = 8
70
+
71
+ /** Used when nothing installed a category, so entries stay findable. */
72
+ const val FALLBACK_TAG = "NitroLogger"
73
+
74
+ private val LOGCAT: (Int, String, String) -> Unit = { priority, tag, message ->
75
+ Log.println(priority, tag, message)
76
+ }
77
+
78
+ /**
79
+ * Codes 0–5 (verbose…todo), matching `LEVEL_ORDER` in TypeScript.
80
+ *
81
+ * Matched on the `Double` rather than converted first. Levels arrive as
82
+ * `Double` because that is what JavaScript has, and `toInt()` does not
83
+ * report a bad one — it saturates, so `NaN` becomes 0 and would log a
84
+ * corrupt level as `VERBOSE`, the one priority most likely to be filtered
85
+ * out entirely.
86
+ *
87
+ * A near-integer code is *rounded*, not rejected: 3.6 is a level 4 that
88
+ * survived a float round trip, and iOS has always read it that way. Only a
89
+ * code that rounds to nothing in 0–5 — negative, enormous, `NaN` — is
90
+ * `INFO`. It carries no trustworthy severity, so it is shown rather than
91
+ * hidden as `VERBOSE` or escalated to `ASSERT`. The rendered line still
92
+ * carries its own level tag either way. iOS answers `.default` here.
93
+ *
94
+ * Not `Math.round`, which is half-*up*: it rounds -0.5 to 0 and -1.5 to -1,
95
+ * where Swift's `.rounded()` is half-*away-from-zero* and answers -1 and -2.
96
+ * The two agree on everything except a negative tie, and a level map that
97
+ * disagrees across platforms on any input is the kind of difference nobody
98
+ * can reproduce from a bug report.
99
+ */
100
+ fun priorityOf(code: Double): Int {
101
+ if (code.isNaN()) return Log.INFO
102
+ return when (roundHalfAwayFromZero(code)) {
103
+ 0.0 -> Log.VERBOSE
104
+ 1.0 -> Log.DEBUG
105
+ 2.0 -> Log.INFO
106
+ 3.0 -> Log.WARN
107
+ 4.0 -> Log.ERROR
108
+ 5.0 -> Log.ASSERT // todo → highest visibility, mirrors os_log .fault
109
+ else -> Log.INFO
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Swift's default rounding rule, which this has to match exactly.
115
+ *
116
+ * Written as "is the fraction at least a half" rather than the usual
117
+ * `floor(v + 0.5)`, because that shortcut is wrong just below every tie:
118
+ * `0.49999999999999994 + 0.5` is not representable and rounds *up* to
119
+ * exactly `1.0`, so the floor answers 1 where Swift answers 0. Comparing
120
+ * the fraction never adds across the boundary and so cannot invent one.
121
+ *
122
+ * Infinities survive as infinities and so match no level, which is what
123
+ * sends them to `INFO`. Above 2^52 every double is already an integer, the
124
+ * fraction is zero, and this returns the value unchanged.
125
+ */
126
+ private fun roundHalfAwayFromZero(value: Double): Double {
127
+ val magnitude = Math.abs(value)
128
+ val lower = Math.floor(magnitude)
129
+ val rounded = if (magnitude - lower >= 0.5) lower + 1.0 else lower
130
+ return Math.copySign(rounded, value)
131
+ }
132
+
133
+ /**
134
+ * Splits a line into pieces logcat will store whole.
135
+ *
136
+ * A line that already fits comes back untouched and unmarked — the common
137
+ * case must look exactly as it was rendered. Only when splitting is
138
+ * unavoidable does each piece get an `(i/n)` prefix, so a reader can tell a
139
+ * continuation from a fresh entry and can see when one is missing.
140
+ *
141
+ * Splits fall on code point boundaries, never inside a surrogate pair:
142
+ * cutting an emoji in half produces replacement characters in logcat and,
143
+ * worse, suggests corruption that is not there. This is a shade weaker than
144
+ * the iOS twin, which splits on grapheme clusters and so also keeps
145
+ * combining sequences together — reaching that here means `BreakIterator`
146
+ * and an ICU version that varies by device, for a case that renders
147
+ * oddly rather than corruptly. `docs/PARITY.md` records the difference.
148
+ *
149
+ * A single code point wider than a whole chunk cannot occur — four bytes at
150
+ * most, against a budget in the thousands.
151
+ */
152
+ fun chunks(message: String): List<String> {
153
+ if (utf8Length(message) <= CHUNK_BYTES) return listOf(message)
154
+
155
+ // Reserve room for the widest marker any of these pieces can carry, so
156
+ // adding the prefix cannot push a piece back over the limit.
157
+ val budget = CHUNK_BYTES - MARKER_WIDTH
158
+ val pieces = mutableListOf<String>()
159
+ val current = StringBuilder()
160
+ var currentBytes = 0
161
+
162
+ var i = 0
163
+ while (i < message.length) {
164
+ val codePoint = message.codePointAt(i)
165
+ val charCount = Character.charCount(codePoint)
166
+ val width = utf8Width(codePoint)
167
+
168
+ if (currentBytes + width > budget && current.isNotEmpty()) {
169
+ pieces.add(current.toString())
170
+ current.setLength(0)
171
+ currentBytes = 0
172
+ if (pieces.size == MAX_CHUNKS) break
173
+ }
174
+ current.appendRange(message, i, i + charCount)
175
+ currentBytes += width
176
+ i += charCount
177
+ }
178
+
179
+ if (pieces.size < MAX_CHUNKS && current.isNotEmpty()) pieces.add(current.toString())
180
+
181
+ // Whatever is left over after the chunk ceiling is announced by size
182
+ // rather than dropped in silence. A byte count is a length, not content,
183
+ // so it says nothing about what was in the line.
184
+ var remaining = utf8Length(message) - pieces.sumOf { utf8Length(it) }
185
+ if (remaining > 0 && pieces.isNotEmpty()) {
186
+ val last = StringBuilder(pieces.removeAt(pieces.size - 1))
187
+ // Make room for the notice inside the entry rather than appending past
188
+ // the limit — otherwise logcat truncates the very sentence that exists
189
+ // to report truncation, which is the one line here that must survive.
190
+ // Each code point given back grows the count it reports.
191
+ while (utf8Length(last) + NOTICE_WIDTH + MARKER_WIDTH > CHUNK_BYTES && last.isNotEmpty()) {
192
+ val dropped = last.codePointBefore(last.length)
193
+ last.setLength(last.length - Character.charCount(dropped))
194
+ remaining += utf8Width(dropped)
195
+ }
196
+ last.append(" …+$remaining bytes truncated")
197
+ pieces.add(last.toString())
198
+ }
199
+
200
+ val total = pieces.size
201
+ return pieces.mapIndexed { index, piece -> "(${index + 1}/$total) $piece" }
202
+ }
203
+
204
+ /** `"(8/8) "` — the widest prefix [MAX_CHUNKS] can produce. */
205
+ private val MARKER_WIDTH = utf8Length("($MAX_CHUNKS/$MAX_CHUNKS) ")
206
+
207
+ /**
208
+ * The truncation notice at its widest, so reserving this much is always
209
+ * enough however large the count turns out to be.
210
+ */
211
+ private val NOTICE_WIDTH = utf8Length(" …+${Int.MAX_VALUE} bytes truncated")
212
+
213
+ /**
214
+ * UTF-8 byte length, computed rather than measured.
215
+ *
216
+ * `toByteArray()` would encode the whole string to answer a question about
217
+ * its length, on every code point of every oversized line.
218
+ */
219
+ private fun utf8Length(text: CharSequence): Int {
220
+ var bytes = 0
221
+ var i = 0
222
+ while (i < text.length) {
223
+ val codePoint = Character.codePointAt(text, i)
224
+ bytes += utf8Width(codePoint)
225
+ i += Character.charCount(codePoint)
226
+ }
227
+ return bytes
228
+ }
229
+
230
+ private fun utf8Width(codePoint: Int): Int = when {
231
+ codePoint < 0x80 -> 1
232
+ codePoint < 0x800 -> 2
233
+ codePoint < 0x10000 -> 3
234
+ else -> 4
235
+ }
236
+ }
237
+ }
@@ -0,0 +1,22 @@
1
+ package com.margelo.nitro.nitrologger
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+
8
+ class NitroLoggerPackage : BaseReactPackage() {
9
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
10
+ return null
11
+ }
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
14
+ return ReactModuleInfoProvider { HashMap() }
15
+ }
16
+
17
+ companion object {
18
+ init {
19
+ System.loadLibrary("nitrologger")
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,165 @@
1
+ package com.margelo.nitro.nitrologger
2
+
3
+ import java.io.File
4
+ import java.io.FileDescriptor
5
+
6
+ /**
7
+ * The handful of operations the writer needs that `java.io.File` cannot answer.
8
+ *
9
+ * Four of them are load-bearing rather than nice to have:
10
+ *
11
+ * - **Link count.** Writes to an unlinked file succeed forever and land
12
+ * nowhere. `File.exists()` cannot see it — the name may have been recreated
13
+ * by something else while this descriptor still points at the orphan — so
14
+ * without `fstat` an externally deleted log means silent loss for the rest of
15
+ * the process's life.
16
+ * - **Directory sync.** `delete()` returning true only means the change is in
17
+ * the directory's in-memory state. Until the directory itself is synced a
18
+ * crash can bring every one of those names back, and a compliance purge
19
+ * exists precisely to promise they are gone.
20
+ * - **Tri-state lookup.** `File.exists()` returns false for "not there" and for
21
+ * "could not tell" alike. A purge that treats the second as the first claims
22
+ * `durable = true` over artifacts that are still on disk.
23
+ * - **Symlink detection.** `FileOutputStream` has no `O_NOFOLLOW`, so the check
24
+ * has to be a separate `lstat`.
25
+ *
26
+ * They live behind an interface so [LogFileWriter] imports nothing from
27
+ * `android.*` and can be tested on a plain JVM, the same reason the iOS writer
28
+ * has no Nitro imports.
29
+ *
30
+ * **`java.nio.file` is API 26 and this library supports 24.** Every use of it
31
+ * is confined to [Jvm], which runs only under unit tests; [AndroidPlatformIo]
32
+ * implements all four with `Os` syscalls and never delegates here for them. An
33
+ * unresolvable class raises `NoClassDefFoundError`, which is an `Error` and
34
+ * passes straight through `catch (Exception)` — so the isolation is what makes
35
+ * this safe, not the catch blocks.
36
+ */
37
+ interface PlatformIo {
38
+ /**
39
+ * What a no-follow lookup could establish about a path.
40
+ *
41
+ * [UNKNOWN] is the reason this is not a `Boolean`: a purge may only count an
42
+ * artifact as gone when the platform actually said it was.
43
+ */
44
+ enum class Presence { PRESENT, ABSENT, UNKNOWN }
45
+
46
+ /**
47
+ * Hard links to the file behind this descriptor, or -1 when unknowable.
48
+ *
49
+ * -1 is not zero: a caller must treat it as "no evidence of a problem"
50
+ * rather than as a deletion, or every write on a platform without `fstat`
51
+ * would look like it had lost its file.
52
+ */
53
+ fun linkCount(descriptor: FileDescriptor): Int
54
+
55
+ /** Commits a directory's own metadata — the removals — to storage. */
56
+ fun syncDirectory(directory: File): Boolean
57
+
58
+ /**
59
+ * Whether [file] exists, without following a final symlink.
60
+ *
61
+ * Returns [Presence.UNKNOWN] for any failure that is not a plain "no such
62
+ * file": a permissions problem, an I/O error, a volume that went away. The
63
+ * purge reports those as failures rather than as successful deletions.
64
+ */
65
+ fun lookup(file: File): Presence
66
+
67
+ /**
68
+ * Whether the final component of [file] is a symbolic link.
69
+ *
70
+ * Returns true when it cannot be established. Refusing costs a log file;
71
+ * guessing wrong writes one wherever the link points — a path the caller
72
+ * never named and the purge would never clean.
73
+ */
74
+ fun isSymbolicLink(file: File): Boolean
75
+
76
+ /**
77
+ * Restricts an artifact to its owner: 0700 for directories, 0600 for files.
78
+ *
79
+ * Returns false if any part did not take, which the writer records as a
80
+ * `protection` degradation rather than a failure. Android's app-private
81
+ * storage is already inaccessible to other apps; these modes are the second
82
+ * layer, and losing them is worth reporting but not worth refusing to log
83
+ * over.
84
+ */
85
+ fun restrictToOwner(file: File, isDirectory: Boolean): Boolean
86
+
87
+ /**
88
+ * Creation time in epoch millis, or null when the platform cannot say.
89
+ *
90
+ * Only ever used to *seed* the age sidecar for a file that has none — the
91
+ * sidecar is authoritative from then on. See `LogFileWriter.creationTimeOf`
92
+ * for why the filesystem's answer cannot be trusted directly.
93
+ */
94
+ fun creationTimeMillis(file: File): Long?
95
+
96
+ /**
97
+ * The unit-test implementation. **Not for Android**: it is the only place in
98
+ * this package that touches `java.nio.file`, which does not exist below API
99
+ * 26.
100
+ */
101
+ object Jvm : PlatformIo {
102
+ override fun linkCount(descriptor: FileDescriptor): Int = -1
103
+
104
+ override fun syncDirectory(directory: File): Boolean = directory.isDirectory
105
+
106
+ override fun lookup(file: File): Presence = Nio.lookup(file)
107
+
108
+ override fun isSymbolicLink(file: File): Boolean = Nio.isSymbolicLink(file)
109
+
110
+ override fun restrictToOwner(file: File, isDirectory: Boolean): Boolean {
111
+ // `false` as the second argument on the deny calls means "everyone",
112
+ // which is the point: strip group and other first, then grant the owner
113
+ // back. Doing it the other way round leaves a window where the file is
114
+ // readable by anyone.
115
+ var ok = file.setReadable(false, false)
116
+ ok = file.setWritable(false, false) && ok
117
+ ok = file.setExecutable(false, false) && ok
118
+ ok = file.setReadable(true, true) && ok
119
+ ok = file.setWritable(true, true) && ok
120
+ if (isDirectory) ok = file.setExecutable(true, true) && ok
121
+ return ok
122
+ }
123
+
124
+ override fun creationTimeMillis(file: File): Long? = Nio.creationTimeMillis(file)
125
+
126
+ /**
127
+ * Every `java.nio.file` reference in the package, in one object that
128
+ * nothing on Android ever calls.
129
+ *
130
+ * Held separately from [Jvm] rather than inlined because
131
+ * [AndroidPlatformIo] *does* resolve [Jvm] — for its `restrictToOwner`
132
+ * fallback, which uses only `java.io` — and resolving it must not drag
133
+ * `java.nio.file` into an Android class load.
134
+ */
135
+ private object Nio {
136
+ fun lookup(file: File): Presence = try {
137
+ val path = file.toPath()
138
+ val noFollow = arrayOf(java.nio.file.LinkOption.NOFOLLOW_LINKS)
139
+ when {
140
+ java.nio.file.Files.exists(path, *noFollow) -> Presence.PRESENT
141
+ java.nio.file.Files.notExists(path, *noFollow) -> Presence.ABSENT
142
+ else -> Presence.UNKNOWN
143
+ }
144
+ } catch (_: Throwable) {
145
+ Presence.UNKNOWN
146
+ }
147
+
148
+ fun isSymbolicLink(file: File): Boolean = try {
149
+ java.nio.file.Files.isSymbolicLink(file.toPath())
150
+ } catch (_: Throwable) {
151
+ true
152
+ }
153
+
154
+ fun creationTimeMillis(file: File): Long? = try {
155
+ val attributes = java.nio.file.Files.readAttributes(
156
+ file.toPath(),
157
+ java.nio.file.attribute.BasicFileAttributes::class.java
158
+ )
159
+ attributes.creationTime().toMillis().takeIf { it > 0 }
160
+ } catch (_: Throwable) {
161
+ null
162
+ }
163
+ }
164
+ }
165
+ }