@hasna/recordings 0.2.11 → 0.2.13
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/README.md +349 -69
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1919 -98
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1973 -236
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
#define _DARWIN_C_SOURCE 1
|
|
2
|
+
#define _GNU_SOURCE 1
|
|
3
|
+
#include <node_api.h>
|
|
4
|
+
|
|
5
|
+
#include <dirent.h>
|
|
6
|
+
#include <errno.h>
|
|
7
|
+
#include <fcntl.h>
|
|
8
|
+
#include <stdint.h>
|
|
9
|
+
#include <stdbool.h>
|
|
10
|
+
#include <signal.h>
|
|
11
|
+
#include <stdio.h>
|
|
12
|
+
#include <stdlib.h>
|
|
13
|
+
#include <string.h>
|
|
14
|
+
#include <sys/stat.h>
|
|
15
|
+
#include <sys/types.h>
|
|
16
|
+
#include <time.h>
|
|
17
|
+
#include <unistd.h>
|
|
18
|
+
#ifdef __APPLE__
|
|
19
|
+
#include <sys/acl.h>
|
|
20
|
+
#elif defined(__linux__)
|
|
21
|
+
#include <sys/xattr.h>
|
|
22
|
+
#endif
|
|
23
|
+
#ifdef __linux__
|
|
24
|
+
#include <sys/syscall.h>
|
|
25
|
+
#endif
|
|
26
|
+
|
|
27
|
+
#ifndef O_CLOEXEC
|
|
28
|
+
#define O_CLOEXEC 0
|
|
29
|
+
#endif
|
|
30
|
+
|
|
31
|
+
typedef struct {
|
|
32
|
+
int fd;
|
|
33
|
+
int closed;
|
|
34
|
+
} recordings_handle;
|
|
35
|
+
|
|
36
|
+
typedef struct {
|
|
37
|
+
uint32_t state[8];
|
|
38
|
+
uint64_t bit_count;
|
|
39
|
+
unsigned char buffer[64];
|
|
40
|
+
size_t buffer_length;
|
|
41
|
+
} sha256_context;
|
|
42
|
+
|
|
43
|
+
static uint32_t rotate_right(uint32_t value, uint32_t count) {
|
|
44
|
+
return (value >> count) | (value << (32U - count));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static void sha256_transform(sha256_context *context, const unsigned char block[64]) {
|
|
48
|
+
static const uint32_t constants[64] = {
|
|
49
|
+
0x428a2f98U,0x71374491U,0xb5c0fbcfU,0xe9b5dba5U,0x3956c25bU,0x59f111f1U,0x923f82a4U,0xab1c5ed5U,
|
|
50
|
+
0xd807aa98U,0x12835b01U,0x243185beU,0x550c7dc3U,0x72be5d74U,0x80deb1feU,0x9bdc06a7U,0xc19bf174U,
|
|
51
|
+
0xe49b69c1U,0xefbe4786U,0x0fc19dc6U,0x240ca1ccU,0x2de92c6fU,0x4a7484aaU,0x5cb0a9dcU,0x76f988daU,
|
|
52
|
+
0x983e5152U,0xa831c66dU,0xb00327c8U,0xbf597fc7U,0xc6e00bf3U,0xd5a79147U,0x06ca6351U,0x14292967U,
|
|
53
|
+
0x27b70a85U,0x2e1b2138U,0x4d2c6dfcU,0x53380d13U,0x650a7354U,0x766a0abbU,0x81c2c92eU,0x92722c85U,
|
|
54
|
+
0xa2bfe8a1U,0xa81a664bU,0xc24b8b70U,0xc76c51a3U,0xd192e819U,0xd6990624U,0xf40e3585U,0x106aa070U,
|
|
55
|
+
0x19a4c116U,0x1e376c08U,0x2748774cU,0x34b0bcb5U,0x391c0cb3U,0x4ed8aa4aU,0x5b9cca4fU,0x682e6ff3U,
|
|
56
|
+
0x748f82eeU,0x78a5636fU,0x84c87814U,0x8cc70208U,0x90befffaU,0xa4506cebU,0xbef9a3f7U,0xc67178f2U,
|
|
57
|
+
};
|
|
58
|
+
uint32_t words[64];
|
|
59
|
+
for (size_t index = 0; index < 16; index++) {
|
|
60
|
+
words[index] = ((uint32_t)block[index * 4] << 24) |
|
|
61
|
+
((uint32_t)block[index * 4 + 1] << 16) |
|
|
62
|
+
((uint32_t)block[index * 4 + 2] << 8) |
|
|
63
|
+
(uint32_t)block[index * 4 + 3];
|
|
64
|
+
}
|
|
65
|
+
for (size_t index = 16; index < 64; index++) {
|
|
66
|
+
uint32_t s0 = rotate_right(words[index - 15], 7) ^ rotate_right(words[index - 15], 18) ^
|
|
67
|
+
(words[index - 15] >> 3);
|
|
68
|
+
uint32_t s1 = rotate_right(words[index - 2], 17) ^ rotate_right(words[index - 2], 19) ^
|
|
69
|
+
(words[index - 2] >> 10);
|
|
70
|
+
words[index] = words[index - 16] + s0 + words[index - 7] + s1;
|
|
71
|
+
}
|
|
72
|
+
uint32_t a=context->state[0], b=context->state[1], c=context->state[2], d=context->state[3];
|
|
73
|
+
uint32_t e=context->state[4], f=context->state[5], g=context->state[6], h=context->state[7];
|
|
74
|
+
for (size_t index = 0; index < 64; index++) {
|
|
75
|
+
uint32_t s1=rotate_right(e,6)^rotate_right(e,11)^rotate_right(e,25);
|
|
76
|
+
uint32_t choice=(e&f)^((~e)&g);
|
|
77
|
+
uint32_t temporary1=h+s1+choice+constants[index]+words[index];
|
|
78
|
+
uint32_t s0=rotate_right(a,2)^rotate_right(a,13)^rotate_right(a,22);
|
|
79
|
+
uint32_t majority=(a&b)^(a&c)^(b&c);
|
|
80
|
+
uint32_t temporary2=s0+majority;
|
|
81
|
+
h=g; g=f; f=e; e=d+temporary1; d=c; c=b; b=a; a=temporary1+temporary2;
|
|
82
|
+
}
|
|
83
|
+
context->state[0]+=a; context->state[1]+=b; context->state[2]+=c; context->state[3]+=d;
|
|
84
|
+
context->state[4]+=e; context->state[5]+=f; context->state[6]+=g; context->state[7]+=h;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static void sha256_init(sha256_context *context) {
|
|
88
|
+
static const uint32_t initial[8] = {
|
|
89
|
+
0x6a09e667U,0xbb67ae85U,0x3c6ef372U,0xa54ff53aU,
|
|
90
|
+
0x510e527fU,0x9b05688cU,0x1f83d9abU,0x5be0cd19U,
|
|
91
|
+
};
|
|
92
|
+
memcpy(context->state, initial, sizeof(initial));
|
|
93
|
+
context->bit_count = 0;
|
|
94
|
+
context->buffer_length = 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static void sha256_update(sha256_context *context, const unsigned char *bytes, size_t length) {
|
|
98
|
+
context->bit_count += (uint64_t)length * 8U;
|
|
99
|
+
while (length > 0) {
|
|
100
|
+
size_t available = 64 - context->buffer_length;
|
|
101
|
+
size_t count = length < available ? length : available;
|
|
102
|
+
memcpy(context->buffer + context->buffer_length, bytes, count);
|
|
103
|
+
context->buffer_length += count;
|
|
104
|
+
bytes += count;
|
|
105
|
+
length -= count;
|
|
106
|
+
if (context->buffer_length == 64) {
|
|
107
|
+
sha256_transform(context, context->buffer);
|
|
108
|
+
context->buffer_length = 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static void sha256_final(sha256_context *context, unsigned char digest[32]) {
|
|
114
|
+
uint64_t original_bits = context->bit_count;
|
|
115
|
+
unsigned char one = 0x80;
|
|
116
|
+
unsigned char zero = 0;
|
|
117
|
+
sha256_update(context, &one, 1);
|
|
118
|
+
while (context->buffer_length != 56) sha256_update(context, &zero, 1);
|
|
119
|
+
unsigned char length[8];
|
|
120
|
+
for (size_t index = 0; index < 8; index++) length[7 - index] = (unsigned char)(original_bits >> (index * 8));
|
|
121
|
+
sha256_update(context, length, 8);
|
|
122
|
+
for (size_t index = 0; index < 8; index++) {
|
|
123
|
+
digest[index*4]=(unsigned char)(context->state[index]>>24);
|
|
124
|
+
digest[index*4+1]=(unsigned char)(context->state[index]>>16);
|
|
125
|
+
digest[index*4+2]=(unsigned char)(context->state[index]>>8);
|
|
126
|
+
digest[index*4+3]=(unsigned char)context->state[index];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static void throw_errno(napi_env env, const char *operation) {
|
|
131
|
+
char code[32];
|
|
132
|
+
char message[320];
|
|
133
|
+
snprintf(code, sizeof(code), "ERRNO_%d", errno);
|
|
134
|
+
snprintf(message, sizeof(message), "%s failed: %s", operation, strerror(errno));
|
|
135
|
+
napi_throw_error(env, code, message);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static void throw_message(napi_env env, const char *code, const char *message) {
|
|
139
|
+
napi_throw_error(env, code, message);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static int check_napi(napi_env env, napi_status status, const char *operation) {
|
|
143
|
+
if (status == napi_ok) return 1;
|
|
144
|
+
const napi_extended_error_info *info = NULL;
|
|
145
|
+
napi_get_last_error_info(env, &info);
|
|
146
|
+
napi_throw_error(env, "NAPI_ERROR", info && info->error_message ? info->error_message : operation);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static void finalize_handle(napi_env env, void *data, void *hint) {
|
|
151
|
+
(void)env;
|
|
152
|
+
(void)hint;
|
|
153
|
+
recordings_handle *handle = (recordings_handle *)data;
|
|
154
|
+
if (handle != NULL) {
|
|
155
|
+
if (!handle->closed && handle->fd >= 0) close(handle->fd);
|
|
156
|
+
free(handle);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static napi_value make_handle(napi_env env, int fd) {
|
|
161
|
+
napi_value object;
|
|
162
|
+
recordings_handle *handle = calloc(1, sizeof(*handle));
|
|
163
|
+
if (handle == NULL) {
|
|
164
|
+
close(fd);
|
|
165
|
+
throw_message(env, "ENOMEM", "could not allocate native filesystem handle");
|
|
166
|
+
return NULL;
|
|
167
|
+
}
|
|
168
|
+
handle->fd = fd;
|
|
169
|
+
if (!check_napi(env, napi_create_object(env, &object), "create handle") ||
|
|
170
|
+
!check_napi(env, napi_wrap(env, object, handle, finalize_handle, NULL, NULL), "wrap handle")) {
|
|
171
|
+
finalize_handle(env, handle, NULL);
|
|
172
|
+
return NULL;
|
|
173
|
+
}
|
|
174
|
+
return object;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static recordings_handle *get_handle(napi_env env, napi_value value) {
|
|
178
|
+
recordings_handle *handle = NULL;
|
|
179
|
+
if (!check_napi(env, napi_unwrap(env, value, (void **)&handle), "unwrap handle")) return NULL;
|
|
180
|
+
if (handle == NULL || handle->closed || handle->fd < 0) {
|
|
181
|
+
throw_message(env, "HANDLE_CLOSED", "native filesystem handle is closed");
|
|
182
|
+
return NULL;
|
|
183
|
+
}
|
|
184
|
+
return handle;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static char *get_string(napi_env env, napi_value value, const char *label) {
|
|
188
|
+
size_t length = 0;
|
|
189
|
+
if (!check_napi(env, napi_get_value_string_utf8(env, value, NULL, 0, &length), label)) return NULL;
|
|
190
|
+
char *result = malloc(length + 1);
|
|
191
|
+
if (result == NULL) {
|
|
192
|
+
throw_message(env, "ENOMEM", "could not allocate native string");
|
|
193
|
+
return NULL;
|
|
194
|
+
}
|
|
195
|
+
size_t written = 0;
|
|
196
|
+
if (!check_napi(env, napi_get_value_string_utf8(env, value, result, length + 1, &written), label)) {
|
|
197
|
+
free(result);
|
|
198
|
+
return NULL;
|
|
199
|
+
}
|
|
200
|
+
if (written != length || memchr(result, '\0', length) != NULL) {
|
|
201
|
+
free(result);
|
|
202
|
+
throw_message(env, "INVALID_PATH", "native path contains an embedded NUL");
|
|
203
|
+
return NULL;
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static int valid_leaf(const char *leaf) {
|
|
209
|
+
return leaf != NULL && leaf[0] != '\0' && strcmp(leaf, ".") != 0 &&
|
|
210
|
+
strcmp(leaf, "..") != 0 && strchr(leaf, '/') == NULL;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static char *get_leaf(napi_env env, napi_value value) {
|
|
214
|
+
char *leaf = get_string(env, value, "read path leaf");
|
|
215
|
+
if (leaf != NULL && !valid_leaf(leaf)) {
|
|
216
|
+
free(leaf);
|
|
217
|
+
throw_message(env, "INVALID_LEAF", "native filesystem leaf must be one non-dot path component");
|
|
218
|
+
return NULL;
|
|
219
|
+
}
|
|
220
|
+
return leaf;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
static int get_uint32(napi_env env, napi_value value, uint32_t *result, const char *label) {
|
|
224
|
+
return check_napi(env, napi_get_value_uint32(env, value, result), label);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
static napi_value make_boolean(napi_env env, int value) {
|
|
228
|
+
napi_value result;
|
|
229
|
+
if (!check_napi(env, napi_get_boolean(env, value != 0, &result), "create boolean")) return NULL;
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
static const char *stat_type(const struct stat *details) {
|
|
234
|
+
if (S_ISREG(details->st_mode)) return "file";
|
|
235
|
+
if (S_ISDIR(details->st_mode)) return "directory";
|
|
236
|
+
if (S_ISLNK(details->st_mode)) return "symlink";
|
|
237
|
+
return "special";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static napi_value metadata_value(napi_env env, const struct stat *details) {
|
|
241
|
+
napi_value object, value;
|
|
242
|
+
if (!check_napi(env, napi_create_object(env, &object), "create metadata")) return NULL;
|
|
243
|
+
#define SET_BIGINT(name, number) \
|
|
244
|
+
do { \
|
|
245
|
+
if (!check_napi(env, napi_create_bigint_uint64(env, (uint64_t)(number), &value), "create bigint") || \
|
|
246
|
+
!check_napi(env, napi_set_named_property(env, object, (name), value), "set bigint")) return NULL; \
|
|
247
|
+
} while (0)
|
|
248
|
+
#define SET_UINT(name, number) \
|
|
249
|
+
do { \
|
|
250
|
+
if (!check_napi(env, napi_create_uint32(env, (uint32_t)(number), &value), "create uint") || \
|
|
251
|
+
!check_napi(env, napi_set_named_property(env, object, (name), value), "set uint")) return NULL; \
|
|
252
|
+
} while (0)
|
|
253
|
+
SET_BIGINT("dev", details->st_dev);
|
|
254
|
+
SET_BIGINT("ino", details->st_ino);
|
|
255
|
+
SET_BIGINT("size", details->st_size);
|
|
256
|
+
SET_UINT("uid", details->st_uid);
|
|
257
|
+
SET_UINT("mode", details->st_mode & 07777);
|
|
258
|
+
SET_UINT("nlink", details->st_nlink);
|
|
259
|
+
if (!check_napi(env, napi_create_string_utf8(env, stat_type(details), NAPI_AUTO_LENGTH, &value), "create type") ||
|
|
260
|
+
!check_napi(env, napi_set_named_property(env, object, "type", value), "set type")) return NULL;
|
|
261
|
+
#undef SET_BIGINT
|
|
262
|
+
#undef SET_UINT
|
|
263
|
+
return object;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
static napi_value open_trusted_home(napi_env env, napi_callback_info info) {
|
|
267
|
+
napi_value argv[2];
|
|
268
|
+
size_t argc = 2;
|
|
269
|
+
uint32_t expected_uid;
|
|
270
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "openTrustedHome args") || argc != 2 ||
|
|
271
|
+
!get_uint32(env, argv[1], &expected_uid, "read expected uid")) return NULL;
|
|
272
|
+
char *path = get_string(env, argv[0], "read trusted home path");
|
|
273
|
+
if (path == NULL) return NULL;
|
|
274
|
+
size_t length = strlen(path);
|
|
275
|
+
if (path[0] != '/' || length < 2 || path[length - 1] == '/' || strstr(path, "//") != NULL) {
|
|
276
|
+
free(path);
|
|
277
|
+
throw_message(env, "INVALID_HOME", "trusted home must be a canonical absolute non-root path");
|
|
278
|
+
return NULL;
|
|
279
|
+
}
|
|
280
|
+
int fd = open("/", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
281
|
+
if (fd < 0) {
|
|
282
|
+
free(path);
|
|
283
|
+
throw_errno(env, "open root");
|
|
284
|
+
return NULL;
|
|
285
|
+
}
|
|
286
|
+
char *cursor = path + 1;
|
|
287
|
+
char *save = NULL;
|
|
288
|
+
for (char *component = strtok_r(cursor, "/", &save); component != NULL;
|
|
289
|
+
component = strtok_r(NULL, "/", &save)) {
|
|
290
|
+
if (!valid_leaf(component)) {
|
|
291
|
+
close(fd);
|
|
292
|
+
free(path);
|
|
293
|
+
throw_message(env, "INVALID_HOME", "trusted home contains an unsafe component");
|
|
294
|
+
return NULL;
|
|
295
|
+
}
|
|
296
|
+
int next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
297
|
+
if (next < 0) {
|
|
298
|
+
close(fd);
|
|
299
|
+
free(path);
|
|
300
|
+
throw_errno(env, "open trusted home component");
|
|
301
|
+
return NULL;
|
|
302
|
+
}
|
|
303
|
+
close(fd);
|
|
304
|
+
fd = next;
|
|
305
|
+
}
|
|
306
|
+
free(path);
|
|
307
|
+
struct stat details;
|
|
308
|
+
if (fstat(fd, &details) != 0 || !S_ISDIR(details.st_mode) || details.st_uid != (uid_t)expected_uid) {
|
|
309
|
+
if (errno == 0) errno = EPERM;
|
|
310
|
+
close(fd);
|
|
311
|
+
throw_errno(env, "validate trusted home");
|
|
312
|
+
return NULL;
|
|
313
|
+
}
|
|
314
|
+
return make_handle(env, fd);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
static napi_value open_dir_at(napi_env env, napi_callback_info info) {
|
|
318
|
+
napi_value argv[2];
|
|
319
|
+
size_t argc = 2;
|
|
320
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "openDirAt args") || argc != 2) return NULL;
|
|
321
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
322
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
323
|
+
if (parent == NULL || leaf == NULL) { free(leaf); return NULL; }
|
|
324
|
+
int fd = openat(parent->fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
325
|
+
free(leaf);
|
|
326
|
+
if (fd < 0) { throw_errno(env, "open directory at capability"); return NULL; }
|
|
327
|
+
return make_handle(env, fd);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
static napi_value open_regular_at(napi_env env, napi_callback_info info) {
|
|
331
|
+
napi_value argv[3];
|
|
332
|
+
size_t argc = 3;
|
|
333
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "openRegularAt args") || argc != 3) return NULL;
|
|
334
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
335
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
336
|
+
char *access = get_string(env, argv[2], "read regular access");
|
|
337
|
+
if (parent == NULL || leaf == NULL || access == NULL) { free(leaf); free(access); return NULL; }
|
|
338
|
+
int flags;
|
|
339
|
+
mode_t mode = 0600;
|
|
340
|
+
if (strcmp(access, "read") == 0) flags = O_RDONLY | O_NOFOLLOW | O_CLOEXEC;
|
|
341
|
+
else if (strcmp(access, "createExclusive") == 0) {
|
|
342
|
+
flags = O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC;
|
|
343
|
+
} else {
|
|
344
|
+
free(leaf); free(access);
|
|
345
|
+
throw_message(env, "INVALID_ACCESS", "regular access must be read or createExclusive");
|
|
346
|
+
return NULL;
|
|
347
|
+
}
|
|
348
|
+
int fd = openat(parent->fd, leaf, flags, mode);
|
|
349
|
+
free(leaf); free(access);
|
|
350
|
+
if (fd < 0) { throw_errno(env, "open regular file at capability"); return NULL; }
|
|
351
|
+
struct stat details;
|
|
352
|
+
if (fstat(fd, &details) != 0 || !S_ISREG(details.st_mode)) {
|
|
353
|
+
if (errno == 0) errno = EINVAL;
|
|
354
|
+
close(fd);
|
|
355
|
+
throw_errno(env, "validate regular file at capability");
|
|
356
|
+
return NULL;
|
|
357
|
+
}
|
|
358
|
+
return make_handle(env, fd);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
static napi_value read_dir(napi_env env, napi_callback_info info) {
|
|
362
|
+
napi_value argv[1], array;
|
|
363
|
+
size_t argc = 1;
|
|
364
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "readDir args") || argc != 1) return NULL;
|
|
365
|
+
recordings_handle *directory = get_handle(env, argv[0]);
|
|
366
|
+
if (directory == NULL) return NULL;
|
|
367
|
+
int iterator_fd = openat(directory->fd, ".", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
368
|
+
if (iterator_fd < 0) { throw_errno(env, "reopen directory capability"); return NULL; }
|
|
369
|
+
DIR *stream = fdopendir(iterator_fd);
|
|
370
|
+
if (stream == NULL) { close(iterator_fd); throw_errno(env, "open directory stream"); return NULL; }
|
|
371
|
+
if (!check_napi(env, napi_create_array(env, &array), "create directory array")) { closedir(stream); return NULL; }
|
|
372
|
+
uint32_t index = 0;
|
|
373
|
+
errno = 0;
|
|
374
|
+
for (struct dirent *entry = readdir(stream); entry != NULL; entry = readdir(stream)) {
|
|
375
|
+
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
|
|
376
|
+
if (!valid_leaf(entry->d_name)) {
|
|
377
|
+
closedir(stream);
|
|
378
|
+
throw_message(env, "INVALID_DIRENT", "directory returned an unsafe entry name");
|
|
379
|
+
return NULL;
|
|
380
|
+
}
|
|
381
|
+
napi_value name;
|
|
382
|
+
if (!check_napi(env, napi_create_string_utf8(env, entry->d_name, NAPI_AUTO_LENGTH, &name), "create entry name") ||
|
|
383
|
+
!check_napi(env, napi_set_element(env, array, index++, name), "append entry name")) {
|
|
384
|
+
closedir(stream);
|
|
385
|
+
return NULL;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
int saved_errno = errno;
|
|
389
|
+
closedir(stream);
|
|
390
|
+
if (saved_errno != 0) { errno = saved_errno; throw_errno(env, "read directory capability"); return NULL; }
|
|
391
|
+
return array;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
static napi_value stat_handle(napi_env env, napi_callback_info info) {
|
|
395
|
+
napi_value argv[1];
|
|
396
|
+
size_t argc = 1;
|
|
397
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "statHandle args") || argc != 1) return NULL;
|
|
398
|
+
recordings_handle *handle = get_handle(env, argv[0]);
|
|
399
|
+
struct stat details;
|
|
400
|
+
if (handle == NULL) return NULL;
|
|
401
|
+
if (fstat(handle->fd, &details) != 0) { throw_errno(env, "stat capability"); return NULL; }
|
|
402
|
+
return metadata_value(env, &details);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
static napi_value stat_at(napi_env env, napi_callback_info info) {
|
|
406
|
+
napi_value argv[2];
|
|
407
|
+
size_t argc = 2;
|
|
408
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "statAt args") || argc != 2) return NULL;
|
|
409
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
410
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
411
|
+
if (parent == NULL || leaf == NULL) { free(leaf); return NULL; }
|
|
412
|
+
struct stat details;
|
|
413
|
+
int result = fstatat(parent->fd, leaf, &details, AT_SYMLINK_NOFOLLOW);
|
|
414
|
+
free(leaf);
|
|
415
|
+
if (result != 0 && errno == ENOENT) {
|
|
416
|
+
napi_value null_value;
|
|
417
|
+
napi_get_null(env, &null_value);
|
|
418
|
+
return null_value;
|
|
419
|
+
}
|
|
420
|
+
if (result != 0) { throw_errno(env, "stat at capability"); return NULL; }
|
|
421
|
+
return metadata_value(env, &details);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
static napi_value mkdir_at(napi_env env, napi_callback_info info) {
|
|
425
|
+
napi_value argv[3];
|
|
426
|
+
size_t argc = 3;
|
|
427
|
+
uint32_t mode;
|
|
428
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "mkdirAt args") || argc != 3 ||
|
|
429
|
+
!get_uint32(env, argv[2], &mode, "read directory mode")) return NULL;
|
|
430
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
431
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
432
|
+
if (parent == NULL || leaf == NULL) { free(leaf); return NULL; }
|
|
433
|
+
if (mkdirat(parent->fd, leaf, (mode_t)(mode & 0777)) != 0) {
|
|
434
|
+
free(leaf); throw_errno(env, "create directory at capability"); return NULL;
|
|
435
|
+
}
|
|
436
|
+
int fd = openat(parent->fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
437
|
+
if (fd < 0) {
|
|
438
|
+
int saved_errno = errno;
|
|
439
|
+
unlinkat(parent->fd, leaf, AT_REMOVEDIR);
|
|
440
|
+
free(leaf); errno = saved_errno; throw_errno(env, "open created directory at capability"); return NULL;
|
|
441
|
+
}
|
|
442
|
+
if (fchmod(fd, (mode_t)(mode & 0777)) != 0 || fsync(fd) != 0) {
|
|
443
|
+
int saved_errno = errno;
|
|
444
|
+
close(fd); unlinkat(parent->fd, leaf, AT_REMOVEDIR);
|
|
445
|
+
free(leaf); errno = saved_errno; throw_errno(env, "secure created directory at capability"); return NULL;
|
|
446
|
+
}
|
|
447
|
+
free(leaf);
|
|
448
|
+
return make_handle(env, fd);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
static int unwrap_two_parents_and_leaves(
|
|
452
|
+
napi_env env, napi_callback_info info, recordings_handle **left_parent, char **left_leaf,
|
|
453
|
+
recordings_handle **right_parent, char **right_leaf) {
|
|
454
|
+
napi_value argv[4];
|
|
455
|
+
size_t argc = 4;
|
|
456
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "filesystem operation args") || argc != 4) return 0;
|
|
457
|
+
*left_parent = get_handle(env, argv[0]);
|
|
458
|
+
*left_leaf = get_leaf(env, argv[1]);
|
|
459
|
+
*right_parent = get_handle(env, argv[2]);
|
|
460
|
+
*right_leaf = get_leaf(env, argv[3]);
|
|
461
|
+
if (*left_parent == NULL || *left_leaf == NULL || *right_parent == NULL || *right_leaf == NULL) {
|
|
462
|
+
free(*left_leaf); free(*right_leaf); return 0;
|
|
463
|
+
}
|
|
464
|
+
return 1;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
static napi_value link_no_replace_at(napi_env env, napi_callback_info info) {
|
|
468
|
+
recordings_handle *source_parent, *destination_parent;
|
|
469
|
+
char *source_leaf = NULL, *destination_leaf = NULL;
|
|
470
|
+
if (!unwrap_two_parents_and_leaves(env, info, &source_parent, &source_leaf,
|
|
471
|
+
&destination_parent, &destination_leaf)) return NULL;
|
|
472
|
+
int result = linkat(source_parent->fd, source_leaf, destination_parent->fd, destination_leaf, 0);
|
|
473
|
+
int saved_errno = errno;
|
|
474
|
+
free(source_leaf); free(destination_leaf);
|
|
475
|
+
if (result == 0) return make_boolean(env, 1);
|
|
476
|
+
if (saved_errno == EEXIST) return make_boolean(env, 0);
|
|
477
|
+
errno = saved_errno; throw_errno(env, "link without replacement at capability"); return NULL;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
static int rename_no_replace(int source_parent_fd, const char *source_leaf,
|
|
481
|
+
int destination_parent_fd, const char *destination_leaf) {
|
|
482
|
+
#ifdef __APPLE__
|
|
483
|
+
return renameatx_np(source_parent_fd, source_leaf, destination_parent_fd,
|
|
484
|
+
destination_leaf, RENAME_EXCL);
|
|
485
|
+
#elif defined(__linux__) && defined(SYS_renameat2)
|
|
486
|
+
return (int)syscall(SYS_renameat2, source_parent_fd, source_leaf,
|
|
487
|
+
destination_parent_fd, destination_leaf, 1U);
|
|
488
|
+
#else
|
|
489
|
+
errno = ENOTSUP;
|
|
490
|
+
return -1;
|
|
491
|
+
#endif
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
static napi_value rename_no_replace_at(napi_env env, napi_callback_info info) {
|
|
495
|
+
recordings_handle *source_parent, *destination_parent;
|
|
496
|
+
char *source_leaf = NULL, *destination_leaf = NULL;
|
|
497
|
+
if (!unwrap_two_parents_and_leaves(env, info, &source_parent, &source_leaf,
|
|
498
|
+
&destination_parent, &destination_leaf)) return NULL;
|
|
499
|
+
int result = rename_no_replace(source_parent->fd, source_leaf,
|
|
500
|
+
destination_parent->fd, destination_leaf);
|
|
501
|
+
int saved_errno = errno;
|
|
502
|
+
free(source_leaf); free(destination_leaf);
|
|
503
|
+
if (result != 0) { errno = saved_errno; throw_errno(env, "rename without replacement at capability"); return NULL; }
|
|
504
|
+
napi_value undefined;
|
|
505
|
+
napi_get_undefined(env, &undefined);
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
static napi_value rename_handle_no_replace_at(napi_env env, napi_callback_info info) {
|
|
510
|
+
napi_value argv[5];
|
|
511
|
+
size_t argc = 5;
|
|
512
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL),
|
|
513
|
+
"renameHandleNoReplaceAt args") || argc != 5) return NULL;
|
|
514
|
+
recordings_handle *source_parent = get_handle(env, argv[0]);
|
|
515
|
+
char *source_leaf = get_leaf(env, argv[1]);
|
|
516
|
+
recordings_handle *source = get_handle(env, argv[2]);
|
|
517
|
+
recordings_handle *destination_parent = get_handle(env, argv[3]);
|
|
518
|
+
char *destination_leaf = get_leaf(env, argv[4]);
|
|
519
|
+
if (source_parent == NULL || source_leaf == NULL || source == NULL ||
|
|
520
|
+
destination_parent == NULL || destination_leaf == NULL) {
|
|
521
|
+
free(source_leaf); free(destination_leaf); return NULL;
|
|
522
|
+
}
|
|
523
|
+
struct stat opened, named;
|
|
524
|
+
int valid_source = fstat(source->fd, &opened) == 0 &&
|
|
525
|
+
fstatat(source_parent->fd, source_leaf, &named, AT_SYMLINK_NOFOLLOW) == 0 &&
|
|
526
|
+
opened.st_dev == named.st_dev && opened.st_ino == named.st_ino;
|
|
527
|
+
if (!valid_source) {
|
|
528
|
+
free(source_leaf); free(destination_leaf); errno = ESTALE;
|
|
529
|
+
throw_errno(env, "retained rename source binding"); return NULL;
|
|
530
|
+
}
|
|
531
|
+
int result = rename_no_replace(source_parent->fd, source_leaf,
|
|
532
|
+
destination_parent->fd, destination_leaf);
|
|
533
|
+
int saved_errno = errno;
|
|
534
|
+
int valid_destination = result == 0 &&
|
|
535
|
+
fstatat(destination_parent->fd, destination_leaf, &named, AT_SYMLINK_NOFOLLOW) == 0 &&
|
|
536
|
+
opened.st_dev == named.st_dev && opened.st_ino == named.st_ino;
|
|
537
|
+
if (result == 0 && !valid_destination) {
|
|
538
|
+
struct stat rebound, rollback_destination;
|
|
539
|
+
int source_missing =
|
|
540
|
+
fstatat(source_parent->fd, source_leaf, &rebound, AT_SYMLINK_NOFOLLOW) != 0 &&
|
|
541
|
+
errno == ENOENT;
|
|
542
|
+
int destination_still_retained = source_missing &&
|
|
543
|
+
fstatat(destination_parent->fd, destination_leaf, &rollback_destination,
|
|
544
|
+
AT_SYMLINK_NOFOLLOW) == 0 &&
|
|
545
|
+
opened.st_dev == rollback_destination.st_dev &&
|
|
546
|
+
opened.st_ino == rollback_destination.st_ino;
|
|
547
|
+
if (destination_still_retained) {
|
|
548
|
+
(void)rename_no_replace(destination_parent->fd, destination_leaf,
|
|
549
|
+
source_parent->fd, source_leaf);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
free(source_leaf); free(destination_leaf);
|
|
553
|
+
if (result != 0 || !valid_destination) {
|
|
554
|
+
errno = result != 0 ? saved_errno : ESTALE;
|
|
555
|
+
throw_errno(env, "rename retained source without replacement at capability");
|
|
556
|
+
return NULL;
|
|
557
|
+
}
|
|
558
|
+
napi_value undefined;
|
|
559
|
+
napi_get_undefined(env, &undefined);
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
static napi_value rename_replace_at(napi_env env, napi_callback_info info) {
|
|
564
|
+
recordings_handle *source_parent, *destination_parent;
|
|
565
|
+
char *source_leaf = NULL, *destination_leaf = NULL;
|
|
566
|
+
if (!unwrap_two_parents_and_leaves(env, info, &source_parent, &source_leaf,
|
|
567
|
+
&destination_parent, &destination_leaf)) return NULL;
|
|
568
|
+
int result = renameat(source_parent->fd, source_leaf, destination_parent->fd, destination_leaf);
|
|
569
|
+
int saved_errno = errno;
|
|
570
|
+
free(source_leaf); free(destination_leaf);
|
|
571
|
+
if (result != 0) { errno = saved_errno; throw_errno(env, "rename with replacement at capability"); return NULL; }
|
|
572
|
+
napi_value undefined;
|
|
573
|
+
napi_get_undefined(env, &undefined);
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
static napi_value unlink_at_common(napi_env env, napi_callback_info info, int flags) {
|
|
578
|
+
napi_value argv[2];
|
|
579
|
+
size_t argc = 2;
|
|
580
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "unlinkAt args") || argc != 2) return NULL;
|
|
581
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
582
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
583
|
+
if (parent == NULL || leaf == NULL) { free(leaf); return NULL; }
|
|
584
|
+
int result = unlinkat(parent->fd, leaf, flags);
|
|
585
|
+
int saved_errno = errno;
|
|
586
|
+
free(leaf);
|
|
587
|
+
if (result != 0) { errno = saved_errno; throw_errno(env, "unlink at capability"); return NULL; }
|
|
588
|
+
napi_value undefined;
|
|
589
|
+
napi_get_undefined(env, &undefined);
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
static napi_value unlink_file_at(napi_env env, napi_callback_info info) {
|
|
594
|
+
return unlink_at_common(env, info, 0);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
static napi_value unlink_dir_at(napi_env env, napi_callback_info info) {
|
|
598
|
+
return unlink_at_common(env, info, AT_REMOVEDIR);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
static napi_value same_binding(napi_env env, napi_callback_info info) {
|
|
602
|
+
napi_value argv[3];
|
|
603
|
+
size_t argc = 3;
|
|
604
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "sameBinding args") || argc != 3) return NULL;
|
|
605
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
606
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
607
|
+
recordings_handle *child = get_handle(env, argv[2]);
|
|
608
|
+
if (parent == NULL || leaf == NULL || child == NULL) { free(leaf); return NULL; }
|
|
609
|
+
struct stat named, opened;
|
|
610
|
+
int named_result = fstatat(parent->fd, leaf, &named, AT_SYMLINK_NOFOLLOW);
|
|
611
|
+
int opened_result = fstat(child->fd, &opened);
|
|
612
|
+
free(leaf);
|
|
613
|
+
if (named_result != 0 && errno == ENOENT) return make_boolean(env, 0);
|
|
614
|
+
if (named_result != 0 || opened_result != 0) { throw_errno(env, "compare capability binding"); return NULL; }
|
|
615
|
+
return make_boolean(env, named.st_dev == opened.st_dev && named.st_ino == opened.st_ino);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
static napi_value fsync_handle(napi_env env, napi_callback_info info) {
|
|
619
|
+
napi_value argv[1];
|
|
620
|
+
size_t argc = 1;
|
|
621
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "fsyncHandle args") || argc != 1) return NULL;
|
|
622
|
+
recordings_handle *handle = get_handle(env, argv[0]);
|
|
623
|
+
if (handle == NULL) return NULL;
|
|
624
|
+
if (fsync(handle->fd) != 0) { throw_errno(env, "fsync capability"); return NULL; }
|
|
625
|
+
napi_value undefined;
|
|
626
|
+
napi_get_undefined(env, &undefined);
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
static napi_value handle_has_no_extended_acl(napi_env env, napi_callback_info info) {
|
|
631
|
+
napi_value argv[1];
|
|
632
|
+
size_t argc = 1;
|
|
633
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL),
|
|
634
|
+
"handleHasNoExtendedAcl args") || argc != 1) return NULL;
|
|
635
|
+
recordings_handle *handle = get_handle(env, argv[0]);
|
|
636
|
+
if (handle == NULL) return NULL;
|
|
637
|
+
#ifdef __APPLE__
|
|
638
|
+
errno = 0;
|
|
639
|
+
acl_t acl = acl_get_fd_np(handle->fd, ACL_TYPE_EXTENDED);
|
|
640
|
+
if (acl == NULL) return make_boolean(env, errno == ENOENT);
|
|
641
|
+
(void)acl_free(acl);
|
|
642
|
+
return make_boolean(env, 0);
|
|
643
|
+
#elif defined(__linux__)
|
|
644
|
+
errno = 0;
|
|
645
|
+
ssize_t length = flistxattr(handle->fd, NULL, 0);
|
|
646
|
+
if (length < 0) return make_boolean(env, 0);
|
|
647
|
+
if (length == 0) return make_boolean(env, 1);
|
|
648
|
+
char *names = malloc((size_t)length);
|
|
649
|
+
if (names == NULL) return make_boolean(env, 0);
|
|
650
|
+
ssize_t read_length = flistxattr(handle->fd, names, (size_t)length);
|
|
651
|
+
if (read_length != length) {
|
|
652
|
+
free(names);
|
|
653
|
+
return make_boolean(env, 0);
|
|
654
|
+
}
|
|
655
|
+
int safe = 1;
|
|
656
|
+
for (ssize_t offset = 0; offset < length;) {
|
|
657
|
+
size_t remaining = (size_t)(length - offset);
|
|
658
|
+
size_t name_length = strnlen(names + offset, remaining);
|
|
659
|
+
if (name_length == remaining) { safe = 0; break; }
|
|
660
|
+
if (strcmp(names + offset, "system.posix_acl_access") == 0 ||
|
|
661
|
+
strcmp(names + offset, "system.posix_acl_default") == 0) {
|
|
662
|
+
safe = 0;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
offset += (ssize_t)name_length + 1;
|
|
666
|
+
}
|
|
667
|
+
free(names);
|
|
668
|
+
return make_boolean(env, safe);
|
|
669
|
+
#else
|
|
670
|
+
return make_boolean(env, 0);
|
|
671
|
+
#endif
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
static napi_value chmod_handle(napi_env env, napi_callback_info info) {
|
|
675
|
+
napi_value argv[2];
|
|
676
|
+
size_t argc = 2;
|
|
677
|
+
uint32_t mode;
|
|
678
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "chmodHandle args") || argc != 2 ||
|
|
679
|
+
!get_uint32(env, argv[1], &mode, "read handle mode")) return NULL;
|
|
680
|
+
recordings_handle *handle = get_handle(env, argv[0]);
|
|
681
|
+
if (handle == NULL) return NULL;
|
|
682
|
+
if (fchmod(handle->fd, (mode_t)(mode & 0777)) != 0 || fsync(handle->fd) != 0) {
|
|
683
|
+
throw_errno(env, "chmod capability"); return NULL;
|
|
684
|
+
}
|
|
685
|
+
napi_value undefined;
|
|
686
|
+
napi_get_undefined(env, &undefined);
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
static int get_boolean(napi_env env, napi_value value, bool *result, const char *label) {
|
|
691
|
+
return check_napi(env, napi_get_value_bool(env, value, result), label);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
static napi_value write_file_at(napi_env env, napi_callback_info info) {
|
|
695
|
+
napi_value argv[4];
|
|
696
|
+
size_t argc = 4;
|
|
697
|
+
uint32_t mode;
|
|
698
|
+
void *bytes = NULL;
|
|
699
|
+
size_t length = 0;
|
|
700
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "writeFileAt args") || argc != 4 ||
|
|
701
|
+
!check_napi(env, napi_get_buffer_info(env, argv[2], &bytes, &length), "read write buffer") ||
|
|
702
|
+
!get_uint32(env, argv[3], &mode, "read file mode")) return NULL;
|
|
703
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
704
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
705
|
+
if (parent == NULL || leaf == NULL) { free(leaf); return NULL; }
|
|
706
|
+
int fd = openat(parent->fd, leaf, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC,
|
|
707
|
+
(mode_t)(mode & 0777));
|
|
708
|
+
int saved_errno = errno;
|
|
709
|
+
free(leaf);
|
|
710
|
+
if (fd < 0) { errno = saved_errno; throw_errno(env, "create file at capability"); return NULL; }
|
|
711
|
+
size_t offset = 0;
|
|
712
|
+
while (offset < length) {
|
|
713
|
+
ssize_t count = write(fd, (const unsigned char *)bytes + offset, length - offset);
|
|
714
|
+
if (count <= 0) {
|
|
715
|
+
saved_errno = errno == 0 ? EIO : errno;
|
|
716
|
+
close(fd); errno = saved_errno; throw_errno(env, "write file at capability"); return NULL;
|
|
717
|
+
}
|
|
718
|
+
offset += (size_t)count;
|
|
719
|
+
}
|
|
720
|
+
if (fsync(fd) != 0) {
|
|
721
|
+
saved_errno = errno; close(fd); errno = saved_errno; throw_errno(env, "fsync written file"); return NULL;
|
|
722
|
+
}
|
|
723
|
+
if (close(fd) != 0) { throw_errno(env, "close written file"); return NULL; }
|
|
724
|
+
napi_value undefined;
|
|
725
|
+
napi_get_undefined(env, &undefined);
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
static int open_bound_regular(int parent_fd, const char *leaf, struct stat *before) {
|
|
730
|
+
int fd = openat(parent_fd, leaf, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
|
731
|
+
if (fd < 0) return -1;
|
|
732
|
+
if (fstat(fd, before) != 0 || !S_ISREG(before->st_mode)) {
|
|
733
|
+
int saved_errno = errno == 0 ? EINVAL : errno;
|
|
734
|
+
close(fd); errno = saved_errno; return -1;
|
|
735
|
+
}
|
|
736
|
+
return fd;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
static int same_timespec(struct timespec left, struct timespec right) {
|
|
740
|
+
return left.tv_sec == right.tv_sec && left.tv_nsec == right.tv_nsec;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
static int same_regular_snapshot(const struct stat *before, const struct stat *after) {
|
|
744
|
+
#ifdef __APPLE__
|
|
745
|
+
return before->st_dev == after->st_dev && before->st_ino == after->st_ino &&
|
|
746
|
+
before->st_size == after->st_size &&
|
|
747
|
+
same_timespec(before->st_mtimespec, after->st_mtimespec) &&
|
|
748
|
+
same_timespec(before->st_ctimespec, after->st_ctimespec);
|
|
749
|
+
#else
|
|
750
|
+
return before->st_dev == after->st_dev && before->st_ino == after->st_ino &&
|
|
751
|
+
before->st_size == after->st_size &&
|
|
752
|
+
same_timespec(before->st_mtim, after->st_mtim) &&
|
|
753
|
+
same_timespec(before->st_ctim, after->st_ctim);
|
|
754
|
+
#endif
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
static int regular_still_bound(int parent_fd, const char *leaf, int fd, const struct stat *before) {
|
|
758
|
+
struct stat after, named;
|
|
759
|
+
return fstat(fd, &after) == 0 && fstatat(parent_fd, leaf, &named, AT_SYMLINK_NOFOLLOW) == 0 &&
|
|
760
|
+
same_regular_snapshot(before, &after) &&
|
|
761
|
+
after.st_dev == named.st_dev && after.st_ino == named.st_ino;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
static napi_value read_regular_at(napi_env env, napi_callback_info info) {
|
|
765
|
+
napi_value argv[3], result;
|
|
766
|
+
size_t argc = 3;
|
|
767
|
+
uint32_t maximum;
|
|
768
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "readRegularAt args") || argc != 3 ||
|
|
769
|
+
!get_uint32(env, argv[2], &maximum, "read maximum bytes")) return NULL;
|
|
770
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
771
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
772
|
+
if (parent == NULL || leaf == NULL) { free(leaf); return NULL; }
|
|
773
|
+
struct stat before;
|
|
774
|
+
int fd = open_bound_regular(parent->fd, leaf, &before);
|
|
775
|
+
if (fd < 0) { int saved_errno=errno; free(leaf); errno=saved_errno; throw_errno(env,"open bounded regular file"); return NULL; }
|
|
776
|
+
if (before.st_size < 0 || (uint64_t)before.st_size > maximum) {
|
|
777
|
+
close(fd); free(leaf); throw_message(env,"FILE_TOO_LARGE","bounded native file exceeds its size limit"); return NULL;
|
|
778
|
+
}
|
|
779
|
+
void *bytes = NULL;
|
|
780
|
+
if (!check_napi(env, napi_create_buffer(env, (size_t)before.st_size, &bytes, &result), "create bounded file buffer")) {
|
|
781
|
+
close(fd); free(leaf); return NULL;
|
|
782
|
+
}
|
|
783
|
+
size_t offset = 0;
|
|
784
|
+
while (offset < (size_t)before.st_size) {
|
|
785
|
+
ssize_t count = pread(fd, (unsigned char *)bytes + offset, (size_t)before.st_size - offset, (off_t)offset);
|
|
786
|
+
if (count <= 0) {
|
|
787
|
+
int saved_errno=errno==0?EIO:errno; close(fd); free(leaf); errno=saved_errno; throw_errno(env,"read bounded regular file"); return NULL;
|
|
788
|
+
}
|
|
789
|
+
offset += (size_t)count;
|
|
790
|
+
}
|
|
791
|
+
if (!regular_still_bound(parent->fd, leaf, fd, &before)) {
|
|
792
|
+
close(fd); free(leaf); throw_message(env,"BINDING_CHANGED","bounded native file changed while being read"); return NULL;
|
|
793
|
+
}
|
|
794
|
+
close(fd); free(leaf);
|
|
795
|
+
return result;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
static napi_value sha256_regular_at(napi_env env, napi_callback_info info) {
|
|
799
|
+
napi_value argv[2], result;
|
|
800
|
+
size_t argc = 2;
|
|
801
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "sha256RegularAt args") || argc != 2) return NULL;
|
|
802
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
803
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
804
|
+
if (parent == NULL || leaf == NULL) { free(leaf); return NULL; }
|
|
805
|
+
struct stat before;
|
|
806
|
+
int fd = open_bound_regular(parent->fd, leaf, &before);
|
|
807
|
+
if (fd < 0) { int saved_errno=errno; free(leaf); errno=saved_errno; throw_errno(env,"open hash input"); return NULL; }
|
|
808
|
+
sha256_context context;
|
|
809
|
+
sha256_init(&context);
|
|
810
|
+
unsigned char buffer[1024 * 1024];
|
|
811
|
+
off_t offset = 0;
|
|
812
|
+
while (offset < before.st_size) {
|
|
813
|
+
size_t requested = (size_t)(before.st_size - offset);
|
|
814
|
+
if (requested > sizeof(buffer)) requested = sizeof(buffer);
|
|
815
|
+
ssize_t count = pread(fd, buffer, requested, offset);
|
|
816
|
+
if (count <= 0) {
|
|
817
|
+
int saved_errno=errno==0?EIO:errno; close(fd); free(leaf); errno=saved_errno; throw_errno(env,"hash regular file"); return NULL;
|
|
818
|
+
}
|
|
819
|
+
sha256_update(&context, buffer, (size_t)count);
|
|
820
|
+
offset += count;
|
|
821
|
+
}
|
|
822
|
+
if (!regular_still_bound(parent->fd, leaf, fd, &before)) {
|
|
823
|
+
close(fd); free(leaf); throw_message(env,"BINDING_CHANGED","hash input changed while being read"); return NULL;
|
|
824
|
+
}
|
|
825
|
+
close(fd); free(leaf);
|
|
826
|
+
unsigned char digest[32];
|
|
827
|
+
char hexadecimal[65];
|
|
828
|
+
static const char alphabet[] = "0123456789abcdef";
|
|
829
|
+
sha256_final(&context, digest);
|
|
830
|
+
for (size_t index = 0; index < 32; index++) {
|
|
831
|
+
hexadecimal[index*2] = alphabet[digest[index] >> 4];
|
|
832
|
+
hexadecimal[index*2+1] = alphabet[digest[index] & 15];
|
|
833
|
+
}
|
|
834
|
+
hexadecimal[64] = '\0';
|
|
835
|
+
if (!check_napi(env, napi_create_string_utf8(env, hexadecimal, 64, &result), "create hash string")) return NULL;
|
|
836
|
+
return result;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
static napi_value sha256_handle(napi_env env, napi_callback_info info) {
|
|
840
|
+
napi_value argv[1], result;
|
|
841
|
+
size_t argc = 1;
|
|
842
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL),
|
|
843
|
+
"sha256Handle args") || argc != 1) return NULL;
|
|
844
|
+
recordings_handle *handle = get_handle(env, argv[0]);
|
|
845
|
+
if (handle == NULL) return NULL;
|
|
846
|
+
struct stat before, after;
|
|
847
|
+
if (fstat(handle->fd, &before) != 0 || !S_ISREG(before.st_mode)) {
|
|
848
|
+
throw_errno(env, "stat retained hash input"); return NULL;
|
|
849
|
+
}
|
|
850
|
+
sha256_context context;
|
|
851
|
+
sha256_init(&context);
|
|
852
|
+
unsigned char buffer[1024 * 1024];
|
|
853
|
+
off_t offset = 0;
|
|
854
|
+
while (offset < before.st_size) {
|
|
855
|
+
size_t requested = (size_t)(before.st_size - offset);
|
|
856
|
+
if (requested > sizeof(buffer)) requested = sizeof(buffer);
|
|
857
|
+
ssize_t count = pread(handle->fd, buffer, requested, offset);
|
|
858
|
+
if (count <= 0) {
|
|
859
|
+
if (errno == 0) errno = EIO;
|
|
860
|
+
throw_errno(env, "hash retained regular file"); return NULL;
|
|
861
|
+
}
|
|
862
|
+
sha256_update(&context, buffer, (size_t)count);
|
|
863
|
+
offset += count;
|
|
864
|
+
}
|
|
865
|
+
if (fstat(handle->fd, &after) != 0 || !same_regular_snapshot(&before, &after)) {
|
|
866
|
+
throw_message(env, "BINDING_CHANGED", "retained hash input changed while being read");
|
|
867
|
+
return NULL;
|
|
868
|
+
}
|
|
869
|
+
unsigned char digest[32];
|
|
870
|
+
char hexadecimal[65];
|
|
871
|
+
static const char alphabet[] = "0123456789abcdef";
|
|
872
|
+
sha256_final(&context, digest);
|
|
873
|
+
for (size_t index = 0; index < 32; index++) {
|
|
874
|
+
hexadecimal[index * 2] = alphabet[digest[index] >> 4];
|
|
875
|
+
hexadecimal[index * 2 + 1] = alphabet[digest[index] & 15];
|
|
876
|
+
}
|
|
877
|
+
hexadecimal[64] = '\0';
|
|
878
|
+
if (!check_napi(env, napi_create_string_utf8(env, hexadecimal, 64, &result),
|
|
879
|
+
"create retained SHA-256 result")) return NULL;
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
static napi_value copy_regular_no_replace_at(napi_env env, napi_callback_info info) {
|
|
884
|
+
napi_value argv[7];
|
|
885
|
+
size_t argc = 7;
|
|
886
|
+
bool crash_during = false, crash_after_publish = false;
|
|
887
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "copyRegularNoReplaceAt args") || argc != 7 ||
|
|
888
|
+
!get_boolean(env, argv[5], &crash_during, "read copy crash flag") ||
|
|
889
|
+
!get_boolean(env, argv[6], &crash_after_publish, "read publish crash flag")) return NULL;
|
|
890
|
+
recordings_handle *source_parent = get_handle(env, argv[0]);
|
|
891
|
+
char *source_leaf = get_leaf(env, argv[1]);
|
|
892
|
+
recordings_handle *destination_parent = get_handle(env, argv[2]);
|
|
893
|
+
char *destination_leaf = get_leaf(env, argv[3]);
|
|
894
|
+
char *temporary_leaf = get_leaf(env, argv[4]);
|
|
895
|
+
if (source_parent == NULL || destination_parent == NULL || source_leaf == NULL ||
|
|
896
|
+
destination_leaf == NULL || temporary_leaf == NULL) {
|
|
897
|
+
free(source_leaf); free(destination_leaf); free(temporary_leaf); return NULL;
|
|
898
|
+
}
|
|
899
|
+
struct stat destination_details;
|
|
900
|
+
if (fstatat(destination_parent->fd, destination_leaf, &destination_details, AT_SYMLINK_NOFOLLOW) == 0) {
|
|
901
|
+
free(source_leaf); free(destination_leaf); free(temporary_leaf); return make_boolean(env, 0);
|
|
902
|
+
}
|
|
903
|
+
if (errno != ENOENT) {
|
|
904
|
+
int saved_errno = errno;
|
|
905
|
+
free(source_leaf); free(destination_leaf); free(temporary_leaf);
|
|
906
|
+
errno = saved_errno; throw_errno(env, "check recovery destination"); return NULL;
|
|
907
|
+
}
|
|
908
|
+
int source_fd = openat(source_parent->fd, source_leaf, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
|
909
|
+
if (source_fd < 0) {
|
|
910
|
+
int saved_errno = errno; free(source_leaf); free(destination_leaf); free(temporary_leaf);
|
|
911
|
+
errno = saved_errno; throw_errno(env, "open recovery source"); return NULL;
|
|
912
|
+
}
|
|
913
|
+
struct stat source_before;
|
|
914
|
+
if (fstat(source_fd, &source_before) != 0 || !S_ISREG(source_before.st_mode)) {
|
|
915
|
+
int saved_errno = errno == 0 ? EINVAL : errno; close(source_fd);
|
|
916
|
+
free(source_leaf); free(destination_leaf); free(temporary_leaf);
|
|
917
|
+
errno = saved_errno; throw_errno(env, "validate recovery source"); return NULL;
|
|
918
|
+
}
|
|
919
|
+
int temporary_fd = openat(destination_parent->fd, temporary_leaf,
|
|
920
|
+
O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
|
|
921
|
+
if (temporary_fd < 0) {
|
|
922
|
+
int saved_errno = errno; close(source_fd);
|
|
923
|
+
free(source_leaf); free(destination_leaf); free(temporary_leaf);
|
|
924
|
+
errno = saved_errno; throw_errno(env, "create recovery temporary"); return NULL;
|
|
925
|
+
}
|
|
926
|
+
unsigned char buffer[65536];
|
|
927
|
+
off_t offset = 0;
|
|
928
|
+
int failure = 0;
|
|
929
|
+
while (offset < source_before.st_size) {
|
|
930
|
+
size_t requested = (size_t)(source_before.st_size - offset);
|
|
931
|
+
if (requested > sizeof(buffer)) requested = sizeof(buffer);
|
|
932
|
+
ssize_t count = pread(source_fd, buffer, requested, offset);
|
|
933
|
+
if (count <= 0) { errno = errno == 0 ? EIO : errno; failure = 1; break; }
|
|
934
|
+
size_t written = 0;
|
|
935
|
+
while (written < (size_t)count) {
|
|
936
|
+
ssize_t result = write(temporary_fd, buffer + written, (size_t)count - written);
|
|
937
|
+
if (result <= 0) { errno = errno == 0 ? EIO : errno; failure = 1; break; }
|
|
938
|
+
written += (size_t)result;
|
|
939
|
+
}
|
|
940
|
+
if (failure) break;
|
|
941
|
+
offset += count;
|
|
942
|
+
if (crash_during && offset < source_before.st_size) kill(getpid(), SIGKILL);
|
|
943
|
+
}
|
|
944
|
+
struct stat source_after, named_source;
|
|
945
|
+
if (!failure && (fstat(source_fd, &source_after) != 0 ||
|
|
946
|
+
fstatat(source_parent->fd, source_leaf, &named_source, AT_SYMLINK_NOFOLLOW) != 0 ||
|
|
947
|
+
!same_regular_snapshot(&source_before, &source_after) ||
|
|
948
|
+
source_after.st_dev != named_source.st_dev ||
|
|
949
|
+
source_after.st_ino != named_source.st_ino)) {
|
|
950
|
+
errno = ESTALE; failure = 1;
|
|
951
|
+
}
|
|
952
|
+
if (!failure && fchmod(temporary_fd, source_before.st_mode & 0777) != 0) failure = 1;
|
|
953
|
+
struct timespec times[2];
|
|
954
|
+
#ifdef __APPLE__
|
|
955
|
+
times[0] = source_before.st_atimespec;
|
|
956
|
+
times[1] = source_before.st_mtimespec;
|
|
957
|
+
#else
|
|
958
|
+
times[0] = source_before.st_atim;
|
|
959
|
+
times[1] = source_before.st_mtim;
|
|
960
|
+
#endif
|
|
961
|
+
if (!failure && futimens(temporary_fd, times) != 0) failure = 1;
|
|
962
|
+
if (!failure && fsync(temporary_fd) != 0) failure = 1;
|
|
963
|
+
int saved_errno = errno;
|
|
964
|
+
close(source_fd);
|
|
965
|
+
close(temporary_fd);
|
|
966
|
+
if (failure) {
|
|
967
|
+
unlinkat(destination_parent->fd, temporary_leaf, 0);
|
|
968
|
+
free(source_leaf); free(destination_leaf); free(temporary_leaf);
|
|
969
|
+
errno = saved_errno == 0 ? EIO : saved_errno; throw_errno(env, "copy recovery file"); return NULL;
|
|
970
|
+
}
|
|
971
|
+
int published = linkat(destination_parent->fd, temporary_leaf,
|
|
972
|
+
destination_parent->fd, destination_leaf, 0) == 0;
|
|
973
|
+
if (!published && errno != EEXIST) {
|
|
974
|
+
saved_errno = errno; unlinkat(destination_parent->fd, temporary_leaf, 0);
|
|
975
|
+
free(source_leaf); free(destination_leaf); free(temporary_leaf);
|
|
976
|
+
errno = saved_errno; throw_errno(env, "publish recovery file"); return NULL;
|
|
977
|
+
}
|
|
978
|
+
if (fsync(destination_parent->fd) != 0) {
|
|
979
|
+
saved_errno = errno; free(source_leaf); free(destination_leaf); free(temporary_leaf);
|
|
980
|
+
errno = saved_errno; throw_errno(env, "fsync recovery parent"); return NULL;
|
|
981
|
+
}
|
|
982
|
+
if (published && crash_after_publish) kill(getpid(), SIGKILL);
|
|
983
|
+
if (unlinkat(destination_parent->fd, temporary_leaf, 0) != 0 || fsync(destination_parent->fd) != 0) {
|
|
984
|
+
saved_errno = errno; free(source_leaf); free(destination_leaf); free(temporary_leaf);
|
|
985
|
+
errno = saved_errno; throw_errno(env, "remove recovery temporary"); return NULL;
|
|
986
|
+
}
|
|
987
|
+
free(source_leaf); free(destination_leaf); free(temporary_leaf);
|
|
988
|
+
return make_boolean(env, published);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
static napi_value close_handle(napi_env env, napi_callback_info info) {
|
|
992
|
+
napi_value argv[1];
|
|
993
|
+
size_t argc = 1;
|
|
994
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "close args") || argc != 1) return NULL;
|
|
995
|
+
recordings_handle *handle = NULL;
|
|
996
|
+
if (!check_napi(env, napi_unwrap(env, argv[0], (void **)&handle), "unwrap handle")) return NULL;
|
|
997
|
+
if (handle != NULL && !handle->closed) {
|
|
998
|
+
if (close(handle->fd) != 0) { throw_errno(env, "close capability"); return NULL; }
|
|
999
|
+
handle->fd = -1;
|
|
1000
|
+
handle->closed = 1;
|
|
1001
|
+
}
|
|
1002
|
+
napi_value undefined;
|
|
1003
|
+
napi_get_undefined(env, &undefined);
|
|
1004
|
+
return undefined;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
static int remove_tree_contents(int directory_fd);
|
|
1008
|
+
|
|
1009
|
+
static int same_directory_binding(int parent_fd, const char *leaf, int directory_fd) {
|
|
1010
|
+
struct stat opened, named;
|
|
1011
|
+
if (fstat(directory_fd, &opened) != 0 ||
|
|
1012
|
+
fstatat(parent_fd, leaf, &named, AT_SYMLINK_NOFOLLOW) != 0) return 0;
|
|
1013
|
+
if (!S_ISDIR(opened.st_mode) || !S_ISDIR(named.st_mode) ||
|
|
1014
|
+
opened.st_dev != named.st_dev || opened.st_ino != named.st_ino) {
|
|
1015
|
+
errno = ESTALE;
|
|
1016
|
+
return 0;
|
|
1017
|
+
}
|
|
1018
|
+
return 1;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
static int remove_entry(int parent_fd, const char *leaf) {
|
|
1022
|
+
int child_fd = openat(parent_fd, leaf,
|
|
1023
|
+
O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
1024
|
+
if (child_fd >= 0) {
|
|
1025
|
+
if (!same_directory_binding(parent_fd, leaf, child_fd) ||
|
|
1026
|
+
remove_tree_contents(child_fd) != 0 ||
|
|
1027
|
+
!same_directory_binding(parent_fd, leaf, child_fd)) {
|
|
1028
|
+
int saved_errno = errno == 0 ? ESTALE : errno;
|
|
1029
|
+
close(child_fd); errno = saved_errno; return -1;
|
|
1030
|
+
}
|
|
1031
|
+
close(child_fd);
|
|
1032
|
+
return unlinkat(parent_fd, leaf, AT_REMOVEDIR);
|
|
1033
|
+
}
|
|
1034
|
+
if (errno != ENOTDIR) return -1;
|
|
1035
|
+
int file_fd = openat(parent_fd, leaf, O_RDONLY | O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC);
|
|
1036
|
+
if (file_fd < 0) return -1;
|
|
1037
|
+
struct stat opened, named;
|
|
1038
|
+
int valid = fstat(file_fd, &opened) == 0 && S_ISREG(opened.st_mode) &&
|
|
1039
|
+
fstatat(parent_fd, leaf, &named, AT_SYMLINK_NOFOLLOW) == 0 &&
|
|
1040
|
+
opened.st_dev == named.st_dev && opened.st_ino == named.st_ino;
|
|
1041
|
+
close(file_fd);
|
|
1042
|
+
if (!valid) { errno = ESTALE; return -1; }
|
|
1043
|
+
return unlinkat(parent_fd, leaf, 0);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
static int remove_tree_contents(int directory_fd) {
|
|
1047
|
+
int iterator_fd = openat(directory_fd, ".", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
1048
|
+
if (iterator_fd < 0) return -1;
|
|
1049
|
+
DIR *stream = fdopendir(iterator_fd);
|
|
1050
|
+
if (stream == NULL) { close(iterator_fd); return -1; }
|
|
1051
|
+
errno = 0;
|
|
1052
|
+
for (struct dirent *entry = readdir(stream); entry != NULL; entry = readdir(stream)) {
|
|
1053
|
+
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
|
|
1054
|
+
if (!valid_leaf(entry->d_name) || remove_entry(directory_fd, entry->d_name) != 0) {
|
|
1055
|
+
int saved_errno = errno == 0 ? EINVAL : errno;
|
|
1056
|
+
closedir(stream); errno = saved_errno; return -1;
|
|
1057
|
+
}
|
|
1058
|
+
errno = 0;
|
|
1059
|
+
}
|
|
1060
|
+
int saved_errno = errno;
|
|
1061
|
+
closedir(stream);
|
|
1062
|
+
if (saved_errno != 0) { errno = saved_errno; return -1; }
|
|
1063
|
+
return 0;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
static napi_value remove_tree_at(napi_env env, napi_callback_info info) {
|
|
1067
|
+
napi_value argv[2];
|
|
1068
|
+
size_t argc = 2;
|
|
1069
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "removeTreeAt args") || argc != 2) return NULL;
|
|
1070
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
1071
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
1072
|
+
if (parent == NULL || leaf == NULL) { free(leaf); return NULL; }
|
|
1073
|
+
int result = remove_entry(parent->fd, leaf);
|
|
1074
|
+
int saved_errno = errno;
|
|
1075
|
+
free(leaf);
|
|
1076
|
+
if (result != 0) { errno = saved_errno; throw_errno(env, "remove tree at capability"); return NULL; }
|
|
1077
|
+
napi_value undefined;
|
|
1078
|
+
napi_get_undefined(env, &undefined);
|
|
1079
|
+
return undefined;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
static napi_value remove_tree_handle_at(napi_env env, napi_callback_info info) {
|
|
1083
|
+
napi_value argv[3];
|
|
1084
|
+
size_t argc = 3;
|
|
1085
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL),
|
|
1086
|
+
"removeTreeHandleAt args") || argc != 3) return NULL;
|
|
1087
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
1088
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
1089
|
+
recordings_handle *directory = get_handle(env, argv[2]);
|
|
1090
|
+
if (parent == NULL || leaf == NULL || directory == NULL) { free(leaf); return NULL; }
|
|
1091
|
+
int result = 0;
|
|
1092
|
+
if (!same_directory_binding(parent->fd, leaf, directory->fd) ||
|
|
1093
|
+
remove_tree_contents(directory->fd) != 0 ||
|
|
1094
|
+
!same_directory_binding(parent->fd, leaf, directory->fd) ||
|
|
1095
|
+
unlinkat(parent->fd, leaf, AT_REMOVEDIR) != 0) result = -1;
|
|
1096
|
+
int saved_errno = errno;
|
|
1097
|
+
free(leaf);
|
|
1098
|
+
if (result != 0) {
|
|
1099
|
+
errno = saved_errno == 0 ? ESTALE : saved_errno;
|
|
1100
|
+
throw_errno(env, "remove retained tree at capability");
|
|
1101
|
+
return NULL;
|
|
1102
|
+
}
|
|
1103
|
+
napi_value undefined;
|
|
1104
|
+
napi_get_undefined(env, &undefined);
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
static napi_value unlink_file_handle_at(napi_env env, napi_callback_info info) {
|
|
1109
|
+
napi_value argv[3];
|
|
1110
|
+
size_t argc = 3;
|
|
1111
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL),
|
|
1112
|
+
"unlinkFileHandleAt args") || argc != 3) return NULL;
|
|
1113
|
+
recordings_handle *parent = get_handle(env, argv[0]);
|
|
1114
|
+
char *leaf = get_leaf(env, argv[1]);
|
|
1115
|
+
recordings_handle *file = get_handle(env, argv[2]);
|
|
1116
|
+
if (parent == NULL || leaf == NULL || file == NULL) { free(leaf); return NULL; }
|
|
1117
|
+
struct stat opened, named;
|
|
1118
|
+
int valid = fstat(file->fd, &opened) == 0 && S_ISREG(opened.st_mode) &&
|
|
1119
|
+
fstatat(parent->fd, leaf, &named, AT_SYMLINK_NOFOLLOW) == 0 &&
|
|
1120
|
+
S_ISREG(named.st_mode) && opened.st_dev == named.st_dev && opened.st_ino == named.st_ino;
|
|
1121
|
+
int result = valid ? unlinkat(parent->fd, leaf, 0) : -1;
|
|
1122
|
+
int saved_errno = valid ? errno : ESTALE;
|
|
1123
|
+
free(leaf);
|
|
1124
|
+
if (result != 0) {
|
|
1125
|
+
errno = saved_errno == 0 ? ESTALE : saved_errno;
|
|
1126
|
+
throw_errno(env, "unlink retained file at capability"); return NULL;
|
|
1127
|
+
}
|
|
1128
|
+
napi_value undefined;
|
|
1129
|
+
napi_get_undefined(env, &undefined);
|
|
1130
|
+
return undefined;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
static napi_value init(napi_env env, napi_value exports) {
|
|
1134
|
+
const napi_property_descriptor properties[] = {
|
|
1135
|
+
{ "openTrustedHome", NULL, open_trusted_home, NULL, NULL, NULL, napi_default, NULL },
|
|
1136
|
+
{ "openDirAt", NULL, open_dir_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1137
|
+
{ "openRegularAt", NULL, open_regular_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1138
|
+
{ "readDir", NULL, read_dir, NULL, NULL, NULL, napi_default, NULL },
|
|
1139
|
+
{ "statHandle", NULL, stat_handle, NULL, NULL, NULL, napi_default, NULL },
|
|
1140
|
+
{ "statAt", NULL, stat_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1141
|
+
{ "mkdirAt", NULL, mkdir_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1142
|
+
{ "linkNoReplaceAt", NULL, link_no_replace_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1143
|
+
{ "renameNoReplaceAt", NULL, rename_no_replace_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1144
|
+
{ "renameHandleNoReplaceAt", NULL, rename_handle_no_replace_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1145
|
+
{ "renameReplaceAt", NULL, rename_replace_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1146
|
+
{ "unlinkFileAt", NULL, unlink_file_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1147
|
+
{ "unlinkDirAt", NULL, unlink_dir_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1148
|
+
{ "sameBinding", NULL, same_binding, NULL, NULL, NULL, napi_default, NULL },
|
|
1149
|
+
{ "fsyncHandle", NULL, fsync_handle, NULL, NULL, NULL, napi_default, NULL },
|
|
1150
|
+
{ "handleHasNoExtendedAcl", NULL, handle_has_no_extended_acl, NULL, NULL, NULL, napi_default, NULL },
|
|
1151
|
+
{ "chmodHandle", NULL, chmod_handle, NULL, NULL, NULL, napi_default, NULL },
|
|
1152
|
+
{ "writeFileAt", NULL, write_file_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1153
|
+
{ "readRegularAt", NULL, read_regular_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1154
|
+
{ "sha256RegularAt", NULL, sha256_regular_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1155
|
+
{ "sha256Handle", NULL, sha256_handle, NULL, NULL, NULL, napi_default, NULL },
|
|
1156
|
+
{ "copyRegularNoReplaceAt", NULL, copy_regular_no_replace_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1157
|
+
{ "removeTreeAt", NULL, remove_tree_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1158
|
+
{ "removeTreeHandleAt", NULL, remove_tree_handle_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1159
|
+
{ "unlinkFileHandleAt", NULL, unlink_file_handle_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1160
|
+
{ "close", NULL, close_handle, NULL, NULL, NULL, napi_default, NULL },
|
|
1161
|
+
};
|
|
1162
|
+
if (!check_napi(env, napi_define_properties(env, exports,
|
|
1163
|
+
sizeof(properties) / sizeof(properties[0]), properties), "define filesystem guard exports")) return NULL;
|
|
1164
|
+
return exports;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
|