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.
- package/LICENSE +20 -0
- package/NitroLogger.podspec +29 -0
- package/README.md +214 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +124 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/AndroidPlatformIo.kt +100 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridFileSink.kt +292 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridNativeConsoleSink.kt +23 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogFileWriter.kt +1328 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogRotationPolicy.kt +186 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogSecureFile.kt +80 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogWriterRegistry.kt +420 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NativeConsoleWriter.kt +237 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NitroLoggerPackage.kt +22 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/PlatformIo.kt +165 -0
- package/docs/PARITY.md +330 -0
- package/docs/PRIVACY.md +341 -0
- package/eslint-plugin/README.md +238 -0
- package/eslint-plugin/index.js +62 -0
- package/eslint-plugin/rules/literal-subsystem.js +102 -0
- package/eslint-plugin/rules/no-computed-metadata-key.js +174 -0
- package/eslint-plugin/rules/no-derived-correlation.js +171 -0
- package/eslint-plugin/rules/no-dynamic-message.js +60 -0
- package/eslint-plugin/shared.js +1948 -0
- package/ios/Gzip.swift +203 -0
- package/ios/HybridFileSink.swift +291 -0
- package/ios/HybridNativeConsoleSink.swift +20 -0
- package/ios/LogFileWriter.swift +1280 -0
- package/ios/LogSecureFile.swift +296 -0
- package/ios/LogWriterRegistry.swift +444 -0
- package/ios/NativeConsoleWriter.swift +180 -0
- package/lib/module/Logger.js +381 -0
- package/lib/module/Logger.js.map +1 -0
- package/lib/module/ScopedLogger.js +70 -0
- package/lib/module/ScopedLogger.js.map +1 -0
- package/lib/module/config.js +23 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/destinations/Batcher.js +647 -0
- package/lib/module/destinations/Batcher.js.map +1 -0
- package/lib/module/destinations/ConsoleDestination.js +51 -0
- package/lib/module/destinations/ConsoleDestination.js.map +1 -0
- package/lib/module/destinations/FileDestination.js +360 -0
- package/lib/module/destinations/FileDestination.js.map +1 -0
- package/lib/module/destinations/NativeConsoleDestination.js +170 -0
- package/lib/module/destinations/NativeConsoleDestination.js.map +1 -0
- package/lib/module/destinations/types.js +4 -0
- package/lib/module/destinations/types.js.map +1 -0
- package/lib/module/formatters/DefaultFormatter.js +136 -0
- package/lib/module/formatters/DefaultFormatter.js.map +1 -0
- package/lib/module/formatters/JsonLinesFormatter.js +162 -0
- package/lib/module/formatters/JsonLinesFormatter.js.map +1 -0
- package/lib/module/formatters/timestamp.js +15 -0
- package/lib/module/formatters/timestamp.js.map +1 -0
- package/lib/module/formatters/types.js +4 -0
- package/lib/module/formatters/types.js.map +1 -0
- package/lib/module/index.js +31 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/integrations/appState.js +81 -0
- package/lib/module/integrations/appState.js.map +1 -0
- package/lib/module/integrations/errorHandler.js +166 -0
- package/lib/module/integrations/errorHandler.js.map +1 -0
- package/lib/module/integrations/sanitizeError.js +238 -0
- package/lib/module/integrations/sanitizeError.js.map +1 -0
- package/lib/module/levels.js +35 -0
- package/lib/module/levels.js.map +1 -0
- package/lib/module/metadata.js +52 -0
- package/lib/module/metadata.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/privacy.js +349 -0
- package/lib/module/privacy.js.map +1 -0
- package/lib/module/specs/FileSink.nitro.js +4 -0
- package/lib/module/specs/FileSink.nitro.js.map +1 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js +4 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utf8.js +42 -0
- package/lib/module/utf8.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/Logger.d.ts +122 -0
- package/lib/typescript/src/Logger.d.ts.map +1 -0
- package/lib/typescript/src/ScopedLogger.d.ts +36 -0
- package/lib/typescript/src/ScopedLogger.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +12 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/destinations/Batcher.d.ts +244 -0
- package/lib/typescript/src/destinations/Batcher.d.ts.map +1 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts +27 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts +200 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts +86 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/types.d.ts +19 -0
- package/lib/typescript/src/destinations/types.d.ts.map +1 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts +22 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts +73 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/timestamp.d.ts +6 -0
- package/lib/typescript/src/formatters/timestamp.d.ts.map +1 -0
- package/lib/typescript/src/formatters/types.d.ts +27 -0
- package/lib/typescript/src/formatters/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +34 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/integrations/appState.d.ts +33 -0
- package/lib/typescript/src/integrations/appState.d.ts.map +1 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts +55 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts.map +1 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts +89 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts.map +1 -0
- package/lib/typescript/src/levels.d.ts +9 -0
- package/lib/typescript/src/levels.d.ts.map +1 -0
- package/lib/typescript/src/metadata.d.ts +23 -0
- package/lib/typescript/src/metadata.d.ts.map +1 -0
- package/lib/typescript/src/privacy.d.ts +103 -0
- package/lib/typescript/src/privacy.d.ts.map +1 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts +131 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts +21 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +37 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utf8.d.ts +15 -0
- package/lib/typescript/src/utf8.d.ts.map +1 -0
- package/nitro.json +33 -0
- package/nitrogen/generated/android/c++/JAppendResult.hpp +79 -0
- package/nitrogen/generated/android/c++/JClearOutcome.hpp +88 -0
- package/nitrogen/generated/android/c++/JFlushOutcome.hpp +81 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.cpp +120 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.cpp +71 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JRejectReason.hpp +64 -0
- package/nitrogen/generated/android/c++/JRotationConfig.hpp +77 -0
- package/nitrogen/generated/android/c++/JSinkStatus.hpp +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/AppendResult.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/ClearOutcome.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/FlushOutcome.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridFileSinkSpec.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridNativeConsoleSinkSpec.kt +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RejectReason.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RotationConfig.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/SinkStatus.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/nitrologgerOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitrologger+autolinking.cmake +83 -0
- package/nitrogen/generated/android/nitrologger+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.cpp +70 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/NitroLogger+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.cpp +50 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.hpp +216 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Umbrella.hpp +69 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.mm +41 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.hpp +150 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/swift/AppendResult.swift +60 -0
- package/nitrogen/generated/ios/swift/ClearOutcome.swift +50 -0
- package/nitrogen/generated/ios/swift/FlushOutcome.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec.swift +61 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec_cxx.swift +227 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec_cxx.swift +148 -0
- package/nitrogen/generated/ios/swift/RejectReason.swift +48 -0
- package/nitrogen/generated/ios/swift/RotationConfig.swift +93 -0
- package/nitrogen/generated/ios/swift/SinkStatus.swift +44 -0
- package/nitrogen/generated/shared/c++/AppendResult.hpp +105 -0
- package/nitrogen/generated/shared/c++/ClearOutcome.hpp +96 -0
- package/nitrogen/generated/shared/c++/FlushOutcome.hpp +107 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.cpp +28 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.hpp +84 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/shared/c++/RejectReason.hpp +84 -0
- package/nitrogen/generated/shared/c++/RotationConfig.hpp +103 -0
- package/nitrogen/generated/shared/c++/SinkStatus.hpp +95 -0
- package/package.json +151 -0
- package/src/Logger.ts +417 -0
- package/src/ScopedLogger.ts +94 -0
- package/src/config.ts +25 -0
- package/src/destinations/Batcher.ts +805 -0
- package/src/destinations/ConsoleDestination.ts +60 -0
- package/src/destinations/FileDestination.ts +473 -0
- package/src/destinations/NativeConsoleDestination.ts +220 -0
- package/src/destinations/types.ts +19 -0
- package/src/formatters/DefaultFormatter.ts +150 -0
- package/src/formatters/JsonLinesFormatter.ts +195 -0
- package/src/formatters/timestamp.ts +12 -0
- package/src/formatters/types.ts +29 -0
- package/src/index.tsx +106 -0
- package/src/integrations/appState.ts +111 -0
- package/src/integrations/errorHandler.ts +223 -0
- package/src/integrations/sanitizeError.ts +287 -0
- package/src/levels.ts +35 -0
- package/src/metadata.ts +52 -0
- package/src/privacy.ts +389 -0
- package/src/specs/FileSink.nitro.ts +144 -0
- package/src/specs/NativeConsoleSink.nitro.ts +21 -0
- package/src/types.ts +45 -0
- package/src/utf8.ts +39 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Creating log files and directories the way a file full of patient data has
|
|
4
|
+
/// to be created.
|
|
5
|
+
///
|
|
6
|
+
/// Every artifact the writer produces goes through here — the directory, the
|
|
7
|
+
/// active file, each archive, the gzip temporary, the recovery staging file —
|
|
8
|
+
/// because the protections are only worth anything if they hold on all of
|
|
9
|
+
/// them. An archive that inherits default permissions is exactly as readable
|
|
10
|
+
/// as the log it was rotated from.
|
|
11
|
+
///
|
|
12
|
+
/// Three separate things are being asked for, and they fail independently:
|
|
13
|
+
///
|
|
14
|
+
/// - **POSIX modes**, type-specific. A directory needs `0700`: owner read,
|
|
15
|
+
/// write, and *search*, since without the execute bit nothing inside it can
|
|
16
|
+
/// be reached by path. A file needs `0600` — an executable log file is a
|
|
17
|
+
/// contradiction, and `0700` on a file is the kind of thing that gets copied
|
|
18
|
+
/// into the next project.
|
|
19
|
+
/// - **Data protection**, `completeUntilFirstUserAuthentication`. Not
|
|
20
|
+
/// `complete`: the crash handler and the background flush both write while
|
|
21
|
+
/// the device may be locked, and a stricter class would turn those writes
|
|
22
|
+
/// into silent failures at exactly the moment the log matters. This class
|
|
23
|
+
/// still means the file is unreadable until the user has unlocked once
|
|
24
|
+
/// since boot.
|
|
25
|
+
/// - **Backup exclusion.** Logs must not ride an iCloud or iTunes backup off
|
|
26
|
+
/// the device.
|
|
27
|
+
///
|
|
28
|
+
/// **Every mode is read back after it is set.** Asking for `0600` and assuming
|
|
29
|
+
/// it took is how a world-readable file inherited from a restored backup gets
|
|
30
|
+
/// treated as protected: `chmod` can fail, and `setAttributes` swallows the
|
|
31
|
+
/// reason. What matters is the mode that is on disk afterwards, so that is what
|
|
32
|
+
/// gets checked and what gets reported.
|
|
33
|
+
///
|
|
34
|
+
/// Failures are reported rather than thrown. A log file that exists with the
|
|
35
|
+
/// wrong protection class is a problem the app should know about; a log file
|
|
36
|
+
/// that could not be created at all is a worse one.
|
|
37
|
+
internal enum LogSecureFile {
|
|
38
|
+
|
|
39
|
+
/// What could not be applied. Empty means everything held.
|
|
40
|
+
struct Shortfall: OptionSet {
|
|
41
|
+
let rawValue: Int
|
|
42
|
+
/// The data-protection class or backup exclusion did not stick.
|
|
43
|
+
static let protection = Shortfall(rawValue: 1 << 0)
|
|
44
|
+
/// The POSIX mode on disk is not what it should be — the artifact is
|
|
45
|
+
/// readable by more than its owner, or a directory is not traversable.
|
|
46
|
+
static let permissions = Shortfall(rawValue: 1 << 1)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static let directoryMode: Int = 0o700
|
|
50
|
+
static let fileMode: Int = 0o600
|
|
51
|
+
|
|
52
|
+
/// Creates `url` and every missing parent as owner-only directories.
|
|
53
|
+
///
|
|
54
|
+
/// **A directory this call did not create is inspected, never changed.** The
|
|
55
|
+
/// log path is caller-supplied and nothing constrains it to a directory this
|
|
56
|
+
/// package owns — `<Documents>/app.log` is a perfectly ordinary thing to ask
|
|
57
|
+
/// for, and `Documents` belongs to the host app. Every one of the three
|
|
58
|
+
/// protections is a property of the *directory* and outlives this sink:
|
|
59
|
+
///
|
|
60
|
+
/// - the backup exclusion is persistent and directory-wide, so claiming it
|
|
61
|
+
/// silently takes the app's whole `Documents` tree out of iCloud backup;
|
|
62
|
+
/// - the protection class is inherited by every file created in there
|
|
63
|
+
/// afterwards, including files this package never writes;
|
|
64
|
+
/// - even the mode is not ours. `0700` looks like harmless tightening, but it
|
|
65
|
+
/// strips group and other access from a directory that may be deliberately
|
|
66
|
+
/// shared — an app group, an extension, a helper process — and the app has
|
|
67
|
+
/// no way to know we did it.
|
|
68
|
+
///
|
|
69
|
+
/// So a pre-existing directory keeps its own policy and a loose mode comes
|
|
70
|
+
/// back as `permissions`, which the writer surfaces as a `protection`
|
|
71
|
+
/// degradation for the app to act on. Report rather than mutate is the rule
|
|
72
|
+
/// the rest of this writer already follows, and the reason the app is told at
|
|
73
|
+
/// all: a world-readable `Logs` left behind by an upgrade or a restored
|
|
74
|
+
/// backup is real, it just is not ours to silently reconfigure.
|
|
75
|
+
///
|
|
76
|
+
/// A directory this call *creates* is unambiguously ours and gets all three.
|
|
77
|
+
///
|
|
78
|
+
/// **Ownership is decided by `mkdir` itself, not by a check before it.** A
|
|
79
|
+
/// `lstat` that says "nothing here" is a statement about the past by the time
|
|
80
|
+
/// the directory is created, and the gap between the two is long enough for
|
|
81
|
+
/// another process to put a directory — or a symlink to one — in the way.
|
|
82
|
+
/// Losing that race means claiming somebody else's directory, which is the
|
|
83
|
+
/// whole thing this is here to prevent. `mkdir` cannot be raced: it either
|
|
84
|
+
/// creates the leaf, in which case it is ours, or it returns `EEXIST`, in
|
|
85
|
+
/// which case it is not, and the kernel decides which with no window in
|
|
86
|
+
/// between. It does not follow symlinks either, so a link already at the path
|
|
87
|
+
/// is `EEXIST` and is inspected rather than claimed through.
|
|
88
|
+
@discardableResult
|
|
89
|
+
static func createDirectory(at url: URL) throws -> Shortfall {
|
|
90
|
+
// Parents first, so the leaf `mkdir` has somewhere to land. The same rule
|
|
91
|
+
// applies to them by construction: `withIntermediateDirectories` gives the
|
|
92
|
+
// mode only to directories it creates and leaves existing ones alone.
|
|
93
|
+
try FileManager.default.createDirectory(
|
|
94
|
+
at: url.deletingLastPathComponent(),
|
|
95
|
+
withIntermediateDirectories: true,
|
|
96
|
+
attributes: [.posixPermissions: directoryMode]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
// Subject to the umask, which is why `secure` re-asserts the mode and reads
|
|
100
|
+
// it back rather than trusting this argument.
|
|
101
|
+
if mkdir(url.path, mode_t(directoryMode)) == 0 {
|
|
102
|
+
return secure(url, isDirectory: true)
|
|
103
|
+
}
|
|
104
|
+
let failure = errno
|
|
105
|
+
guard failure == EEXIST else { throw posixError(failure, at: url) }
|
|
106
|
+
|
|
107
|
+
// `EEXIST` says something is there, not that it is a directory: a regular
|
|
108
|
+
// file, a symlink to one, or a dangling link all answer the same way. That
|
|
109
|
+
// is a configuration error and has to stay one. Reporting it as a mere
|
|
110
|
+
// shortfall would leave the sink to fail on the open a moment later, and a
|
|
111
|
+
// destination that accepts records into a path that can never hold them is
|
|
112
|
+
// worse than one that refuses to start. `stat`, not `lstat` — a symlink to
|
|
113
|
+
// a real directory is a usable directory, and is inspected like any other
|
|
114
|
+
// thing this call did not create.
|
|
115
|
+
var existing = stat()
|
|
116
|
+
if stat(url.path, &existing) != 0 {
|
|
117
|
+
// Its own errno, not a blanket `ENOTDIR`: a dangling link is `ENOENT`, an
|
|
118
|
+
// unreadable parent is `EACCES`, and failing storage is `EIO`. Those want
|
|
119
|
+
// different responses from whoever reads the log about the log.
|
|
120
|
+
let cause = errno
|
|
121
|
+
throw posixError(cause, at: url)
|
|
122
|
+
}
|
|
123
|
+
guard (existing.st_mode & S_IFMT) == S_IFDIR else {
|
|
124
|
+
throw posixError(ENOTDIR, at: url)
|
|
125
|
+
}
|
|
126
|
+
return inspect(url)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// The path is safe to carry here: this error is consumed inside the writer —
|
|
130
|
+
/// `attemptReopen` discards it, and the Nitro adapter replaces every open
|
|
131
|
+
/// failure with a payload-free message before anything crosses into
|
|
132
|
+
/// JavaScript, precisely because a path is the kind of string that carries a
|
|
133
|
+
/// username.
|
|
134
|
+
private static func posixError(_ code: Int32, at url: URL) -> NSError {
|
|
135
|
+
NSError(domain: NSPOSIXErrorDomain, code: Int(code),
|
|
136
|
+
userInfo: [NSFilePathErrorKey: url.path])
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// What a directory we did not create falls short of, changing nothing.
|
|
140
|
+
///
|
|
141
|
+
/// Only the mode is reportable. Whether the app excluded its own directory
|
|
142
|
+
/// from backup, or what protection class it chose, are its decisions and not
|
|
143
|
+
/// shortfalls — the writer is not entitled to an opinion on either.
|
|
144
|
+
private static func inspect(_ url: URL) -> Shortfall {
|
|
145
|
+
hasExpectedMode(url, isDirectory: true) ? [] : .permissions
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Applies the protections to an artifact that already exists — an archive
|
|
149
|
+
/// just moved into place, a gzip staging file, a directory.
|
|
150
|
+
///
|
|
151
|
+
/// Called after every `moveItem` as well as after every create: a move
|
|
152
|
+
/// preserves the source's attributes on the same volume, but says nothing
|
|
153
|
+
/// about a destination created by some other means, and the whole point is
|
|
154
|
+
/// that no artifact is exempt.
|
|
155
|
+
///
|
|
156
|
+
/// Only ever called on an artifact this writer owns. `createDirectory`
|
|
157
|
+
/// decides that question for directories and does not call this when the
|
|
158
|
+
/// answer is no.
|
|
159
|
+
@discardableResult
|
|
160
|
+
static func secure(_ url: URL, isDirectory: Bool = false) -> Shortfall {
|
|
161
|
+
var shortfall = Shortfall()
|
|
162
|
+
let wanted = mode_t(isDirectory ? directoryMode : fileMode)
|
|
163
|
+
|
|
164
|
+
if chmod(url.path, wanted) != 0 { shortfall.insert(.permissions) }
|
|
165
|
+
if currentMode(of: url, followingLinks: isDirectory) != wanted {
|
|
166
|
+
shortfall.insert(.permissions)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
shortfall.formUnion(protect(url, isDirectory: isDirectory))
|
|
170
|
+
return shortfall
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// The same, through an open descriptor.
|
|
174
|
+
///
|
|
175
|
+
/// `fchmod` cannot be redirected: it acts on the file the caller already
|
|
176
|
+
/// holds, so nothing can swap a symlink in between deciding on a path and
|
|
177
|
+
/// changing its mode. Preferred wherever a descriptor is available, which is
|
|
178
|
+
/// everywhere the active log file is concerned.
|
|
179
|
+
///
|
|
180
|
+
/// The protection class and backup exclusion have no descriptor-based
|
|
181
|
+
/// *setter*, so they still go through the path — but what they did is then
|
|
182
|
+
/// **read back through the descriptor**, never through the name.
|
|
183
|
+
///
|
|
184
|
+
/// Comparing inodes after the fact is not enough, and that is the whole
|
|
185
|
+
/// reason this is written the long way. `lstat(path)` matching the descriptor
|
|
186
|
+
/// proves only that the name points here *now*. A rename away, a decoy
|
|
187
|
+
/// created under the name, the protections landing on the decoy, and a rename
|
|
188
|
+
/// back leaves every inode comparison agreeing while this app's file has no
|
|
189
|
+
/// protection at all. Asking the descriptor whether the xattr and the
|
|
190
|
+
/// protection class are actually on it has no such window: there is no name
|
|
191
|
+
/// in the question, so there is nothing to swap.
|
|
192
|
+
@discardableResult
|
|
193
|
+
static func secure(descriptor: Int32, at url: URL) -> Shortfall {
|
|
194
|
+
var shortfall = Shortfall()
|
|
195
|
+
let wanted = mode_t(fileMode)
|
|
196
|
+
|
|
197
|
+
if fchmod(descriptor, wanted) != 0 { shortfall.insert(.permissions) }
|
|
198
|
+
var opened = stat()
|
|
199
|
+
guard fstat(descriptor, &opened) == 0 else { return [.permissions, .protection] }
|
|
200
|
+
if (opened.st_mode & 0o7777) != wanted { shortfall.insert(.permissions) }
|
|
201
|
+
|
|
202
|
+
shortfall.formUnion(protect(url, isDirectory: false))
|
|
203
|
+
if !isProtected(descriptor: descriptor) { shortfall.insert(.protection) }
|
|
204
|
+
return shortfall
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Whether `url` still names the inode described by `info`.
|
|
208
|
+
///
|
|
209
|
+
/// A point-in-time answer, and only ever used as one: it tells the writer
|
|
210
|
+
/// whether the descriptor it just opened is still reachable under the path
|
|
211
|
+
/// that rotation and purge will address it by. It is *not* evidence about
|
|
212
|
+
/// protections — see `secure(descriptor:at:)` for why.
|
|
213
|
+
static func namesSameFile(_ url: URL, as info: stat) -> Bool {
|
|
214
|
+
var onDisk = stat()
|
|
215
|
+
guard lstat(url.path, &onDisk) == 0 else { return false }
|
|
216
|
+
return onDisk.st_dev == info.st_dev && onDisk.st_ino == info.st_ino
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// The extended attribute `isExcludedFromBackup` writes. Reading it back is
|
|
220
|
+
/// the only way to ask the *file* — rather than a path — whether exclusion
|
|
221
|
+
/// stuck.
|
|
222
|
+
private static let backupExclusionKey = "com.apple.metadata:com_apple_backup_excludeItem"
|
|
223
|
+
|
|
224
|
+
/// From `<sys/fcntl.h>`. Spelled out rather than imported: these macros are
|
|
225
|
+
/// not surfaced to Swift consistently across SDKs, and a missing symbol would
|
|
226
|
+
/// fail the build over a check that exists to be belt-and-braces.
|
|
227
|
+
private static let getProtectionClassCommand: Int32 = 63
|
|
228
|
+
private static let protectionClassCompleteUntilFirstAuth: Int32 = 3
|
|
229
|
+
|
|
230
|
+
/// Whether the protections are on the file this descriptor holds.
|
|
231
|
+
static func isProtected(descriptor: Int32) -> Bool {
|
|
232
|
+
guard fgetxattr(descriptor, backupExclusionKey, nil, 0, 0, 0) >= 0 else { return false }
|
|
233
|
+
|
|
234
|
+
#if os(iOS) || os(tvOS) || os(watchOS)
|
|
235
|
+
// Only iOS-family filesystems carry a protection class; asking elsewhere
|
|
236
|
+
// returns an error that would be read as a shortfall on every single file.
|
|
237
|
+
let assigned = fcntl(descriptor, getProtectionClassCommand)
|
|
238
|
+
guard assigned == protectionClassCompleteUntilFirstAuth else { return false }
|
|
239
|
+
#endif
|
|
240
|
+
|
|
241
|
+
return true
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Data protection plus backup exclusion, reported rather than thrown.
|
|
245
|
+
private static func protect(_ url: URL, isDirectory: Bool) -> Shortfall {
|
|
246
|
+
var shortfall = Shortfall()
|
|
247
|
+
|
|
248
|
+
#if os(iOS) || os(tvOS) || os(watchOS)
|
|
249
|
+
// Directories carry a protection class too, and it is what newly created
|
|
250
|
+
// children inherit — setting it only on files leaves anything the system
|
|
251
|
+
// creates in there unprotected.
|
|
252
|
+
do {
|
|
253
|
+
try FileManager.default.setAttributes(
|
|
254
|
+
[.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication],
|
|
255
|
+
ofItemAtPath: url.path
|
|
256
|
+
)
|
|
257
|
+
} catch {
|
|
258
|
+
shortfall.insert(.protection)
|
|
259
|
+
}
|
|
260
|
+
#endif
|
|
261
|
+
|
|
262
|
+
var resource = URLResourceValues()
|
|
263
|
+
resource.isExcludedFromBackup = true
|
|
264
|
+
var mutable = url
|
|
265
|
+
do {
|
|
266
|
+
try mutable.setResourceValues(resource)
|
|
267
|
+
} catch {
|
|
268
|
+
shortfall.insert(.protection)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_ = isDirectory
|
|
272
|
+
return shortfall
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// The mode actually on disk.
|
|
276
|
+
///
|
|
277
|
+
/// Following links has to match what `chmod` just did, or the check
|
|
278
|
+
/// contradicts the change. A log *directory* reached through a symlink is
|
|
279
|
+
/// supported — the registry resolves it, and `chmod` follows it — so
|
|
280
|
+
/// verifying that one with `lstat` would read the link's own `0755` and
|
|
281
|
+
/// report a shortfall against a target that is correctly `0700`. A log
|
|
282
|
+
/// *file*, by contrast, must never be a link at all, so there `lstat` is the
|
|
283
|
+
/// point: the mode of the link itself is exactly what we want to see.
|
|
284
|
+
static func currentMode(of url: URL, followingLinks: Bool) -> mode_t? {
|
|
285
|
+
var info = stat()
|
|
286
|
+
let ok = followingLinks ? stat(url.path, &info) == 0 : lstat(url.path, &info) == 0
|
|
287
|
+
guard ok else { return nil }
|
|
288
|
+
return info.st_mode & 0o7777
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Whether `url`'s POSIX mode is exactly what this type should have.
|
|
292
|
+
static func hasExpectedMode(_ url: URL, isDirectory: Bool) -> Bool {
|
|
293
|
+
currentMode(of: url, followingLinks: isDirectory)
|
|
294
|
+
== mode_t(isDirectory ? directoryMode : fileMode)
|
|
295
|
+
}
|
|
296
|
+
}
|