strict-path 0.1.0-alpha.5

Prevent directory traversal with type-safe path restriction and safe symlinks
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# strict-path


[![Crates.io](https://img.shields.io/crates/v/strict-path.svg)](https://crates.io/crates/strict-path)
[![Documentation](https://docs.rs/strict-path/badge.svg)](https://docs.rs/strict-path)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](https://github.com/DK26/strict-path-rs#license)
[![CI](https://github.com/DK26/strict-path-rs/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/DK26/strict-path-rs/actions/workflows/ci.yml)
[![Security Audit](https://github.com/DK26/strict-path-rs/actions/workflows/audit.yml/badge.svg?branch=main)](https://github.com/DK26/strict-path-rs/actions/workflows/audit.yml)
[![Type-State Police](https://img.shields.io/badge/protected%20by-Type--State%20Police-blue.svg)](https://github.com/DK26/strict-path-rs)

πŸ“š **[Complete Guide & Examples]https://dk26.github.io/strict-path-rs/** | πŸ“– **[API Docs]https://docs.rs/strict-path**

**Prevent directory traversal with type-safe path restriction and safe symlinks.**

Quick start

```rust
use strict_path::{StrictPath, VirtualPath};

// Strict system path rooted at ./data
let sp = StrictPath::with_boundary("./data")?
    .strict_join("users/alice.txt")?;

// Virtual view rooted at ./public (displays as "/...")
let vp = VirtualPath::with_root("./public")?
    .virtual_join("assets/logo.png")?;
```

> *The Type-State Police have set up PathBoundary checkpoints*  
> *keeping your unruly paths in line, because your LLM is running wild*

## 🚨 **One Line of Code Away from Disaster**


```rust
// ❌ This single line can destroy your server
std::fs::write(user_input, data)?;  // user_input = "../../../etc/passwd"

// βœ… This single line makes it mathematically impossible  
StrictPath::with_boundary("uploads")?.strict_join(user_input)?.write_bytes(data)?;
```

**The Reality**: Every web server, LLM agent, and file processor faces the same vulnerability. One unvalidated path from user input, config files, or AI responses can grant attackers full filesystem access.

**The Solution**: Comprehensive path security with mathematical guarantees. No more hoping you "got it right."

## πŸ›‘οΈ **How We Solve The Entire Problem Class**


**strict-path isn't just validationβ€”it's a complete solution to path security:**

1. **πŸ”§ [`soft-canonicalize`]https://github.com/DK26/soft-canonicalize-rs foundation**: Heavily tested against 19+ globally known path-related CVEs
2. **🚫 Hacky string rejection**: Advanced pattern detection blocks encoding tricks and malformed inputs  
3. **πŸ“ Mathematical correctness**: Rust's type system provides compile-time proof of path boundaries
4. **πŸ‘οΈ Explicit operations**: Method names like `strict_join()` make security violations visible in code review
5. **πŸ€– LLM-aware design**: Built specifically for untrusted AI-generated paths and modern threat models
6. **πŸ”— Symlink resolution**: Safe handling of symbolic links with cycle detection and boundary enforcement
7. **⚑ Dual protection modes**: Choose **Strict** (validate & reject) or **Virtual** (clamp & contain) based on your use case
8. **πŸ—οΈ Battle-tested architecture**: Prototyped and refined across real-world production systems
9. **🎯 Zero-allocation interop**: Seamless integration with existing `std::path` ecosystems

### **Recently Addressed CVEs**

- **CVE-2025-8088** (WinRAR ADS): NTFS Alternate Data Stream traversal prevention
- **CVE-2022-21658** (TOCTOU): Race condition protection during path resolution  
- **CVE-2019-9855, CVE-2020-12279, CVE-2017-17793**: Windows 8.3 short name vulnerabilities

**Your security audit becomes**: *"We use strict-path for comprehensive path security."* βœ…

## ⚑ **Get Secure in 30 Seconds**


```toml
[dependencies]
strict-path = "0.1.0-alpha.5"
```

```rust
use strict_path::StrictPath;

// 1. Create a boundary (your security perimeter)
//    Use sugar for simple flows; switch to PathBoundary when you need reusable policy
let safe_root = StrictPath::with_boundary("uploads")?;

// 2. ANY external input becomes safe
let safe_path = safe_root.strict_join(dangerous_user_input)?;  // Attack = Error

// 3. Use normal file operations - guaranteed secure
safe_path.write_bytes(file_data)?;
let info = safe_path.metadata()?; // Inspect filesystem metadata when needed
safe_path.remove_file()?; // Remove when cleanup is required
safe_path.remove_file()?; // Remove when cleanup is required
```

**That's it.** No complex validation logic. No CVE research. No security expertise required.

## πŸ›‘οΈ **Security Features**


- **CVE-Aware Protection**: Built on 19+ real-world vulnerabilities - we've done the security research so you don't have to
- **Mathematical Guarantees**: Paths are canonicalized and boundary-checked - impossible to escape the restriction  
- **Type Safety**: Marker types prevent mixing different storage contexts at compile time
- **LLM-Ready**: Designed specifically for untrusted AI-generated paths and modern threat models
- **Platform Security**: Handles Windows 8.3 short names, symlinks, and other OS-specific attack vectors
- **Zero-Allocation Interop**: `.interop_path()` for seamless integration with existing `std::path` code
- **Misuse Resistant**: API design makes security violations visible in code review

## 🎯 **When to Use Each Type**


| Your Input Source                           | Use This       | Why                                                 |
| ------------------------------------------- | -------------- | --------------------------------------------------- |
| **HTTP requests, LLM output, config files** | `StrictPath`   | Reject attacks explicitly - perfect for validation  |
| **User uploads, archive extraction**        | `VirtualPath`  | Clamp hostile paths safely - perfect for sandboxing |
| **Your own hardcoded paths**                | `Path/PathBuf` | You control it, no validation needed                |

**Think of it this way:**
- `StrictPath` = **Security Filter** β€” validates and rejects unsafe paths
- `VirtualPath` = **Complete Sandbox** β€” clamps any input to stay safe

## πŸ›‘οΈ **Core Security Foundation**


At the heart of this crate is **`StrictPath`** - the fundamental security primitive that provides our ironclad guarantee: **every `StrictPath` is mathematically proven to be within its boundary**. 

Everything in this crate builds upon `StrictPath`:
- `PathBoundary` creates and validates `StrictPath` instances
- `VirtualPath` extends `StrictPath` with user-friendly virtual root semantics  
- `VirtualRoot` provides a root context for creating `VirtualPath` instances

**The core promise:** If you have a `StrictPath<Marker>`, it is impossible for it to reference anything outside its designated boundary. This isn't just validation - it's a type-level guarantee backed by cryptographic-grade path canonicalization.


**Core Security Principle: Secure Every External Path**

Any path from untrusted sources (HTTP, CLI, config, DB, LLMs, archives) must be validated into a boundary‑enforced type (`StrictPath` or `VirtualPath`) before I/O.

## 🎯 **Choose Your Weapon: When to Use What**


### 🌐 **VirtualPath** - User Sandboxes & Cloud Storage

*"Give users their own private universe"*

```rust
use strict_path::VirtualPath;

// Archive extraction - hostile names get clamped, not rejected
let extract_root = VirtualPath::with_root("./extracted")?;
for entry_name in malicious_zip_entries {
    let safe_path = extract_root.virtual_join(entry_name)?; // "../../../etc" β†’ "/etc"  
    safe_path.write_bytes(entry.data())?; // Always safe
}

// User cloud storage - users see friendly paths
let doc = VirtualPath::with_root(format!("users/{user_id}"))?
    .virtual_join("My Documents/report.pdf")?;
println!("Saved to: {}", doc.virtualpath_display()); // Shows "/My Documents/report.pdf"
```

### βš”οΈ **StrictPath** - LLM Agents & System Boundaries  

*"Validate everything, trust nothing"*

```rust
use strict_path::PathBoundary;

// LLM Agent file operations
let ai_workspace = PathBoundary::try_new("ai_sandbox")?;
let ai_request = llm.generate_path(); // Could be anything malicious
let safe_path = ai_workspace.strict_join(ai_request)?; // Attack β†’ Explicit Error
safe_path.write_string(&ai_generated_content)?;

// Limited system access with clear boundaries
struct ConfigFiles; 
let config_dir = PathBoundary::<ConfigFiles>::try_new("./config")?;
let user_config = config_dir.strict_join(user_selected_config)?; // Validated
```

### πŸ”“ **Path/PathBuf** - Controlled Access

*"When you control the source"*

```rust
use std::path::PathBuf;

// βœ… You control the input - no validation needed
let log_file = PathBuf::from(format!("logs/{}.log", timestamp));
let app_config = Path::new("config/app.toml"); // Hardcoded = safe

// ❌ NEVER with external input
let user_file = Path::new(user_input); // 🚨 SECURITY DISASTER
```

## πŸŽ–οΈ **The Golden Rule**


> **If you didn't create the path yourself, secure it first.**

| Input Source                              | Use This       | Why                        |
| ----------------------------------------- | -------------- | -------------------------- |
| **HTTP requests, CLI args, config files** | `StrictPath`   | Reject attacks explicitly  |
| **LLM/AI output, database records**       | `StrictPath`   | Validate before execution  |
| **Archive contents, user uploads**        | `VirtualPath`  | Clamp hostile paths safely |
| **Your own code, hardcoded paths**        | `Path/PathBuf` | You control it             |

## πŸš€ **Real-World Examples**


### LLM Agent File Manager

```rust
use strict_path::PathBoundary;

// Encode guarantees in signature: pass workspace boundary and untrusted request
async fn llm_file_operation(workspace: &PathBoundary, request: &LlmRequest) -> Result<String> {
    // LLM could suggest anything: "../../../etc/passwd", "C:/Windows/System32", etc.
    let safe_path = workspace.strict_join(&request.filename)?; // Attack = Error

    match request.operation.as_str() {
        "write" => safe_path.write_string(&request.content)?,
        "read" => return Ok(safe_path.read_to_string()?),
        _ => return Err("Invalid operation".into()),
    }
    Ok(format!("File {} processed safely", safe_path.strictpath_display()))
}
```

### Zip Extraction (Zip Slip Prevention)

```rust
use strict_path::VirtualPath;

// Encode guarantees in signature: construct a root once; pass untrusted entry names
fn extract_zip(zip_entries: impl IntoIterator<Item=(String, Vec<u8>)>) -> std::io::Result<()> {
    let extract_root = VirtualPath::with_root("./extracted")?;
    for (name, data) in zip_entries {
        // Hostile names like "../../../etc/passwd" get clamped to "/etc/passwd"
        let vpath = extract_root.virtual_join(&name)?;
        vpath.create_parent_dir_all()?;
        vpath.write_bytes(&data)?;
    }
    Ok(())
}
```

### Web File Server

```rust
use strict_path::PathBoundary;

struct StaticFiles;

async fn serve_static(static_dir: &PathBoundary<StaticFiles>, path: &str) -> Result<Response> {
    let safe_path = static_dir.strict_join(path)?; // "../../../" β†’ Error
    Ok(Response::new(safe_path.read_bytes()?))
}

// Function signature prevents bypass - no validation needed inside!
async fn serve_file(safe_path: &strict_path::StrictPath<StaticFiles>) -> Response {
    Response::new(safe_path.read_bytes().unwrap_or_default())
}
```

### Configuration Manager

```rust
use strict_path::PathBoundary;

struct UserConfigs;

fn load_user_config(config_dir: &PathBoundary<UserConfigs>, config_name: &str) -> Result<Config> {
    let config_file = config_dir.strict_join(config_name)?;
    Ok(serde_json::from_str(&config_file.read_to_string()?)?)
}
```

## ⚠️ **Security Scope**


**What this protects against (99% of attacks):**
- Path traversal (`../../../etc/passwd`)  
- Symlink escapes and directory bombs
- Archive extraction attacks (zip slip)
- Unicode/encoding bypass attempts
- Windows-specific attacks (8.3 names, UNC paths)
- Race conditions during path resolution

**What requires system-level privileges (rare):**
- **Hard links**: Multiple filesystem entries to same file data
- **Mount points**: Admin/root can redirect paths via filesystem mounts

**Bottom line**: If attackers have root/admin access, they've already won. This library stops the 99% of practical attacks that don't require special privileges.

## πŸ“‹ **Input Source Decision Matrix**


| Source                      | Typical Input                  | Use VirtualPath For                       | Use StrictPath For        | Notes                                                   |
| --------------------------- | ------------------------------ | ----------------------------------------- | ------------------------- | ------------------------------------------------------- |
| 🌐 **HTTP requests**         | URL path segments, file names  | Display/logging, safe virtual joins       | System-facing interop/I/O | Always clamp user paths via `VirtualPath::virtual_join` |
| 🌍 **Web forms**             | Form file fields, route params | User-facing display, UI navigation        | System-facing interop/I/O | Treat all form inputs as untrusted                      |
| βš™οΈ **Configuration files**   | Paths in config                | UI display and I/O within boundary        | System-facing interop/I/O | Validate each path before I/O                           |
| πŸ’Ύ **Database content**      | Stored file paths              | Rendering paths in UI dashboards          | System-facing interop/I/O | Storage does not imply safety; validate on use          |
| πŸ“‚ **CLI arguments**         | Command-line path args         | Pretty printing, I/O within boundary      | System-facing interop/I/O | Validate args before touching filesystem                |
| πŸ”Œ **External APIs**         | Webhooks, 3rd-party payloads   | Present sanitized paths to logs           | System-facing interop/I/O | Never trust external systems                            |
| πŸ€– **LLM/AI output**         | Generated file names/paths     | Display suggestions, I/O within boundary  | System-facing interop/I/O | LLM output is untrusted by default                      |
| πŸ“¨ **Inter-service msgs**    | Queue/event payloads           | Observability output, I/O within boundary | System-facing interop/I/O | Validate on the consumer side                           |
| πŸ“± **Apps (desktop/mobile)** | Drag-and-drop, file pickers    | Show picked paths in UI                   | System-facing interop/I/O | Validate selected paths before I/O                      |
| πŸ“¦ **Archive contents**      | Entry names from ZIP/TAR       | Progress UI, virtual joins                | System-facing interop/I/O | Validate each entry to block zip-slip                   |
| πŸ”§ **File format internals** | Embedded path strings          | Diagnostics, I/O within boundary          | System-facing interop/I/O | Never dereference without validation                    |

Note: This is not β€œStrictPath vs VirtualPath.” `VirtualPath` conceptually extends `StrictPath` with a virtual-root view and restricted, path boundary-aware operations. Both support I/O and interop; choose based on whether you need virtual, user-facing path semantics or raw system-facing semantics.

**Think of it this way:**
- `StrictPath` = **Security Filter** β€” validates and rejects unsafe paths
- `VirtualPath` = **Complete Sandbox** β€” clamps any input to stay safe

**Unified Signatures (When Appropriate)**: Prefer marker-specific `&StrictPath<Marker>` for stronger guarantees. Use a generic `&StrictPath<_>` only when the function is intentionally shared across contexts; call with `vpath.as_unvirtual()` when starting from a `VirtualPath`.

```rust
use strict_path::{StrictPath, VirtualPath};

fn process_file<M>(path: &strict_path::StrictPath<M>) -> std::io::Result<Vec<u8>> {
    path.read_bytes()
}

// Call with either type
let spath: StrictPath = StrictPath::with_boundary("directory")?.strict_join("file.txt")?;
process_file(&spath)?;

let vpath: VirtualPath = VirtualPath::with_root("directory")?.virtual_join("file.txt")?;
process_file(vpath.as_unvirtual())?;
```

## πŸ” **Advanced: Type-Safe Context Separation**


Use markers to prevent mixing different storage contexts at compile time:

```rust
use strict_path::{PathBoundary, StrictPath, VirtualRoot, VirtualPath};

struct WebAssets;    // CSS, JS, images  
struct UserFiles;    // Uploaded documents

// Functions enforce context via type system
fn serve_asset(path: &StrictPath<WebAssets>) -> Response { /* ... */ }
fn process_upload(path: &StrictPath<UserFiles>) -> Result<()> { /* ... */ }

// Create context-specific boundaries
let assets_root: VirtualRoot<WebAssets> = VirtualRoot::try_new("public")?;
let uploads_root: VirtualRoot<UserFiles> = VirtualRoot::try_new("uploads")?;

let css: VirtualPath<WebAssets> = assets_root.virtual_join("app.css")?;
let doc: VirtualPath<UserFiles> = uploads_root.virtual_join("report.pdf")?;

// Type system prevents context mixing
serve_asset(css.as_unvirtual());         // Correct context
// serve_asset(doc.as_unvirtual());      // Compile error!
```

**Your IDE and compiler become security guards.**

**App Configuration with `app_path`:**
```rust
// ❌ Vulnerable - app dirs + user paths
use app_path::AppPath;
let app_dir = AppPath::new("MyApp").get_app_dir();
let config_file = app_dir.join(user_config_name); // 🚨 Potential escape
fs::write(config_file, settings)?;

// βœ… Protected - bounded app directories  
use strict_path::PathBoundary;
let boundary = PathBoundary::try_new_create(AppPath::new("MyApp").get_app_dir())?;
let safe_config = boundary.strict_join(user_config_name)?; // βœ… Validated
safe_config.write_string(&settings)?;
```

## ⚠️ Anti-Patterns (Tell‑offs and Fixes)


### DON'T Mix Interop with Display


```rust
use strict_path::PathBoundary;
let boundary = PathBoundary::try_new("uploads")?;

// ❌ ANTI-PATTERN: Wrong method for display
println!("Path: {}", boundary.interop_path().to_string_lossy());

// βœ… CORRECT: Use proper display methods
println!("Path: {}", boundary.strictpath_display());

// For virtual flows, prefer `VirtualPath` and borrow strict view when needed:
use strict_path::VirtualPath;
let vpath = VirtualPath::with_root("uploads")?.virtual_join("file.txt")?;
println!("Virtual: {}", vpath.virtualpath_display());
println!("System: {}", vpath.as_unvirtual().strictpath_display());
```

**Why this matters:**
- `interop_path()` is designed for external API interop (`AsRef<Path>`)
- `*_display()` methods are designed for human-readable output
- Mixing concerns makes code harder to understand and maintain

### Web Server File Serving

```rust
struct StaticFiles; // Marker for static assets

async fn serve_static_file(safe_path: &StrictPath<StaticFiles>) -> Result<Response> {
    // Function signature enforces safety - no validation needed inside!
    Ok(Response::new(safe_path.read_bytes()?))
}

// Caller handles validation once:
let static_files_dir = PathBoundary::<StaticFiles>::try_new("./static")?;
let safe_path = static_files_dir.strict_join(&user_requested_path)?;
serve_static_file(&safe_path).await?;
```

### Archive Extraction (Zip Slip Prevention)

```rust
let extract_root = VirtualPath::with_root("./extracted")?;
for (name, data) in zip_entries {
    let vpath = extract_root.virtual_join(&name)?;  // Neutralizes zip slip (clamps hostile)
    vpath.create_parent_dir_all()?;
    vpath.write_bytes(&data)?;
}
```

### Cloud Storage API  


```rust
// User chooses any path - always safe
let file_path = VirtualPath::with_root(format!("/cloud/user_{id}"))?
    .virtual_join(&user_requested_path)?;
file_path.write_bytes(upload_data)?;
```

### Configuration Files

```rust
use strict_path::PathBoundary;

// Encode guarantees via the signature: pass the boundary and an untrusted name
fn load_config(config_dir: &PathBoundary, name: &str) -> Result<String> {
    config_dir.strict_join(name)?.read_to_string()
}
```




- Constructing boundaries/roots inside helpers
  - Helpers shouldn’t decide policy. Take a `&PathBoundary`/`&VirtualRoot` and a name, or accept a `&StrictPath`/`&VirtualPath`.
- Wrapping secure types in std paths
  - Don’t wrap `interop_path()` in `Path::new`/`PathBuf::from`; pass `interop_path()` directly to `AsRef<Path>` APIs.




### LLM/AI File Operations

```rust
// AI suggests file operations - always validated
let ai_workspace = PathBoundary::try_new("ai_workspace")?;
let ai_suggested_path = llm_generate_filename(); // Could be anything!
let safe_ai_path = ai_workspace.strict_join(ai_suggested_path)?; // Guaranteed safe
safe_ai_path.write_string(&ai_generated_content)?;
```



## πŸ“š **API Quick Reference**


| Feature            | `Path`/`PathBuf`                            | `StrictPath`                        | `VirtualPath`                                      |
| ------------------ | ------------------------------------------- | ----------------------------------- | -------------------------------------------------- |
| **Security**       | None πŸ’₯                                      | Validates & rejects βœ…               | Clamps any input βœ…                                 |
| **Join safety**    | Unsafe (can escape)                         | Boundary-checked                    | Boundary-clamped                                   |
| **Example attack** | `"../../../etc/passwd"` β†’ **System breach** | `"../../../etc/passwd"` β†’ **Error** | `"../../../etc/passwd"` β†’ **`/etc/passwd`** (safe) |
| **Best for**       | Known-safe paths                            | System boundaries                   | User interfaces                                    |

```rust
// StrictPath - validate and reject
let path = StrictPath::with_boundary("uploads")?.strict_join("file.txt")?; // Error if unsafe

// VirtualPath - clamp any input safely  
let vpath = VirtualPath::with_root("userspace")?.virtual_join("any/path/here")?; // Always works

// Both support the same I/O operations
path.write_bytes(data)?;
vpath.read_to_string()?;
```

## πŸ“š **Documentation & Resources**


- **πŸ“– [Complete API Reference]https://docs.rs/strict-path** - Comprehensive API documentation
- **πŸ“š [User Guide & Examples]https://dk26.github.io/strict-path-rs/** - In-depth tutorials and patterns  
- **πŸ”§ [API_REFERENCE.md]API_REFERENCE.md** - Quick reference for all methods
- **πŸ› οΈ [`soft-canonicalize`]https://github.com/DK26/soft-canonicalize-rs** - The underlying path resolution engine

## πŸ”Œ **Integrations**


- **πŸ—‚οΈ OS Directories** (`dirs` feature): `PathBoundary::try_new_os_config()`, `try_new_os_downloads()`, etc.
- **πŸ“„ Serde** (`serde` feature): Safe serialization/deserialization of path types
- **🌐 Axum**: Custom extractors for web servers (see `demos/` for examples)

## πŸ“„ **License**


MIT OR Apache-2.0