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

path(const path &other)

Copy constructor.

Parameters:

other – The path to copy 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 directory

  • resolve() - 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 when std::filesystem::create_directories reports 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 a std::filesystem::filesystem_error — a missing target, a permission problem, a symlink loop, an invalid component — into the returned std::expected rather than letting it escape. A path that does not begin with ~ is returned unchanged. Home directory resolution failing (path::home() returning std::nullopt) is reported the same way.

  • exists() calls the throwing form of std::filesystem::exists. An absent path is simply 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.

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

  • ~/Library paths are used for some directories

  • Full 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;
    }
}