Platform Utilities¶
The platform module provides cross-platform utilities for interacting with the operating system.
environment¶
-
class environment¶
Utility class for safe environment variable access.
The environment class provides safe, cross-platform access to environment variables using std::optional to handle cases where variables may not exist. This approach avoids the security risks and undefined behavior associated with direct getenv() usage.
Key features:
Safe environment variable access with std::optional
Cross-platform compatibility (Unix, Windows)
No undefined behavior for missing variables
Thread-safe read operations
Security considerations:
Always check if a variable exists before using it
Be aware that environment variables are visible to all processes
Avoid storing sensitive information in environment variables
Performance characteristics:
O(1) typical case for environment variable lookup
Thread-safe for concurrent read access
No dynamic memory allocation for the API itself
// Basic usage if (auto home = environment::value("HOME")) { std::cout << "Home directory: " << *home << std::endl; } else { std::cout << "HOME environment variable not set" << std::endl; } // Providing defaults std::string shell = environment::value("SHELL").value_or("/bin/sh"); // Common environment variables auto path = environment::value("PATH"); auto user = environment::value("USER"); auto temp = environment::value("TMPDIR").value_or("/tmp");
Public Static Functions
-
static std::optional<std::string> value(const std::string &name)¶
Get the value of an environment variable.
Safely retrieves the value of the specified environment variable. Returns std::nullopt only when the variable is not set. A variable set to an empty string yields an optional holding an empty string.
Thread safety:
Safe for concurrent read access from multiple threads
Environment modifications during execution may not be visible
Platform notes:
On Unix-like systems, uses getenv() internally
On Windows, uses GetEnvironmentVariable() internally
Variable names are case-sensitive on Unix, case-insensitive on Windows
// Check if variable exists if (auto value = environment::value("MY_VAR")) { std::cout << "MY_VAR = " << *value << std::endl; } // Use with default value std::string config_dir = environment::value("CONFIG_DIR") .value_or("/etc/myapp");
- Parameters:
name – The name of the environment variable to retrieve
- Returns:
std::optional containing the value if the variable exists, std::nullopt otherwise
The environment class provides read-only access to environment variables
through a single static accessor, value():
#include <iostream>
#include <string>
#include <dross/platform/environment.h>
// Read an environment variable. The result is std::nullopt only when
// the variable is unset; a variable set to "" yields an optional
// holding an empty string. Handle the missing case explicitly.
if (auto home = dross::environment::value("HOME")) {
std::cout << "Home directory: " << *home << std::endl;
} else {
std::cout << "HOME is not set" << std::endl;
}
// Or fold the missing case into a default
std::string shell = dross::environment::value("SHELL").value_or("/bin/sh");
std::cout << "Shell: " << shell << std::endl;
path¶
-
class path¶
Cross-platform filesystem path operations with error handling.
The path class provides a safe, cross-platform wrapper around filesystem operations using std::filesystem as the underlying implementation. It uses std::expected for operations that may fail and std::optional for operations that may not return a value.
Key features:
Cross-platform path handling (Windows, Unix-like systems)
Safe error handling with std::expected and std::optional
Path expansion and resolution
Directory creation with proper error reporting
Integration with std::filesystem
Error handling:
Uses std::expected<path, std::filesystem::filesystem_error> for fallible operations
Uses std::optional<path> for operations that may not return a value
No exceptions thrown directly, but std::filesystem ones propagate: exists() and the default constructor call throwing std::filesystem functions
Performance characteristics:
Thin wrapper over std::filesystem with minimal overhead
Path operations are typically O(1) or O(path_length)
Filesystem I/O operations depend on underlying system performance
Thread safety:
Path objects are not thread-safe for modification
Const operations are thread-safe
Filesystem operations may have race conditions inherent to the filesystem
// Basic path operations path config_path{"~/.config/myapp"}; if (auto expanded = config_path.expand()) { std::cout << "Expanded path: " << expanded->string() << std::endl; } // Directory creation path new_dir{"/tmp/myapp/data"}; if (auto result = path::mkdir(new_dir.string())) { std::cout << "Directory created: " << result->string() << std::endl; } else { std::cerr << "Failed to create directory: " << result.error().what() << std::endl; } // Path building if (auto home = path::home()) { path config_file = home->append("myapp").append("config.toml"); if (config_file.exists()) { // Process config file } }
Public Functions
-
path()¶
Default constructor holding the current working directory.
Resolves “.” to an absolute path, so the result is the working directory at the time of construction, not an empty path. Uses the throwing form of std::filesystem::absolute.
-
path(const std::string &path_str)¶
Construct a path from a string.
Creates a path object from the given string. The string is interpreted using the native path format for the current platform.
- Parameters:
path_str – The path string to construct from
-
path(const std::filesystem::path &fs_path)¶
Construct a path from a std::filesystem::path.
Creates a path object wrapping the given std::filesystem::path.
- Parameters:
fs_path – The filesystem path to construct from
-
bool exists() const¶
Check if the path exists in the filesystem.
Checks whether the path refers to an existing filesystem entity. This includes files, directories, symbolic links, and other filesystem objects.
Uses the throwing form of std::filesystem::exists. A path that is merely absent yields false, but an error while querying it — an over-long name, or a directory the process may not traverse — escapes as a std::filesystem::filesystem_error.
- Returns:
true if the path exists (file or directory), false otherwise
-
path append(const std::string &component) const¶
Append a path component to this path.
Creates a new path by appending the given component using the platform-appropriate path separator. Does not modify this path object.
path base{"/usr/local"}; path full = base.append("bin").append("myapp"); // Result: "/usr/local/bin/myapp"
- Parameters:
component – The path component to append
- Returns:
New path object with the component appended
-
std::string string() const¶
Get the string representation of the path.
Returns the path as a string using the native format for the current platform. On Unix-like systems, uses forward slashes. On Windows, uses backslashes.
- Returns:
String representation using native path format
-
std::expected<path, std::filesystem::filesystem_error> expand() const¶
Expand user home directory (~) in the path.
Expands tilde (~) notation to the actual home directory path, then canonicalises the result — the returned path has symbolic links resolved. Because canonicalisation requires the target to exist, expand() returns unexpected when the expanded path does not (yet) exist. A path that does not start with “~” is returned unchanged and always succeeds.
path user_config{std::string{"~/.config/myapp"}}; if (auto expanded = user_config.expand()) { // expanded contains something like "/home/user/.config/myapp" } else { // Suppose the target does not exist yet. mkdir() does not // expand ~, so build the path from path::home() before // creating it. }
- Returns:
Expected containing the expanded path on success, or filesystem_error on failure
-
std::expected<path, std::filesystem::filesystem_error> resolve() const¶
Resolve the path to an absolute, canonical form.
Converts the path to an absolute path and resolves any symbolic links, “.” and “..” components. The resulting path is in canonical form.
path relative{"../config/../data/file.txt"}; if (auto resolved = relative.resolve()) { // resolved contains the canonical absolute path }
- Returns:
Expected containing the resolved path on success, or filesystem_error on failure
-
operator std::string() const¶
Convert to string representation.
Implicit conversion to string for convenient usage with APIs that expect string paths.
- Returns:
String representation of the path
-
operator std::filesystem::path() const¶
Convert to std::filesystem::path.
Provides access to the underlying std::filesystem::path for interoperability with standard library filesystem operations.
- Returns:
The underlying std::filesystem::path object
Public Static Functions
-
static std::expected<path, std::filesystem::filesystem_error> mkdir(const std::string &dir_path)¶
Create a directory from a string path.
Creates the specified directory and any necessary parent directories. Succeeds both when it creates the directory and when dir_path is already a directory — the call is idempotent. It fails only when the underlying std::filesystem::create_directories call reports an actual error. See the std::filesystem::path overload for the failure and safety notes.
if (auto result = path::mkdir("/tmp/myapp/data")) { std::cout << "Created: " << result->string() << std::endl; } else { std::cerr << "Error: " << result.error().what() << std::endl; }
- Parameters:
dir_path – The directory path to create as a string
- Returns:
Expected containing the created path on success, or filesystem_error on failure
-
static std::expected<path, std::filesystem::filesystem_error> mkdir(const std::filesystem::path &dir_path)¶
Create a directory from a filesystem::path.
Creates the specified directory and any necessary parent directories. Succeeds both when it creates the directory and when dir_path is already a directory — the call is idempotent, closer to “ensure this directory exists” than a strict create. It fails only when std::filesystem::create_directories reports an actual error, for example when a path component exists and is not a directory. The operation is not atomic — directories created before the failure may remain. Some failures are rejected before anything is created at all. Because an already-present directory is accepted without inspection, a directory, or a symbolic link that resolves to one, left there by another party is accepted too. Checking beforehand does not close that gap — the check and the use are separate operations, and the entry can be replaced in between. This call does not check who owns the directories along the path, what their permissions are, or where any links beneath them point, and it does not set the permissions of the directories it creates: those are left to the platform’s default for new directories, which can be group- or world-writable. A caller who needs any of that has to arrange it separately. This overload holds the logic; the std::string one forwards to it.
- Parameters:
dir_path – The directory path to create as a filesystem::path
- Returns:
Expected containing the created path on success, or filesystem_error on failure
-
static std::optional<path> home()¶
Get the user’s home directory.
Attempts to determine the user’s home directory using platform-appropriate methods:
Unix-like systems: $HOME environment variable
Windows: USERPROFILE% or HOMEDRIVE%HOMEPATH%
if (auto home = path::home()) { path config = home->append(".config"); } else { // Handle case where home directory cannot be determined }
- Returns:
Optional containing the home directory path, or std::nullopt if not determinable
-
static std::string separator()¶
Get the platform-appropriate path separator.
Returns the native path separator for the current platform. Useful for building paths manually or for display purposes.
- Returns:
String containing the path separator (“/” on Unix, “\” on Windows)
The path class provides filesystem path operations:
#include <iostream>
#include <string>
#include <dross/platform/path.h>
// home() returns std::optional<path>; append() builds on top of it
if (auto home = dross::path::home()) {
dross::path config_path = home->append(".config").append("app");
std::cout << "Config path: " << config_path.string() << std::endl;
// Create the directory, including any missing parents. mkdir() is
// idempotent: it succeeds whether it creates the directory or
// finds it already there.
if (auto created = dross::path::mkdir(config_path.string())) {
std::cout << "Created: " << created->string() << std::endl;
} else {
std::cerr << "mkdir: " << created.error().what() << std::endl;
}
}
// A bare string literal is ambiguous between the std::string and the
// std::filesystem::path constructor, so name the type you mean.
dross::path relative{std::string{"../file.txt"}};
// Convert to an absolute, canonical path
if (auto resolved = relative.resolve()) {
std::cout << "Resolved: " << resolved->string() << std::endl;
} else {
std::cerr << "Resolve failed: " << resolved.error().what() << std::endl;
}
// Check whether a path exists
if (relative.exists()) {
std::cout << "The relative path exists" << std::endl;
}
// Expand a leading ~ to the home directory. For a ~ path, expand()
// canonicalises and turns any failure the standard library reports as
// a std::filesystem::filesystem_error -- a missing target, a
// permission problem, a symlink loop -- into the returned
// std::expected instead of letting it escape. A path that does not
// begin with ~ is returned unchanged.
dross::path user_config{std::string{"~/.config/app"}};
if (auto expanded = user_config.expand()) {
std::cout << "Expanded: " << expanded->string() << std::endl;
} else {
std::cerr << "Expand failed: " << expanded.error().what() << std::endl;
}
Path Operations¶
Building and inspecting a path, without touching the filesystem:
append() - Return a new path with a component appended
string() - Get the native string representation
separator() - Get the platform’s path separator (static)
Filesystem Operations¶
Operations that consult the filesystem:
exists() - Check whether the path exists
expand() - Expand a leading
~to the home directoryresolve() - Convert to an absolute, canonical path
mkdir() - Create a directory and any missing parents (static)
home() - Get the user’s home directory (static)
expand(), resolve() and mkdir() are declared to return
std::expected<path, std::filesystem::filesystem_error>; home() returns
std::optional<path>. Reading and writing file contents is not part of
path — use the standard library’s <fstream> for that.
Some caveats apply to the current implementation:
mkdir()is idempotent: it succeeds whether it creates the directory or finds it already there. It fails only whenstd::filesystem::create_directoriesreports an actual error, such as a path component that exists and is not a directory. The operation is not atomic — directories created before the failure may remain. Some failures are rejected before anything is created at all. Because an already-present directory is accepted without inspection, a directory, or a symbolic link that resolves to one, left there by another party is accepted too. Checking beforehand does not close that gap — the check and the use are separate operations, and the entry can be replaced in between. This call does not check who owns the directories along the path, what their permissions are, or where any links beneath them point, and it does not set the permissions of the directories it creates: those are left to the platform’s default for new directories, which can be group- or world-writable. A caller who needs any of that has to arrange it separately.expand()routes canonicalisation failures through its return type. For a path beginning with~it canonicalises and converts any error the standard library reports as astd::filesystem::filesystem_error— a missing target, a permission problem, a symlink loop, an invalid component — into the returnedstd::expectedrather than letting it escape. A path that does not begin with~is returned unchanged. Home directory resolution failing (path::home()returningstd::nullopt) is reported the same way.exists()calls the throwing form ofstd::filesystem::exists. An absent path is simplyfalse, but an error while querying it — an over-long name, or a directory the process may not traverse — escapes as astd::filesystem::filesystem_error.
xdg¶
-
class xdg¶
XDG Base Directory Specification implementation for proper app data storage.
The xdg class implements the XDG Base Directory Specification, which defines standard locations for application data, configuration, cache, and state files on Unix-like systems. This ensures applications store their data in appropriate locations that integrate well with the desktop environment and user expectations.
Key features:
XDG Base Directory Specification compliance
Automatic fallback to standard directories when XDG variables are unset
Application-specific subdirectory creation
Cross-platform compatibility (Unix-like systems primarily)
XDG directories:
Config: User-specific configuration files
Data: User-specific data files
Cache: User-specific non-essential cached data
State: User-specific state data (logs, history, etc.)
Environment variables used:
XDG_CONFIG_HOME (default: ~/.config)
XDG_DATA_HOME (default: ~/.local/share)
XDG_CACHE_HOME (default: ~/.cache)
XDG_STATE_HOME (default: ~/.local/state)
// Application-specific XDG directories xdg app_dirs{"myapp"}; // Get configuration directory if (auto config_dir = app_dirs.config_home()) { path config_file = path{*config_dir} / "config.toml"; // Store configuration in ~/.config/myapp/config.toml } // Get data directory if (auto data_dir = app_dirs.data_home()) { path db_file = path{*data_dir} / "database.sqlite"; // Store data in ~/.local/share/myapp/database.sqlite } // Get cache directory if (auto cache_dir = app_dirs.cache_home()) { path cache_file = path{*cache_dir} / "thumbnails"; // Store cache in ~/.cache/myapp/thumbnails }
Platform compatibility:
Full support on Linux and other Unix-like systems
Limited support on macOS (uses similar directory structure)
Not applicable on Windows (consider using appropriate Windows APIs)
Public Functions
-
xdg(const std::string &app_name)¶
Construct XDG directory helper for a specific application.
Creates an XDG directory helper that will create application-specific subdirectories within the standard XDG base directories. The application name is used as the subdirectory name.
The application name should be:
A valid directory name (no path separators)
Unique to your application
Following naming conventions (lowercase, hyphens for separation)
xdg app_dirs{"my-awesome-app"}; // Will create directories like ~/.config/my-awesome-app/
- Parameters:
app_name – The name of the application for directory creation
-
std::optional<std::string> config_home() const¶
Get the application’s configuration directory.
Returns the application-specific configuration directory according to XDG Base Directory Specification:
Uses $XDG_CONFIG_HOME/app_name if XDG_CONFIG_HOME is set
Falls back to $HOME/.config/app_name
Returns std::nullopt if HOME cannot be determined
The returned directory may not exist yet. Use path::mkdir() to create it.
xdg app{"myapp"}; if (auto config_dir = app.config_home()) { // Typically returns something like "/home/user/.config/myapp" path config_path{*config_dir}; if (auto result = path::mkdir(config_path.string())) { // Directory ready for config files, whether just created or // already there } }
- Returns:
Optional containing the config directory path, or std::nullopt on error
-
std::optional<std::string> data_home() const¶
Get the application’s data directory.
Returns the application-specific data directory according to XDG Base Directory Specification:
Uses $XDG_DATA_HOME/app_name if XDG_DATA_HOME is set
Falls back to $HOME/.local/share/app_name
Returns std::nullopt if HOME cannot be determined
Use this directory for application data files, databases, etc.
- Returns:
Optional containing the data directory path, or std::nullopt on error
-
std::optional<std::string> cache_home() const¶
Get the application’s cache directory.
Returns the application-specific cache directory according to XDG Base Directory Specification:
Uses $XDG_CACHE_HOME/app_name if XDG_CACHE_HOME is set
Falls back to $HOME/.cache/app_name
Returns std::nullopt if HOME cannot be determined
Use this directory for non-essential cached data that can be regenerated. Cache files may be deleted by system cleanup tools.
- Returns:
Optional containing the cache directory path, or std::nullopt on error
-
std::optional<std::string> state_home() const¶
Get the application’s state directory.
Returns the application-specific state directory according to XDG Base Directory Specification:
Uses $XDG_STATE_HOME/app_name if XDG_STATE_HOME is set
Falls back to $HOME/.local/state/app_name
Returns std::nullopt if HOME cannot be determined
Use this directory for state data like logs, history, recently used files, etc. This data should persist between application runs but is not user configuration.
- Returns:
Optional containing the state directory path, or std::nullopt on error
The xdg class implements the XDG Base Directory Specification:
#include <iostream>
#include <dross/platform/xdg.h>
// The accessors are instance methods: the application name given here is
// appended to every directory they return.
dross::xdg app{"myapp"};
// User-specific data directory
if (auto data_home = app.data_home()) {
std::cout << "Data: " << *data_home << std::endl;
// Default: $HOME/.local/share/myapp
}
// User-specific configuration directory
if (auto config_home = app.config_home()) {
std::cout << "Config: " << *config_home << std::endl;
// Default: $HOME/.config/myapp
}
// User-specific cache directory
if (auto cache_home = app.cache_home()) {
std::cout << "Cache: " << *cache_home << std::endl;
// Default: $HOME/.cache/myapp
}
// User-specific state directory
if (auto state_home = app.state_home()) {
std::cout << "State: " << *state_home << std::endl;
// Default: $HOME/.local/state/myapp
}
XDG Directories¶
xdg exposes the four per-user base directories, each already suffixed with
the application name passed to the constructor:
data_home() - Application data that should persist
config_home() - User-specific configuration files
cache_home() - Non-essential cached data
state_home() - Application state data (logs, history, etc.)
Every accessor returns std::optional<std::string> and yields
std::nullopt when the home directory cannot be determined. The directory
itself is not created for you — pass the result to path::mkdir(), which
accepts an already-present directory as success.
Example Usage¶
Creating application directories:
#include <iostream>
#include <dross/platform/path.h>
#include <dross/platform/xdg.h>
dross::xdg app{"myapp"};
// Create the config directory, then name a file inside it. mkdir()
// accepts an already-present directory as success, so any failure
// here is a real problem.
if (auto config_home = app.config_home()) {
const dross::path config_dir{*config_home};
if (auto created = dross::path::mkdir(*config_home); !created) {
std::cerr << "mkdir: " << created.error().what() << std::endl;
}
dross::path config_file = config_dir.append("settings.toml");
std::cout << "Config file: " << config_file.string() << std::endl;
}
// The data directory works the same way
if (auto data_home = app.data_home()) {
dross::path data_file = dross::path{*data_home}.append("database.db");
std::cout << "Data file: " << data_file.string() << std::endl;
}
// ...and so does the cache directory
if (auto cache_home = app.cache_home()) {
dross::path cache_file =
dross::path{*cache_home}.append("thumbnails.cache");
std::cout << "Cache file: " << cache_file.string() << std::endl;
}
Platform Considerations¶
Windows Support¶
On Windows systems:
XDG directories map to appropriate Windows locations
Path separators are handled automatically
Environment variables use Windows conventions
macOS Support¶
On macOS:
XDG directories follow macOS conventions where appropriate
~/Librarypaths are used for some directoriesFull POSIX compatibility is maintained
Error Handling¶
Fallible path operations return
std::expected<path, std::filesystem::filesystem_error>. The error type is
the standard library’s, so it is inspected with code() and reported with
what():
auto result = dross::path::mkdir(std::string{"/nonexistent/dir"});
if (!result) {
const std::error_code code = result.error().code();
if (code == std::errc::no_such_file_or_directory) {
std::cerr << "No such file or directory" << std::endl;
} else if (code == std::errc::permission_denied) {
std::cerr << "Permission denied" << std::endl;
} else {
std::cerr << "Error: " << result.error().what() << std::endl;
}
}