cheatah
Source

stdlib/os/os.hpp

1// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).
2// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.
3#pragma once
5/**
6 * @file os.hpp
7 * @brief cheatah `os` — Python-like operating-system interface over
8 * `std::filesystem`, plus the `os.path` submodule. `import os` to use it.
9 *
10 * `import os` includes this header AND links `libcheatah_os`. Templated entry
11 * points (e.g. `os.path.join`) live here; the rest is compiled into the library.
12 * Unit tests: `stdlib/tests/os_test.cpp`; the suite runs under AddressSanitizer
13 * (the `asan` preset) and Valgrind (`security/run-valgrind.sh`) on every QA-gate
14 * run.
15 *
16 * @note Most calls touch the filesystem/environment, so they perform a syscall
17 * in addition to the cost noted per function; `n` is the path length.
18 */
19#include <cstdint>
20#include <filesystem>
21#include <string>
22#include <string_view>
23#include <utility>
24#include <vector>
26namespace cheatah::os {
28/// StringLike<T>: a std::string can be constructed from T — exactly what
29/// os.path.join() does (std::string(part)). Naming it yields a clear "constraint
30/// StringLike not satisfied" message while still accepting everything it does today
31/// (const char*, char arrays, std::string, std::string_view, …).
32template <typename T>
33concept StringLike = requires(const T& value) { std::string(value); };
35/**
36 * Current working directory.
37 *
38 * Queries the process's current directory via `std::filesystem::current_path`
39 * and returns it as an absolute path string.
40 * @return the absolute cwd.
41 * @complexity O(n) + a syscall.
42 * @alloc allocates the result string.
43 * @test CheatahOs.CwdAndCpuCount
44 * @crtest OsCompileRun.Getcwd
45 * @systest StdlibE2E.Os
46 */
47std::string getcwd();
48/**
49 * Change the working directory.
50 *
51 * Sets the process's current directory; subsequent relative paths resolve
52 * against it. Throws if @p path does not exist or is not a directory.
53 * @param path the target directory.
54 * @complexity O(1) + a syscall.
55 * @alloc none.
56 * @test CheatahOs.MakedirsAndChdir
57 * @crtest OsCompileRun.Chdir
58 * @systest StdlibE2E.Os
59 */
60void chdir(const std::string& path);
61/**
62 * List a directory's entries (basenames only).
63 *
64 * Iterates @p path and returns each entry's filename component (not a full
65 * path), in unspecified order; `.` and `..` are not included. Throws if @p path
66 * does not exist or is not a directory.
67 * @param path the directory (default `.`).
68 * @return the entry names.
69 * @complexity O(entries) + syscalls.
70 * @alloc allocates a vector of strings.
71 * @test CheatahOs.ListdirAndRename
72 * @crtest OsCompileRun.Listdir
73 * @systest StdlibE2E.Os
74 */
75std::vector<std::string> listdir(const std::string& path = ".");
76/**
77 * Create a single directory.
78 *
79 * Creates the leaf directory only; the parent must already exist (use
80 * makedirs to create missing parents). Does nothing if @p path already exists
81 * as a directory.
82 * @param path the directory to create.
83 * @complexity O(1) + a syscall.
84 * @alloc none.
85 * @test CheatahOs.MakeDirExistsThenRemove
86 * @crtest OsCompileRun.Mkdir
87 * @systest StdlibE2E.Os
88 */
89void mkdir(const std::string& path);
90/**
91 * Create a directory and any missing parents.
92 *
93 * Creates @p path along with every intermediate directory that does not yet
94 * exist. Succeeds without error if the full path already exists as a directory.
95 * @param path the nested directory to create.
96 * @complexity O(depth) + syscalls.
97 * @alloc none.
98 * @test CheatahOs.MakedirsAndChdir
99 * @crtest OsCompileRun.Makedirs
100 * @systest StdlibE2E.Os
101 */
102void makedirs(const std::string& path);
103/**
104 * Remove an (empty) directory.
105 *
106 * Deletes a single, empty directory; throws if @p path is non-empty. A missing
107 * @p path is a no-op (no error). Note this is the same `fs::remove` used by
108 * remove(), so it will also delete a regular file at @p path.
109 * @param path the directory to remove.
110 * @complexity O(1) + a syscall.
111 * @alloc none.
112 * @test CheatahOs.MakeDirExistsThenRemove
113 * @crtest OsCompileRun.Rmdir
114 * @systest StdlibE2E.Os
115 */
116void rmdir(const std::string& path);
117/**
118 * Remove a file or empty directory.
119 *
120 * Deletes a single file or empty directory and returns whether anything was
121 * removed; a missing @p path returns false rather than throwing. Throws if
122 * @p path is a non-empty directory.
123 * @param path the entry to remove.
124 * @return true iff something was removed.
125 * @complexity O(1) + a syscall.
126 * @alloc none.
127 * @test CheatahOs.FileQueriesIsfileAndGetsize
128 * @crtest OsCompileRun.Remove
129 * @systest StdlibE2E.Os
130 */
131bool remove(const std::string& path); // true if a file was removed
132/**
133 * Rename/move @p src to @p dst.
134 *
135 * Moves or renames an entry; an existing @p dst is overwritten when permitted
136 * by the underlying `fs::rename`. Crossing filesystems or other failures throw.
137 * @param src source path.
138 * @param dst destination path.
139 * @complexity O(1) + a syscall.
140 * @alloc none.
141 * @test CheatahOs.ListdirAndRename
142 * @crtest OsCompileRun.Rename
143 * @systest StdlibE2E.Os
144 */
145void rename(const std::string& src, const std::string& dst);
147/**
148 * Read an environment variable.
149 *
150 * Returns @p fallback (default `""`) when the variable is unset; an empty
151 * string result therefore does not distinguish "unset" from "set to empty".
152 * @param name the variable name.
153 * @param fallback returned when unset.
154 * @return the value, or @p fallback.
155 * @complexity O(environment size) — `std::getenv` is a linear scan of the C library's
156 * environment table (no syscall).
157 * @alloc allocates the returned string.
158 * @test CheatahOs.GetenvFallback, CheatahOs.SetenvThenGetenv
159 * @crtest OsCompileRun.Getenv
160 * @systest StdlibE2E.Os
161 */
162std::string getenv(const std::string& name, const std::string& fallback = "");
163/**
164 * Set an environment variable.
165 *
166 * When @p overwrite is false and the variable already exists, the existing
167 * value is kept; otherwise it is created or replaced. The change affects only
168 * this process and its future children.
169 * @param name the variable name.
170 * @param value the value to set.
171 * @param overwrite replace an existing value when true.
172 * @complexity O(environment size) — the C library scans and updates its environment
173 * table (no syscall).
174 * @alloc may allocate inside the C library's environment table.
175 * @test CheatahOs.SetenvThenGetenv
176 * @crtest OsCompileRun.Setenv
177 * @systest StdlibE2E.Os
178 */
179void setenv(const std::string& name, const std::string& value, bool overwrite = true);
181/**
182 * Process id.
183 * @return the current process's pid.
184 * @complexity O(1) + a syscall.
185 * @alloc none.
186 * @test CheatahOs.PidAndSystem
187 * @crtest OsCompileRun.Getpid
188 * @systest StdlibE2E.Os
189 */
190int getpid();
191/**
192 * Logical CPU count.
193 *
194 * Reports `std::thread::hardware_concurrency()`, the number of concurrent
195 * threads supported; the standard allows it to return 0 when the value cannot
196 * be determined, so callers should treat 0 as "unknown".
197 * @return the number of hardware threads (0 if undetermined).
198 * @complexity O(1).
199 * @alloc none.
200 * @test CheatahOs.CwdAndCpuCount
201 * @crtest OsCompileRun.CpuCount
202 * @systest StdlibE2E.Os
203 */
204unsigned cpu_count();
205/**
206 * Run a shell command.
207 *
208 * Passes @p command to the system shell via `std::system` and blocks until it
209 * finishes; the returned status is implementation-defined (on POSIX, a wait
210 * status, conventionally decoded so that 0 means success).
211 * @param command the command line.
212 * @return the command's exit status.
213 * @complexity O(1) here + the cost of the spawned process (fork/exec via the shell).
214 * @alloc none.
215 * @warning @p command is interpreted by the shell (quoting, expansion, `;`/`|`) — never
216 * build it from untrusted input.
217 * @test CheatahOs.PidAndSystem
218 * @crtest OsCompileRun.System
219 * @systest StdlibE2E.Os
220 */
221int system(const std::string& command);
222/**
223 * Cryptographically secure random bytes (like Python's `os.urandom`).
224 *
225 * Reads @p n bytes from the operating system's CSPRNG — `getentropy`/`/dev/urandom`
226 * on POSIX, `BCryptGenRandom` on Windows — suitable for keys and signatures. Unlike
227 * the `random` module (a deterministic, seedable PRNG), this is NOT reproducible and
228 * must not be seeded. Throws `std::runtime_error` if the OS source cannot be read
229 * (so a key is never built from non-random bytes), and `std::invalid_argument` for a
230 * negative @p n.
231 * @param n the number of bytes to return (must be non-negative).
232 * @return a string of @p n random bytes (may contain embedded NULs).
233 * @complexity O(n), plus one syscall per 256-byte chunk on POSIX (getentropy's
234 * per-call limit; a single BCryptGenRandom call on Windows).
235 * @alloc allocates the n-byte result.
236 * @test CheatahOs.Urandom
237 * @crtest OsCompileRun.Urandom
238 * @systest StdlibE2E.Os
239 */
240std::string urandom(int n);
242/**
243 * The loadable-module file extension for this platform.
244 *
245 * A compiled cheatah program is a native loadable module run by the `cheatah`
246 * host; its file extension is `.so` on Linux/BSD, `.dylib` on macOS, and `.dll`
247 * on Windows. Tools that build or name modules (e.g. the `biome` package manager)
248 * use this instead of hardcoding `.so`, so the paths they print and generate are
249 * correct on every platform. The result includes the leading dot.
250 * @return the platform module extension (e.g. `".so"`, `".dylib"`, `".dll"`).
251 * @complexity O(1).
252 * @alloc allocates the returned string.
253 * @test CheatahOs.ModuleExt
254 * @crtest OsCompileRun.ModuleExt
255 * @systest StdlibE2E.Os
256 */
257std::string module_ext();
259/// os.path — the path-manipulation submodule.
260namespace path {
262/**
263 * Join path components with the platform separator.
264 *
265 * Appends each component with `path::operator/=`, inserting a separator as
266 * needed; following `std::filesystem` rules, an absolute component discards
267 * everything joined before it. Purely lexical — the filesystem is not touched.
268 * @param first the first component.
269 * @param rest any further string-constructible components.
270 * @return e.g. `join("a","b","c") -> "a/b/c"`.
271 * @complexity O(total length).
272 * @alloc allocates the result string and per-part path temporaries.
273 * @test CheatahOs.PathJoin
274 * @crtest OsCompileRun.PathJoin
275 * @systest StdlibE2E.Os
276 */
277template <StringLike... Parts>
278std::string join(const std::string& first, const Parts&... rest) {
279 std::filesystem::path p(first);
280 ((p /= std::filesystem::path(std::string(rest))), ...);
281 return p.string();
284/**
285 * Path existence test.
286 *
287 * Follows symlinks and is true for any existing entry — file, directory, or
288 * other; returns false for a missing path.
289 * @param p the path.
290 * @return true iff @p p exists.
291 * @complexity O(n) + a syscall.
292 * @alloc none.
293 * @warning The answer is a snapshot: the entry can be created or removed between this
294 * check and any subsequent use (TOCTOU) — do not rely on it as a security check.
295 * @test CheatahOs.MakeDirExistsThenRemove
296 * @crtest OsCompileRun.PathExists
297 * @systest StdlibE2E.Os
298 */
299bool exists(const std::string& p);
300/**
301 * Regular-file test.
302 *
303 * Returns false (rather than throwing) when @p p is missing or is a non-regular
304 * entry such as a directory; symlinks are followed to their target.
305 * @param p the path.
306 * @return true iff @p p is a regular file.
307 * @complexity O(n) + a syscall.
308 * @alloc none.
309 * @test CheatahOs.FileQueriesIsfileAndGetsize
310 * @crtest OsCompileRun.PathIsfile
311 * @systest StdlibE2E.Os
312 */
313bool isfile(const std::string& p);
314/**
315 * Directory test.
316 *
317 * Returns false (rather than throwing) when @p p is missing or is not a
318 * directory; symlinks are followed to their target.
319 * @param p the path.
320 * @return true iff @p p is a directory.
321 * @complexity O(n) + a syscall.
322 * @alloc none.
323 * @test CheatahOs.MakeDirExistsThenRemove
324 * @crtest OsCompileRun.PathIsdir
325 * @systest StdlibE2E.Os
326 */
327bool isdir(const std::string& p);
328/**
329 * Final path component.
330 *
331 * Returns the trailing filename component lexically, without touching the
332 * filesystem; a path ending in a separator (e.g. `a/b/`) yields an empty
333 * string, matching `std::filesystem::path::filename`.
334 * @param p the path.
335 * @return the basename (filename).
336 * @complexity O(n).
337 * @alloc allocates a path temporary and the result string.
338 * @test CheatahOs.PathBasenameDirname
339 * @crtest OsCompileRun.PathBasename
340 * @systest StdlibE2E.Os
341 */
342std::string basename(const std::string& p);
343/**
344 * Parent path.
345 *
346 * Returns everything before the final component lexically, without touching the
347 * filesystem; a bare filename with no separator (e.g. `file.txt`) yields an
348 * empty string, matching `std::filesystem::path::parent_path`.
349 * @param p the path.
350 * @return the directory portion of @p p.
351 * @complexity O(n).
352 * @alloc allocates a path temporary and the result string.
353 * @test CheatahOs.PathBasenameDirname
354 * @crtest OsCompileRun.PathDirname
355 * @systest StdlibE2E.Os
356 */
357std::string dirname(const std::string& p);
358/**
359 * Absolute path.
360 *
361 * Prepends the current working directory to a relative @p p; it does not
362 * collapse `.`/`..` segments or resolve symlinks (combine with normpath for
363 * that), and @p p need not exist.
364 * @param p the path.
365 * @return @p p resolved against the cwd.
366 * @complexity O(n) + a syscall (reads the cwd).
367 * @alloc allocates the result string.
368 * @test CheatahOs.AbspathAndNormpath
369 * @crtest OsCompileRun.PathAbspath
370 * @systest StdlibE2E.Os
371 */
372std::string abspath(const std::string& p);
373/**
374 * Lexically normalized path (collapses current-dir and parent-dir segments).
375 * @param p the path.
376 * @return the normalized path.
377 * @complexity O(n) (purely lexical, no syscall).
378 * @alloc allocates a path temporary and the result string.
379 * @test CheatahOs.AbspathAndNormpath
380 * @crtest OsCompileRun.PathNormpath
381 * @systest StdlibE2E.Os
382 */
383std::string normpath(const std::string& p);
384/**
385 * File size in bytes.
386 *
387 * Defined only for regular files; querying a missing path, or a directory or
388 * other non-regular entry, throws rather than returning a sentinel.
389 * @param p the file path.
390 * @return @p p's size.
391 * @complexity O(1) + a syscall.
392 * @alloc none.
393 * @test CheatahOs.FileQueriesIsfileAndGetsize
394 * @crtest OsCompileRun.PathGetsize
395 * @systest StdlibE2E.Os
396 */
397std::uintmax_t getsize(const std::string& p);
399/**
400 * Split a path into root and extension.
401 *
402 * Splits at the last dot of the final component so that concatenating the two
403 * results reproduces @p p; when there is no extension the whole path is the
404 * root and the extension is empty. The extension includes its leading dot, and
405 * a leading-dot name (e.g. `.bashrc`) is treated as having no extension.
406 * @param p the path.
407 * @return e.g. `splitext("dir/file.purr") -> {"dir/file", ".purr"}` (empty extension when
408 * none).
409 * @complexity O(n).
410 * @alloc allocates the two result strings and a path temporary.
411 * @test CheatahOs.PathSplitext
412 * @crtest OsCompileRun.PathSplitext
413 * @systest StdlibE2E.Os
414 */
415std::pair<std::string, std::string> splitext(const std::string& p);
417} // namespace path
418} // namespace cheatah::os