windows-development-sandbox 1.13.2 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/app/features/storage/storage.run.js +1 -1
  2. package/dist/bin/agent-sandbox.js +2 -2
  3. package/dist/bin/{chunk-VENWSGX2.js → chunk-BQ23GBLC.js} +411 -58
  4. package/dist/bin/{chunk-K4SU3MHJ.js → chunk-F7IMME5G.js} +1 -1
  5. package/dist/bin/{chunk-IL7BXJF2.js → chunk-LQHEUN2A.js} +1 -1
  6. package/dist/bin/claude-host.js +2 -2
  7. package/dist/bin/claude-sbx.js +2 -2
  8. package/dist/bin/codex-sbx.js +2 -2
  9. package/dist/bin/gemini-sbx.js +2 -2
  10. package/dist/bin/sandbox.js +2 -2
  11. package/dist/bin/sbx-hide.js +2 -2
  12. package/dist/bin/sbx-show.js +2 -2
  13. package/dist/cli/commands/launch/launch.js +1 -1
  14. package/dist/cli/commands/repair/checks/contained.js +1 -1
  15. package/dist/guest/daemon/sessions.d.ts +7 -0
  16. package/dist/guest/daemon/sessions.js +23 -2
  17. package/dist/guest/entries/daemon.js +1 -0
  18. package/dist/guest/sessions/pty-host.d.ts +10 -0
  19. package/dist/guest/sessions/pty-host.js +48 -3
  20. package/dist/host/channel/channel.js +1 -1
  21. package/dist/host/install/install-record.d.ts +1 -0
  22. package/dist/host/install/install-record.js +2 -0
  23. package/dist/host/sandbox/contained-deps.d.ts +2 -1
  24. package/dist/host/sandbox/contained-deps.js +6 -4
  25. package/dist/host/sharing/lifecycle.js +9 -1
  26. package/dist/host/storage/contained/contained-storage.js +13 -0
  27. package/dist/host/storage/contained/disk-port.d.ts +5 -0
  28. package/dist/host/storage/contained/helper-port.d.ts +72 -0
  29. package/dist/host/storage/contained/helper-port.js +162 -0
  30. package/dist/host/storage/contained/index.d.ts +1 -0
  31. package/dist/host/storage/contained/index.js +1 -0
  32. package/dist/host/web/bridge.d.ts +5 -0
  33. package/dist/host/web/bridge.js +11 -1
  34. package/dist/host/web/server.d.ts +2 -0
  35. package/dist/host/web/server.js +11 -1
  36. package/dist/runtime/agent.js +9 -0
  37. package/dist/runtime/daemon.js +57 -3
  38. package/dist/runtime/db.js +9 -0
  39. package/dist/runtime/main.js +195 -12
  40. package/dist/runtime/netproxy.js +9 -0
  41. package/dist/runtime/view.js +195 -12
  42. package/dist/selftest/channel.test.js +72 -1
  43. package/dist/selftest/contained.test.js +1 -37
  44. package/dist/selftest/hcs.test.js +5 -2
  45. package/dist/selftest/index.js +2 -0
  46. package/dist/selftest/install.test.js +11 -0
  47. package/dist/selftest/live-daemon.test.js +51 -1
  48. package/dist/selftest/live-terminal.test.js +112 -4
  49. package/dist/selftest/storage-helper.test.d.ts +2 -0
  50. package/dist/selftest/storage-helper.test.js +493 -0
  51. package/dist/selftest/support/vhdx.d.ts +14 -0
  52. package/dist/selftest/support/vhdx.js +72 -0
  53. package/dist/selftest/web.test.js +14 -2
  54. package/dist/shared/channel/run-dir.d.ts +6 -0
  55. package/dist/shared/channel/run-dir.js +9 -0
  56. package/dist/shared/pty/index.d.ts +3 -1
  57. package/dist/shared/pty/index.js +3 -1
  58. package/dist/shared/pty/key-gate.d.ts +24 -0
  59. package/dist/shared/pty/key-gate.js +45 -0
  60. package/dist/shared/pty/mouse-modes.d.ts +27 -0
  61. package/dist/shared/pty/mouse-modes.js +155 -0
  62. package/dist/shared/pty/terminal.d.ts +0 -5
  63. package/dist/shared/pty/terminal.js +51 -20
  64. package/dist/sync/watcher.js +1 -1
  65. package/dist/web/index.html +1 -1
  66. package/dist/web/{main-NBQ3LF4E.js → main-6XNBTJLT.js} +33 -33
  67. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  68. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  69. package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
  70. package/native/csharp/StorageHelper/Client.cs +297 -0
  71. package/native/csharp/StorageHelper/Log.cs +177 -0
  72. package/native/csharp/StorageHelper/MountRecords.cs +160 -0
  73. package/native/csharp/StorageHelper/Operations.cs +596 -0
  74. package/native/csharp/StorageHelper/Pins.cs +270 -0
  75. package/native/csharp/StorageHelper/Privileges.cs +78 -0
  76. package/native/csharp/StorageHelper/Program.cs +349 -0
  77. package/native/csharp/StorageHelper/Protocol.cs +107 -0
  78. package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
  79. package/native/csharp/StorageHelper/Rules.cs +156 -0
  80. package/native/csharp/StorageHelper/Server.cs +656 -0
  81. package/native/csharp/StorageHelper/Settings.cs +297 -0
  82. package/native/csharp/StorageHelper/StorageService.cs +53 -0
  83. package/native/csharp/StorageHelper/UserSettings.cs +206 -0
  84. package/native/csharp/StorageHelper/Vhdx.cs +230 -0
  85. package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
  86. package/native/csharp/StorageHelper/Volumes.cs +379 -0
  87. package/native/csharp/StorageHelper/Watchdog.cs +128 -0
  88. package/native/csharp/StorageHelper/Win32.cs +250 -0
  89. package/native/scripts/set-storage-helper.ps1 +203 -0
  90. package/package.json +1 -1
  91. package/sandbox/csharp/PtyHost.cs +236 -1
