PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
scratch_directory.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <cerrno>
4#include <cstdint>
5#include <cstdlib>
6#include <fcntl.h>
7#include <string>
8#include <system_error>
9#include <sys/stat.h>
10#include <unistd.h>
11#if defined(__linux__)
12#include <sys/vfs.h>
13#elif defined(__APPLE__)
14#include <sys/param.h>
15#include <sys/mount.h>
16#endif
17
18namespace pineforge::detail {
19
20inline const char* scratch_memory_filesystem_name(std::uint64_t type) noexcept {
21 // Normalize f_type on platforms where the filesystem word is signed.
22 const auto magic = static_cast<std::uint32_t>(type);
23 if (magic == UINT32_C(0x01021994)) return "tmpfs";
24 if (magic == UINT32_C(0x858458f6)) return "ramfs";
25 return nullptr;
26}
27
28inline bool scratch_filesystem_is_memory(std::uint64_t type) noexcept {
29 return scratch_memory_filesystem_name(type) != nullptr;
30}
31
32// Keep the historical operation alone unless a selection note needs the
33// selected directory too. Shared by both file-create implementations.
34inline std::string scratch_create_message(const char* operation,
35 const std::string& directory,
36 const std::string& selection_note) {
37 if (selection_note.empty()) return operation;
38 return std::string(operation) + "; directory '" + directory + "'; " + selection_note;
39}
40
41// Translate only create selection/refusal notes, never an inner operation or
42// its system_error suffix. The outer typed error supplies its own suffix.
43inline std::string scratch_failure_detail(const char* what, int errno_value) {
44 std::string message(what);
45 const auto suffix = ": " + std::error_code(errno_value, std::generic_category()).message();
46 if (message.size() >= suffix.size()
47 && message.compare(message.size() - suffix.size(), suffix.size(), suffix) == 0)
48 message.resize(message.size() - suffix.size());
49 constexpr char create_prefix[] = "scratch create; ";
50 constexpr char refusal_prefix[] = "scratch requires disk backing; ";
51 if (message.compare(0, sizeof(create_prefix) - 1, create_prefix) == 0)
52 return message.substr(sizeof(create_prefix) - 1);
53 // The refusal itself is part of the note; retain its established wording.
54 if (message.compare(0, sizeof(refusal_prefix) - 1, refusal_prefix) == 0)
55 return message;
56 return {};
57}
58
59inline std::string scratch_with_detail(const char* prefix, const char* what, int errno_value) {
60 const auto detail = scratch_failure_detail(what, errno_value);
61 return detail.empty() ? std::string(prefix) : std::string(prefix) + "; " + detail;
62}
63
64// Return an errno, not a boolean: a failed query is not proof of disk backing.
65inline int scratch_directory_filesystem_type(const char* path, std::uint64_t& type) noexcept {
66#if defined(__linux__) || defined(__APPLE__)
67 // A filesystem type is evidence only for a searchable directory. In
68 // particular, statfs("/dev/null") can report tmpfs rather than ENOTDIR.
69 // Do not require read permission: a write/search-only directory is usable.
70 struct stat directory{};
71 int status;
72 do { status = ::stat(path, &directory); } while (status != 0 && errno == EINTR);
73 if (status != 0) return errno;
74 if (!S_ISDIR(directory.st_mode)) return ENOTDIR;
75#if defined(AT_EACCESS)
76 // Match the effective credentials used by mkstemp.
77 do { status = ::faccessat(AT_FDCWD, path, X_OK, AT_EACCESS); }
78 while (status != 0 && errno == EINTR);
79#else
80 // Without AT_EACCESS, retain the available real-id search-permission check.
81 do { status = ::access(path, X_OK); } while (status != 0 && errno == EINTR);
82#endif
83 if (status != 0) return errno;
84 struct statfs filesystem{};
85 do { status = ::statfs(path, &filesystem); } while (status != 0 && errno == EINTR);
86 if (status != 0) return errno;
87 type = static_cast<std::uint32_t>(filesystem.f_type);
88 return 0;
89#else
90 (void)path;
91 (void)type;
92 return ENOTSUP;
93#endif
94}
95
96// Empty return => refusal/error are populated. proven_memory distinguishes a
97// successful memory-filesystem query from a query that itself returns ENODEV.
98// A successfully queried fallback retains its selection note for a later
99// create failure; accepted first/explicit candidates have no rejection note.
100// No directory is created here.
101// A nonempty explicit path is never replaced or normalized. With no explicit
102// path, retain the legacy TMPDIR-or-/tmp choice; only proven tmpfs/ramfs permits
103// trying /var/tmp. ENOTSUP supplies no filesystem evidence: keep that candidate
104// with error 0, proven_memory false and no note, including a reached fallback.
105// Other default query errors stop with their errno; explicit query errors leave
106// creation to the caller. /tmp is used only if TMPDIR is unset/empty.
107// Probe has signature int(const char*, uint64_t&) and returns a syscall errno.
108// The injected form lets tests cover a tmpfs /tmp without changing mounts.
109template<class Probe>
110inline std::string resolve_scratch_directory(const std::string& configured,
111 std::string& refusal, int& error,
112 bool& proven_memory, Probe probe) {
113 refusal.clear();
114 error = 0;
115 proven_memory = false;
116 int query_error = 0;
117 const auto reject = [&](const std::string& path, const char* source) {
118 std::uint64_t type = 0;
119 query_error = probe(path.c_str(), type);
120 proven_memory = query_error == 0 && scratch_filesystem_is_memory(type);
121 if (query_error == ENOTSUP) {
122 // Unsupported probing supplies no evidence in either direction.
123 refusal.clear();
124 error = 0;
125 return false;
126 }
127 if (query_error == 0 && !proven_memory) return false;
128 if (!refusal.empty()) refusal += "; ";
129 refusal += source;
130 refusal += " '";
131 refusal += path;
132 refusal += "': ";
133 if (query_error != 0) {
134 error = query_error;
135 refusal += "directory check failed (errno ";
136 refusal += std::to_string(query_error);
137 refusal += ": ";
138 refusal += std::error_code(query_error, std::generic_category()).message();
139 refusal += ')';
140 } else {
141 error = ENODEV;
142 refusal += scratch_memory_filesystem_name(type);
143 refusal += " requires disk backing, not tmpfs or ramfs";
144 }
145 return true;
146 };
147 if (!configured.empty()) {
148 if (!reject(configured, "explicit directory")) return configured;
149 // Preserve the caller's original create errno for an invalid explicit
150 // path. A query failure is no reason to refuse or redirect that path.
151 if (query_error != 0) {
152 refusal.clear();
153 error = 0;
154 return configured;
155 }
156 refusal.insert(0, "scratch requires disk backing; ");
157 return {};
158 }
159 const char* temporary = std::getenv("TMPDIR");
160 const bool has_temporary = temporary && *temporary;
161 const std::string first = has_temporary ? temporary : "/tmp";
162 if (!reject(first, has_temporary ? "TMPDIR" : "legacy /tmp (TMPDIR unset or empty)"))
163 return first;
164 // Only a successful tmpfs/ramfs probe permits fallback; even ENODEV from
165 // the probe itself is a hard query failure, with proven_memory == false.
166 if (query_error != 0) {
167 refusal.insert(0, "scratch create; ");
168 return {};
169 }
170 if (!reject("/var/tmp", "fallback")) {
171 if (query_error != ENOTSUP)
172 refusal += "; selected fallback '/var/tmp'; set TMPDIR to a disk-backed directory";
173 error = 0;
174 return "/var/tmp";
175 }
176 if (query_error != 0) {
177 refusal.insert(0, "scratch create; ");
178 return {};
179 }
180 // Both default candidates are proven memory-backed. Keep the established
181 // errno/text and add every rejected candidate and a remedy.
182 error = ENODEV;
183 refusal.insert(0, "scratch requires disk backing; ");
184 refusal += "; set TMPDIR to a disk-backed directory";
185 return {};
186}
187
188inline std::string resolve_scratch_directory(const std::string& configured,
189 std::string& refusal, int& error,
190 bool& proven_memory) {
191 return resolve_scratch_directory(configured, refusal, error, proven_memory,
193}
194
195inline std::string resolve_scratch_directory(const std::string& configured,
196 std::string& refusal, int& error) {
197 bool ignored_memory = false;
198 return resolve_scratch_directory(configured, refusal, error, ignored_memory);
199}
200
201} // namespace pineforge::detail
std::string scratch_with_detail(const char *prefix, const char *what, int errno_value)
std::string scratch_failure_detail(const char *what, int errno_value)
std::string resolve_scratch_directory(const std::string &configured, std::string &refusal, int &error, bool &proven_memory, Probe probe)
const char * scratch_memory_filesystem_name(std::uint64_t type) noexcept
int scratch_directory_filesystem_type(const char *path, std::uint64_t &type) noexcept
bool scratch_filesystem_is_memory(std::uint64_t type) noexcept
std::string scratch_create_message(const char *operation, const std::string &directory, const std::string &selection_note)