You can provide a custom `FileSystem` to serve pages and assets directly from an engine asset system, a pack file, or memory. The library routes every `file:///` URL and internal resource file through this handler.

## The Default File System

When you call `App::Create()`, the library automatically installs a file system that reads from the OS, rooted at `Settings::file_system_path` (`./assets/` by default).

`Renderer::Create()` does not install a file system— one is required, so you must provide one. You can call `GetPlatformFileSystem()` to use the default OS file system without creating an `App`.

To replace the default, set a custom file system on `Platform` before calling `App::Create()` or `Renderer::Create()`— see [Setting Up the Platform](/docs/2.0/setting-up-the-platform).

## Implementing the FileSystem Interface

Subclass `FileSystem`, override its four methods, and pass an instance to `Platform::instance().set_file_system()`.

Every file path passed to these methods is relative— it is the path that follows the `file:///` prefix.

| Method | Description |
| :--- | :--- |
| `FileExists()` | Whether a file exists at the specified path. |
| `GetFileMimeType()` | The MIME type of the file, typically determined by its extension (eg, `text/html`). If the type is unknown, return `application/unknown`. |
| `GetFileCharset()` | The character encoding of a text file (eg, `utf-8`). If the encoding cannot be determined, return `utf-8`. |
| `OpenFile()` | The file contents wrapped in a `Buffer`, or `nullptr` if the file cannot be opened. |

> 🚧 Thread Safety Required
>
> The library calls these methods from the Renderer's thread and from a background thread used to load `file:///` URLs. Both threads can access the file system at the same time, so the implementation must be safe to call from multiple threads at once.

## Returning File Data from OpenFile()

`OpenFile()` returns the contents of a file wrapped in a `Buffer`.

File data addresses should be aligned to 16-byte boundaries— the ICU data file requires this alignment.

### Wrapping Existing Memory

To avoid copying file data into the renderer, call `Buffer::Create()` to wrap existing memory (eg, a memory-mapped file).

You can pass an optional destruction callback that runs when the library releases the buffer, letting you unmap or free the data.

### Copying File Data

Call `Buffer::CreateFromCopy()` to have the library allocate and manage its own copy of the data.

Use this when the source memory will not stay valid until the buffer is released (eg, a scratch buffer reused for subsequent files), or when you cannot guarantee 16-byte alignment.

## Example Implementation

The following example implements a file system that wraps memory-mapped files and copies data from scratch memory.

```cpp
#include <Ultralight/Ultralight.h>
#include <cstring>

using namespace ultralight;

///
/// Does the path end with the given extension?
///
static bool HasExtension(const String8& path, const char* ext) {
  size_t ext_len = strlen(ext);
  return path.length() >= ext_len &&
         strcmp(path.data() + path.length() - ext_len, ext) == 0;
}

///
/// Runs when the library releases a Buffer we created with Buffer::Create.
///
static void OnUnmapAsset(void* user_data, void* data) {
  // Pseudo-code, unmap the file.
  UnmapAsset(data);
}

class MyFileSystem : public FileSystem {
 public:
  bool FileExists(const String& file_path) override {
    // Pseudo-code, ask your asset store whether the file exists.
    return AssetExists(file_path.utf8().data());
  }

  String GetFileMimeType(const String& file_path) override {
    ///
    /// Decide by extension, and admit it when we can't tell.
    ///
    const String8& path = file_path.utf8();
    if (HasExtension(path, ".html"))
      return "text/html";
    if (HasExtension(path, ".css"))
      return "text/css";
    if (HasExtension(path, ".js"))
      return "text/javascript";
    if (HasExtension(path, ".png"))
      return "image/png";
    return "application/unknown";
  }

  String GetFileCharset(const String& file_path) override {
    ///
    /// Only matters for text files-- our assets are all UTF-8.
    ///
    return "utf-8";
  }

  RefPtr<Buffer> OpenFile(const String& file_path) override {
    const char* path = file_path.utf8().data();
    void* data = nullptr;
    size_t size = 0;

    ///
    /// Big files come out of our archive memory-mapped, so wrap the
    /// mapping without a copy and unmap it in the destruction callback.
    ///
    // Pseudo-code, map the file if the archive has it.
    if (MapAsset(path, &data, &size))
      return Buffer::Create(data, size, nullptr, OnUnmapAsset);

    ///
    /// Small files are read into a scratch block we reuse, so hand the
    /// library its own (16-byte aligned) copy instead.
    ///
    // Pseudo-code, read the file into scratch memory (false if missing).
    if (ReadAssetIntoScratch(path, &data, &size))
      return Buffer::CreateFromCopy(data, size);

    return nullptr;
  }
};

MyFileSystem file_system;

void InitPlatform() {
  ///
  /// Install our file system before creating the Renderer. (Ownership
  /// stays with us, so 'file_system' must outlive the Renderer.)
  ///
  Platform::instance().set_file_system(&file_system);
}
```

## File Caching

The renderer reads the page's HTML file through `OpenFile()` on every load.

Files that the page references (such as stylesheets, scripts, and images) stay in a memory cache— later requests can use the cached copy without calling `OpenFile()`.

This reuse can happen when you load the same page again with `View::LoadURL()`, or when the page requests an asset after it finishes loading.

How long a file stays cached is not fixed— the library periodically frees files that no loaded page uses, so reuse across page loads is likely but not guaranteed.

Calling `View::Reload()` bypasses the memory cache and re-reads every file through the file system.

### Bypassing the Cache

To force the renderer to load a fresh copy of a file, append a query string to its URL:

```html
<link rel="stylesheet" href="file:///hud.css?v=2">
```

Changing the URL creates a new cache entry— your `FileSystem` still receives the path without the query string.

The change takes effect immediately when a script on the page updates the URL, or the next time the page loads if you update the markup before calling `View::LoadURL()`.

## Serving Library Resources

The renderer loads its internal resources through the file system under the directory configured by `Config::resource_path_prefix` (`resources/` by default)— see [Creating the Renderer](/docs/2.0/creating-the-renderer#content-common-options).

You provide the SDK's `resources/` folder in its entirety— the library fetches these files through the file system:

- `icudt67l.dat` — Unicode tables the renderer uses to lay out and format text.
- `cacert.pem` — Root certificates for authenticating HTTPS connections.
- `mediaControls.css`, `mediaControls.js`, and `mediaControlsLocalizedStrings.js` — Stylesheet, scripts, and localized UI labels for the built-in `<video>` player (in editions with media support).

For instructions on staging these files with an application, see [Linking to the Library](/docs/2.0/linking-to-the-library).

### Missing Resource Errors

If `icudt67l.dat` cannot be loaded, the library logs a `LogLevel::Fatal` message and the process exits at startup.

If `cacert.pem` cannot be loaded, the library logs an error and all HTTPS requests fail.