@@ -0,0 +1,297 @@
1
+ // Where a user's contained storage is, worked out the way the CLI does it (src/core/config/loader.ts and
2
+ // src/cli/commands/context.ts): the built-in defaults, then the machine file (%ProgramData%\AgentSandbox\config.json),
3
+ // then the user's own file (<profile>\.agent-sandbox\config.json, JSON with comments) win (except that a
4
+ // size limit set in the machine file is a ceiling); %VARIABLES% expand from the
5
+ // user's environment, a leading ~ is the profile, and a relative folder is relative to the folder of the file. Only
6
+ // storage.root and storage.containedMaxGb are read. The texts are handed in, so this file reads nothing itself.
7
+ using System;
8
+ using System.Collections.Generic;
9
+ using System.Text;
10
+ using System.Text.RegularExpressions;
11
+ using System.Web.Script.Serialization;
12
+
13
+ namespace AgentSandbox.StorageHelper
14
+ {
15
+ public sealed class StorageSettings
16
+ {
17
+ public StorageSettings(string storageRoot, long maxGb)
18
+ {
19
+ StorageRoot = storageRoot;
20
+ MaxGb = maxGb;
21
+ }
22
+
23
+ public string StorageRoot { get; private set; }
24
+ public long MaxGb { get; private set; }
25
+ }
26
+
27
+ public static class Settings
28
+ {
29
+ public const string DefaultRoot = "%ProgramData%\\AgentSandbox\\users\\%USERNAME%";
30
+ public const long DefaultMaxGb = 64;
31
+ public const int MaxConfigBytes = 1024 * 1024;
32
+ private static readonly Regex Variable = new Regex("%([^%]+)%", RegexOptions.CultureInvariant);
33
+
34
+ /// <summary>
35
+ /// The storage folder and size limit. `machineText` / `userText` may be null (no file). `userBase` is the folder
36
+ /// of the user's file (a relative root is relative to it), `profile` the user's profile folder.
37
+ /// </summary>
38
+ public static StorageSettings Derive(string machineText, string userText, string userBase, string profile, IDictionary<string, string> environment)
39
+ {
40
+ string root = DefaultRoot;
41
+ string rootBase = userBase;
42
+ long maxGb = DefaultMaxGb;
43
+ long ceiling = long.MaxValue;
44
+ if (machineText != null)
45
+ {
46
+ Dictionary<string, object> machine = Parse(machineText, "the machine settings");
47
+ string value;
48
+ if (TryString(machine, "root", out value))
49
+ {
50
+ // The machine file must give a full path (the CLI refuses it otherwise).
51
+ if (!IsRooted(Expand(value, profile, environment)))
52
+ {
53
+ throw new RefusedException("storage.root in the machine settings must be a full path.");
54
+ }
55
+ root = value;
56
+ }
57
+ long gb;
58
+ if (TryNumber(machine, out gb))
59
+ {
60
+ // A limit an administrator set for the PC is a ceiling: a user's own file may only go lower.
61
+ maxGb = gb;
62
+ ceiling = gb;
63
+ }
64
+ }
65
+ if (userText != null)
66
+ {
67
+ Dictionary<string, object> user = Parse(userText, "your settings");
68
+ string value;
69
+ if (TryString(user, "root", out value))
70
+ {
71
+ root = value;
72
+ }
73
+ long gb;
74
+ if (TryNumber(user, out gb))
75
+ {
76
+ maxGb = Math.Min(gb, ceiling);
77
+ }
78
+ }
79
+ string expanded = Expand(root, profile, environment);
80
+ string full = IsRooted(expanded) ? expanded : Combine(rootBase, expanded);
81
+ return new StorageSettings(Normalize(full), maxGb);
82
+ }
83
+
84
+ /// <summary>%VAR% from the user's environment (names ignore case, unknown ones stay), then a leading ~.</summary>
85
+ public static string Expand(string input, string profile, IDictionary<string, string> environment)
86
+ {
87
+ string text = Variable.Replace(input, delegate(Match m)
88
+ {
89
+ string name = m.Groups[1].Value;
90
+ foreach (KeyValuePair<string, string> pair in environment)
91
+ {
92
+ if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
93
+ {
94
+ return pair.Value;
95
+ }
96
+ }
97
+ return m.Value;
98
+ });
99
+ if (text == "~" || text.StartsWith("~\\") || text.StartsWith("~/"))
100
+ {
101
+ text = profile + text.Substring(1);
102
+ }
103
+ return text;
104
+ }
105
+
106
+ private static bool IsRooted(string path)
107
+ {
108
+ return path.Length >= 3 && char.IsLetter(path[0]) && path[1] == ':' && (path[2] == '\\' || path[2] == '/');
109
+ }
110
+
111
+ private static string Combine(string folder, string relative)
112
+ {
113
+ return folder.TrimEnd('\\', '/') + "\\" + relative;
114
+ }
115
+
116
+ /// <summary>Like path.resolve for a drive path: slashes become backslashes, . and .. parts are worked out.</summary>
117
+ public static string Normalize(string path)
118
+ {
119
+ string text = path.Replace('/', '\\');
120
+ if (!IsRooted(text))
121
+ {
122
+ return text;
123
+ }
124
+ List<string> parts = new List<string>();
125
+ foreach (string part in text.Substring(3).Split('\\'))
126
+ {
127
+ if (part.Length == 0 || part == ".")
128
+ {
129
+ continue;
130
+ }
131
+ if (part == "..")
132
+ {
133
+ if (parts.Count > 0)
134
+ {
135
+ parts.RemoveAt(parts.Count - 1);
136
+ }
137
+ continue;
138
+ }
139
+ parts.Add(part);
140
+ }
141
+ return char.ToUpperInvariant(text[0]) + ":\\" + string.Join("\\", parts.ToArray());
142
+ }
143
+
144
+ private static Dictionary<string, object> Parse(string text, string what)
145
+ {
146
+ object value;
147
+ try
148
+ {
149
+ JavaScriptSerializer serializer = new JavaScriptSerializer();
150
+ serializer.MaxJsonLength = MaxConfigBytes;
151
+ serializer.RecursionLimit = 32;
152
+ value = serializer.DeserializeObject(StripJsonc(text));
153
+ }
154
+ catch (Exception)
155
+ {
156
+ // The file's text is never repeated: it may be something the service was not meant to show.
157
+ throw new RefusedException("Could not read " + what + " (not valid JSON). Run sandbox doctor.");
158
+ }
159
+ Dictionary<string, object> top = value as Dictionary<string, object>;
160
+ if (top == null)
161
+ {
162
+ throw new RefusedException("Could not read " + what + " (not a JSON object). Run sandbox doctor.");
163
+ }
164
+ object storage;
165
+ if (!top.TryGetValue("storage", out storage))
166
+ {
167
+ return new Dictionary<string, object>();
168
+ }
169
+ Dictionary<string, object> section = storage as Dictionary<string, object>;
170
+ if (section == null)
171
+ {
172
+ throw new RefusedException("Could not read " + what + " (storage is not an object).");
173
+ }
174
+ return section;
175
+ }
176
+
177
+ private static bool TryString(Dictionary<string, object> storage, string key, out string value)
178
+ {
179
+ value = null;
180
+ object raw;
181
+ if (!storage.TryGetValue(key, out raw))
182
+ {
183
+ return false;
184
+ }
185
+ value = raw as string;
186
+ if (value == null || value.Length == 0 || value.Length > 1000)
187
+ {
188
+ throw new RefusedException("storage." + key + " in the settings is not a folder.");
189
+ }
190
+ return true;
191
+ }
192
+
193
+ private static bool TryNumber(Dictionary<string, object> storage, out long value)
194
+ {
195
+ value = 0;
196
+ object raw;
197
+ if (!storage.TryGetValue("containedMaxGb", out raw))
198
+ {
199
+ return false;
200
+ }
201
+ if (raw is int)
202
+ {
203
+ value = (int)raw;
204
+ return true;
205
+ }
206
+ if (raw is long)
207
+ {
208
+ value = (long)raw;
209
+ return true;
210
+ }
211
+ throw new RefusedException("storage.containedMaxGb in the settings is not a whole number.");
212
+ }
213
+
214
+ /// <summary>JSON with comments and trailing commas (like the CLI's jsonc-parser) made plain JSON. Strings are kept.</summary>
215
+ public static string StripJsonc(string text)
216
+ {
217
+ if (text.Length > 0 && text[0] == '')
218
+ {
219
+ text = text.Substring(1);
220
+ }
221
+ StringBuilder sb = new StringBuilder(text.Length);
222
+ int i = 0;
223
+ while (i < text.Length)
224
+ {
225
+ char ch = text[i];
226
+ if (ch == '"')
227
+ {
228
+ int start = i;
229
+ i++;
230
+ while (i < text.Length && text[i] != '"')
231
+ {
232
+ i += text[i] == '\\' ? 2 : 1;
233
+ }
234
+ i++;
235
+ sb.Append(text, start, Math.Min(i, text.Length) - start);
236
+ continue;
237
+ }
238
+ if (ch == '/' && i + 1 < text.Length && text[i + 1] == '/')
239
+ {
240
+ while (i < text.Length && text[i] != '\n')
241
+ {
242
+ i++;
243
+ }
244
+ continue;
245
+ }
246
+ if (ch == '/' && i + 1 < text.Length && text[i + 1] == '*')
247
+ {
248
+ int end = text.IndexOf("*/", i + 2, StringComparison.Ordinal);
249
+ i = end < 0 ? text.Length : end + 2;
250
+ sb.Append(' ');
251
+ continue;
252
+ }
253
+ if (ch == ',')
254
+ {
255
+ // A comma followed only by blanks and comments before } or ] is dropped.
256
+ int j = SkipBlank(text, i + 1);
257
+ if (j < text.Length && (text[j] == '}' || text[j] == ']'))
258
+ {
259
+ i++;
260
+ continue;
261
+ }
262
+ }
263
+ sb.Append(ch);
264
+ i++;
265
+ }
266
+ return sb.ToString();
267
+ }
268
+
269
+ private static int SkipBlank(string text, int i)
270
+ {
271
+ while (i < text.Length)
272
+ {
273
+ if (char.IsWhiteSpace(text[i]))
274
+ {
275
+ i++;
276
+ }
277
+ else if (text[i] == '/' && i + 1 < text.Length && text[i + 1] == '/')
278
+ {
279
+ while (i < text.Length && text[i] != '\n')
280
+ {
281
+ i++;
282
+ }
283
+ }
284
+ else if (text[i] == '/' && i + 1 < text.Length && text[i + 1] == '*')
285
+ {
286
+ int end = text.IndexOf("*/", i + 2, StringComparison.Ordinal);
287
+ i = end < 0 ? text.Length : end + 2;
288
+ }
289
+ else
290
+ {
291
+ break;
292
+ }
293
+ }
294
+ return i;
295
+ }
296
+ }
297
+ }
@@ -0,0 +1,53 @@
1
+ // The Windows service AgentSandboxStorage (LocalSystem, automatic start, installed by scripts\install.ps1 through
2
+ // native\scripts\set-storage-helper.ps1): starts the pipe server when Windows starts the service, stops it on stop.
3
+ using System;
4
+ using System.ServiceProcess;
5
+
6
+ namespace AgentSandbox.StorageHelper
7
+ {
8
+ public sealed class StorageService : ServiceBase
9
+ {
10
+ public const string Name = "AgentSandboxStorage";
11
+ private readonly Func<Server> create;
12
+ private Server server;
13
+
14
+ public StorageService(Func<Server> create)
15
+ {
16
+ this.create = create;
17
+ ServiceName = Name;
18
+ CanStop = true;
19
+ CanShutdown = true;
20
+ CanPauseAndContinue = false;
21
+ AutoLog = true;
22
+ }
23
+
24
+ protected override void OnStart(string[] args)
25
+ {
26
+ server = create();
27
+ try
28
+ {
29
+ server.Start();
30
+ }
31
+ catch
32
+ {
33
+ server.Dispose();
34
+ server = null;
35
+ throw;
36
+ }
37
+ }
38
+
39
+ protected override void OnStop()
40
+ {
41
+ if (server != null)
42
+ {
43
+ server.Dispose();
44
+ server = null;
45
+ }
46
+ }
47
+
48
+ protected override void OnShutdown()
49
+ {
50
+ OnStop();
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,206 @@
1
+ // Reads what Settings.Derive needs for one client: the machine file (read as SYSTEM, accepted only when administrators
2
+ // own it and nobody else may change it, never through a link), the user's own file (opened while acting as the user, so only a file the user
3
+ // may read is read, never through a link; read after acting as the user has ended), the profile folder (from the
4
+ // registry) and the user's environment (CreateEnvironmentBlock with the user's token, called as SYSTEM).
5
+ using System;
6
+ using System.Collections.Generic;
7
+ using System.IO;
8
+ using System.Runtime.InteropServices;
9
+ using System.Security.AccessControl;
10
+ using System.Security.Principal;
11
+ using System.Text;
12
+ using Microsoft.Win32;
13
+ using Microsoft.Win32.SafeHandles;
14
+
15
+ namespace AgentSandbox.StorageHelper
16
+ {
17
+ public sealed class SettingsSources
18
+ {
19
+ /// <summary>The machine file (%ProgramData%\AgentSandbox\config.json).</summary>
20
+ public string MachineFile { get; set; }
21
+
22
+ /// <summary>Test aid (console mode only): the profile folder to use instead of the registry's.</summary>
23
+ public string ProfileOverride { get; set; }
24
+ }
25
+
26
+ public static class UserSettings
27
+ {
28
+ private static readonly SecurityIdentifier SystemSid = new SecurityIdentifier("S-1-5-18");
29
+ private static readonly SecurityIdentifier AdministratorsSid = new SecurityIdentifier("S-1-5-32-544");
30
+
31
+ private static class NativeMethods
32
+ {
33
+ [DllImport("userenv.dll", SetLastError = true)]
34
+ [return: MarshalAs(UnmanagedType.Bool)]
35
+ public static extern bool CreateEnvironmentBlock(out IntPtr environment, SafeAccessTokenHandle token, [MarshalAs(UnmanagedType.Bool)] bool inherit);
36
+
37
+ [DllImport("userenv.dll", SetLastError = true)]
38
+ [return: MarshalAs(UnmanagedType.Bool)]
39
+ public static extern bool DestroyEnvironmentBlock(IntPtr environment);
40
+ }
41
+
42
+ public static StorageSettings For(Client client, SettingsSources sources)
43
+ {
44
+ string profile = sources.ProfileOverride ?? ProfileOf(client.Sid);
45
+ string machineText = ReadMachine(sources.MachineFile);
46
+ string userFile = profile + "\\.agent-sandbox\\config.json";
47
+ string userText = ReadAsUser(client, userFile);
48
+ IDictionary<string, string> environment = EnvironmentOf(client);
49
+ if (sources.ProfileOverride != null)
50
+ {
51
+ environment["USERPROFILE"] = profile;
52
+ }
53
+ return Settings.Derive(machineText, userText, profile + "\\.agent-sandbox", profile, environment);
54
+ }
55
+
56
+ public static string ProfileOf(SecurityIdentifier sid)
57
+ {
58
+ using (RegistryKey key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + sid.Value))
59
+ {
60
+ string value = key == null ? null : key.GetValue("ProfileImagePath") as string;
61
+ if (string.IsNullOrEmpty(value))
62
+ {
63
+ throw new RefusedException("Windows has no profile folder for this user.");
64
+ }
65
+ string profile = value.TrimEnd('\\');
66
+ if (!Rules.IsPlainAbsolute(profile))
67
+ {
68
+ throw new RefusedException("The profile folder is not a plain full path.");
69
+ }
70
+ return profile;
71
+ }
72
+ }
73
+
74
+ /// <summary>
75
+ /// The machine file's text, or null without one. Refused when someone other than administrators owns it or may
76
+ /// change it. It is read through the handle that was checked, which shares no write or delete, so it cannot be
77
+ /// swapped in between (a file someone else put there instead is theirs, and refused).
78
+ /// </summary>
79
+ public static string ReadMachine(string file)
80
+ {
81
+ if (file == null)
82
+ {
83
+ return null;
84
+ }
85
+ using (SafeFileHandle h = Win32.OpenNoFollow(file, Win32.GenericRead | Win32.ReadControl, Win32.FileShareRead))
86
+ {
87
+ if (h == null)
88
+ {
89
+ return null;
90
+ }
91
+ if (!Win32.IsAt(h, file))
92
+ {
93
+ throw new RefusedException("The machine settings are behind a link; ask an administrator to run the installer again.");
94
+ }
95
+ CheckAdminOnly(h, file);
96
+ return ReadText(h, file);
97
+ }
98
+ }
99
+
100
+ private static void CheckAdminOnly(SafeFileHandle h, string path)
101
+ {
102
+ Win32.ByHandleFileInformation info = Win32.InfoOf(h, path);
103
+ if ((info.FileAttributes & Win32.AttributeReparsePoint) != 0)
104
+ {
105
+ throw new RefusedException("The machine settings are behind a link; ask an administrator to run the installer again.");
106
+ }
107
+ RawSecurityDescriptor sd = Win32.SecurityOf(h);
108
+ bool adminOwned = sd.Owner != null && (sd.Owner.Equals(SystemSid) || sd.Owner.Equals(AdministratorsSid));
109
+ if (!adminOwned || Pins.OthersWhoMayChange(sd, SystemSid) != null)
110
+ {
111
+ throw new RefusedException("The machine settings (" + path + ") may be changed by someone who is not an administrator; ask an administrator to run the installer again.");
112
+ }
113
+ }
114
+
115
+ /// <summary>The user's file as the user may read it (null when there is none), never through a link.</summary>
116
+ private static string ReadAsUser(Client client, string file)
117
+ {
118
+ SafeFileHandle h;
119
+ using (WindowsImpersonationContext acting = WindowsIdentity.Impersonate(client.Token.DangerousGetHandle()))
120
+ {
121
+ try
122
+ {
123
+ h = Win32.OpenNoFollow(file, Win32.GenericRead, Win32.FileShareRead | Win32.FileShareWrite);
124
+ }
125
+ finally
126
+ {
127
+ acting.Undo();
128
+ }
129
+ }
130
+ if (h == null)
131
+ {
132
+ return null;
133
+ }
134
+ using (h)
135
+ {
136
+ Win32.ByHandleFileInformation info = Win32.InfoOf(h, file);
137
+ if ((info.FileAttributes & (Win32.AttributeReparsePoint | Win32.AttributeDirectory)) != 0)
138
+ {
139
+ throw new RefusedException("Your settings file is a link or a folder: " + file);
140
+ }
141
+ return ReadText(h, file);
142
+ }
143
+ }
144
+
145
+ private static string ReadText(SafeFileHandle h, string file)
146
+ {
147
+ using (FileStream stream = new FileStream(h, FileAccess.Read))
148
+ {
149
+ if (stream.Length > Settings.MaxConfigBytes)
150
+ {
151
+ throw new RefusedException("The settings file is too large: " + file);
152
+ }
153
+ byte[] bytes = new byte[stream.Length];
154
+ int done = 0;
155
+ while (done < bytes.Length)
156
+ {
157
+ int n = stream.Read(bytes, done, bytes.Length - done);
158
+ if (n <= 0)
159
+ {
160
+ break;
161
+ }
162
+ done += n;
163
+ }
164
+ return new UTF8Encoding(false, false).GetString(bytes, 0, done);
165
+ }
166
+ }
167
+
168
+ /// <summary>The user's own environment (what their programs see), by name.</summary>
169
+ public static IDictionary<string, string> EnvironmentOf(Client client)
170
+ {
171
+ Dictionary<string, string> result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
172
+ IntPtr block;
173
+ if (!NativeMethods.CreateEnvironmentBlock(out block, client.Token, false))
174
+ {
175
+ throw Win32.LastError("Could not read the user's environment");
176
+ }
177
+ try
178
+ {
179
+ int offset = 0;
180
+ while (true)
181
+ {
182
+ string entry = Marshal.PtrToStringUni(new IntPtr(block.ToInt64() + offset));
183
+ if (string.IsNullOrEmpty(entry))
184
+ {
185
+ break;
186
+ }
187
+ offset += (entry.Length + 1) * 2;
188
+ int eq = entry.IndexOf('=', 1);
189
+ if (eq > 0)
190
+ {
191
+ result[entry.Substring(0, eq)] = entry.Substring(eq + 1);
192
+ }
193
+ if (offset > 1024 * 1024)
194
+ {
195
+ break;
196
+ }
197
+ }
198
+ }
199
+ finally
200
+ {
201
+ NativeMethods.DestroyEnvironmentBlock(block);
202
+ }
203
+ return result;
204
+ }
205
+ }
206
+ }