Skip to content

CkFile

Job: make file names and paths from the outside world safe to use — clean the name, read the extension, guard the path, and decode base64 uploads.

Files reach an application as text: a name typed into a form, a path stored in a database, an upload sent as base64. None of that text can be trusted as-is. CkFile splits the work in two: FileName is a sanitizer — it repairs a name until it is safe to save; RelativePath is a gate — a suspicious path is rejected outright, never repaired. The standard rejection values apply: a string method answers "" (empty), FromBase64 answers an empty byte array, and HasExtension answers false. Nothing throws unless you ask (How it works).

Quick look

using SafeCoder.CommonKit;

CkFile.FileName("Report: Q3?.pdf");                     // -> "Report_ Q3_.pdf"
CkFile.Extension("archive.tar.gz");                     // -> "gz"
CkFile.RelativePath("a/../secrets.txt");                // -> ""  (a climb — rejected)
CkFile.FromBase64("AQID");                              // -> bytes { 1, 2, 3 }
CkFile.DataUriType("data:image/PNG;base64,iVBORw0KGgo"); // -> "image/png"
CkFile.IsRelativePath("a/../secrets.txt");              // -> false
CkFile.InMediaTypes(upload, "image/*");                 // -> true for any image
CkFile.IsWithinSize(upload, 5 * 1024 * 1024);           // -> true when 5 MB or less
CkFile.Combine(uploads, "../secrets.txt");   // -> ""  (the join escapes the root)
CkFile.SizeToString(1536);                   // -> "1.5 KB"
CkFile.Explain("a/../b");                    // -> the gate's reason

Methods

FileName

A name that is safe to save.

string FileName(
    string? input = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw value.
fallback string? null Value to use when nothing usable survives. It goes through the same sanitizing and is never returned as typed; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) survives the sanitizing The safe name: "Report_ Q3_.pdf" The safe name: "Report_ Q3_.pdf"
Rejected: missing, over CkSettings.MaxPathInputLength (1,024) characters, or nothing survives the sanitizing Empty string Throws CkFileException — carries Input, Expected and Reason

A sanitizer, not a validator — use RelativePath when the value claims to be a path.

Turns whatever arrives into a name any operating system will accept. The characters Windows forbids (< > : " / \ | ? *) become underscores, trailing dots and spaces go, and a name longer than 255 characters (CkSettings.MaxFileNameLength) is cut with its extension kept — never through the middle of a character: an emoji is two chars, and a cut between them would leave half of one, which no filesystem takes. Windows also reserves some device names — CON, PRN, NUL, COM1 and friends — that hang or misbehave when used as a file name, even with an extension; those are defused with a leading underscore. This is a rename, not a rejection: the goal is a name you can write to disk.

CkFile.FileName("Report: Q3?.pdf");     // -> "Report_ Q3_.pdf"
CkFile.FileName("CON.txt");             // -> "_CON.txt"  (reserved name defused)
CkFile.FileName("invoice/2026.pdf");    // -> "invoice_2026.pdf"  (a slash is not a name)
CkFile.FileName("...");                 // -> ""  (nothing survives — rejected)

Extension

What kind of file the name claims.

string Extension(
    string? input = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw file name or path.
fallback string? null A file name to use when the value has no usable extension. It goes through the same reading and is never returned as typed; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering "", when neither the value nor the fallback has a usable extension.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) has a usable extension The extension, lower-case and without the dot: "pdf" The extension, lower-case and without the dot: "pdf"
Rejected: missing, oversized, no dot, a dotfile, or an extension that is not letters and digits Empty string Throws CkFileException — carries Input, Expected and Reason

With throwIfInvalid: true a dotfile throws too — no extension counts as a rejection here.

The extension of a file name or path — lower-case, without the dot, letters and digits only. A path answers its last segment's extension; only the last extension counts. A dotfile (a name starting with a dot, like .gitignore) and a name without a dot have no extension and answer "".

CkFile.Extension("Report.PDF");                // -> "pdf"
CkFile.Extension("archive.tar.gz");            // -> "gz"  (only the last)
CkFile.Extension("exports/2026/report.pdf");   // -> "pdf"
CkFile.Extension(".gitignore");                // -> ""  (a dotfile has no extension)

HasExtension

The allow-list check.

bool HasExtension(
    string? input = null,
    params IEnumerable<string?> allowed
)
Parameter Type Default What it does
input string? null The raw file name or path.
allowed params IEnumerable<string?> — The extensions to accept, compared without case or dots; null, empty and blank entries are skipped.

Returns

When You get
The name's extension matches an entry on the list true
No extension, an empty list, or no match false — and it never throws

No throwIfInvalid here (a params list must come last) — call Extension with it first when you need the error.

True when the name's extension is on your list. Entries are compared without case or dots, so "pdf", ".PDF" and "Pdf" all mean the same thing. A name without an extension, or an empty list, answers false.

CkFile.HasExtension(
    input: "Report.PDF",
    allowed: ["pdf", "docx"]
);   // -> true

CkFile.HasExtension(
    input: "photo.exe",
    allowed: ["pdf", "docx"]
);   // -> false

CkFile.HasExtension(
    input: "scan.pdf",
    allowed: [".PDF"]
);   // -> true  (dots and casing in entries are forgiven)

RelativePath

The path-traversal gate.

string RelativePath(
    string? input = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw path.
fallback string? null A path to use when the value cannot be used. It goes through the same validation and is never returned as typed; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) stays inside its folder The tidied path, forward slashes only: "exports/2026/report.pdf" The tidied path, forward slashes only: "exports/2026/report.pdf"
Rejected: missing, oversized, rooted, a drive or stream colon, a .. climb, a forbidden character, a segment starting with a space or ending in a dot or space, or a reserved Windows name in a segment Empty string Throws CkFileException — carries Input, Expected and Reason

Rejection over repair — a "fixed" hostile path is still hostile intent.

Validates a relative path you stored or received — "exports/2026/report.pdf" — as one that stays inside its folder. Path traversal is the classic attack where a stored path uses ".." (which means "the folder above") to climb out of the folder you meant and reach files like system secrets. So this method rejects instead of repairing: a rooted path (one starting with /), a drive or stream colon (C:), a ".." climb, a forbidden character, a segment starting with a space or ending in a dot or space, or a reserved Windows name in any segment rejects the whole value. What passes is tidied — backslashes become forward slashes, "." segments and doubled slashes collapse. Combine the result with your own base folder (Path.Combine(baseDir, relative)); never accept an absolute path from data.

CkFile.RelativePath("exports\\2026\\report.pdf");   // -> "exports/2026/report.pdf"
CkFile.RelativePath("./a/./b.txt");                 // -> "a/b.txt"
CkFile.RelativePath("a/../secrets.txt");            // -> ""  (a climb — rejected)
CkFile.RelativePath("/etc/passwd");                 // -> ""  (rooted — rejected)
CkFile.RelativePath("C:\\temp\\x.txt");             // -> ""  (drive colon — rejected)

FromBase64

Text back into bytes.

byte[] FromBase64(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The base64 text, or a data URI holding one.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering an empty array, when the value cannot be decoded.

Returns

When Default With throwIfInvalid: true
The text decodes The bytes: { 1, 2, 3 } The bytes: { 1, 2, 3 }
Rejected: missing, over CkSettings.MaxBase64InputLength (16,777,216) characters, a data URI without a base64 payload, an empty payload, or not valid base64 Empty array — never null Throws CkFileException — carries Input, Expected and Reason

The declared media type of a data URI reads separately with DataUriType.

Decodes base64 text into a byte[]. Base64 is the standard way of writing bytes as plain text — it is how file uploads travel inside JSON. This method reads plain base64, the URL-safe alphabet (- and _ instead of + and /), unpadded text (missing = signs at the end), and a full data URI like "data:image/png;base64,…" (the payload after the comma). Whitespace inside is ignored. The answer is never null: an empty array means the value was rejected — so "bytes came back" means it really decoded. What counts as base64 is what IsBase64 counts: text whose last character carries bits no byte can hold — "no", "US", a date — is refused, not read as the bytes a lenient decoder would invent for it.

CkFile.FromBase64("AQID");                          // -> bytes { 1, 2, 3 }
CkFile.FromBase64("data:image/png;base64,AQID");    // -> bytes { 1, 2, 3 }
CkFile.FromBase64("AQ");                            // -> bytes { 1 }  (unpadded is fine)
CkFile.FromBase64("not base64!");                   // -> empty array — rejected
CkFile.FromBase64("2026-08-31");                    // -> empty array — not base64, whatever a lenient decoder makes of it

DataUriType

What a data URI says it is.

string DataUriType(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The data URI.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering "", when the value cannot be used.

Returns

When Default With throwIfInvalid: true
The data URI declares a well-formed media type The lowercase type: "image/png" The lowercase type: "image/png"
Rejected: missing, not a data URI, or no well-formed media type in it Empty string Throws CkFileException — carries Input, Expected and Reason

Shape-checked, not verified — the type is what the sender claimed, not what the bytes are.

The media type a data URI declares — the label that says what kind of file it is, like "image/png" — lowercased. The type is shape-checked: one name, one slash, letters, digits and .+- only, so a stored value cannot smuggle markup into a Content-Type header. Empty means the value is missing, is not a data URI, or declares no well-formed type. Note the declared type is only a claim — it tells you what the sender said, not what the bytes really are.

CkFile.DataUriType("data:image/PNG;base64,iVBORw0KGgo");   // -> "image/png"
CkFile.DataUriType("data:;base64,AAAA");                   // -> ""  (no type declared)
CkFile.DataUriType("just text");                           // -> ""

IsSafeFileName

Already a name FileName would leave alone?

bool IsSafeFileName(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw file name.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering false, when the value is not a safe name as written.

Returns

When Default With throwIfInvalid: true
The cleaned value equals what FileName would make of it true true
FileName would have to change it — a forbidden character, a reserved name, a trailing dot, over-long false Throws — the reason says what it would become
An invisible character in the name — a zero-width space, a soft hyphen, a bidi mark, a control character false Throws — "an invisible character in the name: FileName would remove it"
Rejected: nothing usable at all false Throws CkFileException — carries Input, Expected and Reason

A shape check for intake policies that reject rather than repair. Surrounding whitespace and markup are cleaned away first, as every shape check does; then the name must be exactly what FileName would produce. "who?.pdf" is false, and with the flag the reason reads FileName would change it to "who_.pdf". An invisible character is the one thing the cleaning is not allowed to excuse: "re​port.pdf" cleans to "report.pdf" and looks like it, but it is a different name and a different file, so it is false — the name as written is what is judged.

CkFile.IsSafeFileName("Report Q3.pdf");     // -> true
CkFile.IsSafeFileName("who?.pdf");           // -> false
CkFile.IsSafeFileName("con.txt");            // -> false  (a reserved name)
CkFile.IsSafeFileName("re​port.pdf");   // -> false  (a zero-width space inside)

IsRelativePath

Does it pass the traversal gate?

bool IsRelativePath(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw path.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering false, when the value is not a safe relative path.

Returns

When Default With throwIfInvalid: true
Relative, no drive or stream colon, no .. climb, no forbidden character, no reserved segment true true
Rejected: any of those false Throws CkFileException — carries Input, Expected and Reason

The yes-or-no form of RelativePath, for validation code; call the gate itself when you want the normalized path back. A bare file name is a relative path.

CkFile.IsRelativePath("exports/2026/report.pdf");   // -> true
CkFile.IsRelativePath("a/../secrets.txt");           // -> false  (a climb)
CkFile.IsRelativePath("/etc/passwd");                // -> false  (rooted)

IsBase64

Does the text decode?

bool IsBase64(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The base64 text or data URI.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering false, when the value is not base64.

Returns

When Default With throwIfInvalid: true
Plain, URL-safe or unpadded base64, or a data URI with a base64 payload, decoding to at least one byte true true
Rejected: not base64, an empty payload, a data URI without base64, over the cap false Throws CkFileException — carries Input, Expected and Reason

The yes-or-no form of FromBase64, judged without allocating the bytes: the same shaping — whitespace ignored, the URL-safe alphabet mapped, padding restored — then the framework's own validity check. An empty payload is not a file, so it is false.

CkFile.IsBase64("SGVsbG8");                  // -> true   (unpadded is fine)
CkFile.IsBase64("data:text/plain,hello");    // -> false  (no base64 payload)
CkFile.IsBase64("AAAAA");                    // -> false  (a length no base64 has)

IsDataUri

A data URI with a file in it?

bool IsDataUri(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw value.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering false, when the value is not such a data URI.

Returns

When Default With throwIfInvalid: true
The data: prefix, a well-shaped media type, ;base64, and a payload that decodes to at least one byte true true
Rejected: not a data URI, no type, no base64, an empty or invalid payload false Throws CkFileException — carries Input, Expected and Reason — the reason says which

Everything an upload field should carry, in one check. A plain-text data URI (data:text/plain,hello) is false, as FromBase64 treats it. The type is shape-checked, not verified against the bytes; see InMediaTypes to allow-list it.

CkFile.IsDataUri("data:image/png;base64,iVBORw0KGgo");   // -> true
CkFile.IsDataUri("data:;base64,AQID");                   // -> false  (no type)
CkFile.IsDataUri("data:image/png;base64,");              // -> false  (an empty payload)

InMediaTypes

One of the types we accept?

bool InMediaTypes(
    string? input = null,
    params IEnumerable<string?> types
)
Parameter Type Default What it does
input string? null The data URI.
types params IEnumerable<string?> — The media types to accept, type/subtype, type/* for a family, or */* for anything that has a type. Null, empty and blank entries are skipped.

Returns

When You get
The value is a data URI whose declared type matches an entry true
It is not a data URI, declares no well-formed type, no entry matches, or the list is empty false — an answer, not a failure; it never throws

The upload allow-list check, "images only". The declared type is read the way DataUriType reads it, lowercase, and entries are compared the same way. This judges the declared type alone — a well-shaped lie passes — so pair it with IsDataUri for the payload and check the bytes when it matters. A params list must be last, so there is no throwIfInvalid; call DataUriType with it first.

CkFile.InMediaTypes(upload, "image/png", "image/jpeg");   // -> true for a PNG
CkFile.InMediaTypes(upload, "image/*");                   // -> true for any image type
CkFile.InMediaTypes("data:application/pdf;base64,…", "image/*");   // -> false

IsWithinSize

Small enough to accept?

bool IsWithinSize(
    string? input = null,
    long maxBytes = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The base64 text or data URI.
maxBytes long 0 The largest decoded size allowed, in bytes. Below 0 acts as 0.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering false, when the value is not base64.

Returns

When Default With throwIfInvalid: true
Valid base64 that decodes to no more than maxBytes true true
Valid base64 that decodes to more false false
Rejected: not base64 false Throws CkFileException — carries Input, Expected and Reason

The "uploads up to 5 MB" rule, worked out from the text alone: base64 length maps to byte count exactly once padding is accounted for, so nothing is decoded and the check is cheap enough to run before anything is. Too big is an answer, not an error.

CkFile.IsWithinSize(upload, 5 * 1024 * 1024);   // -> true when the file is 5 MB or less
CkFile.IsWithinSize("AQID", 2);                  // -> false  (three bytes)

Combine

A full path that cannot escape its folder.

string Combine(
    string? root = null,
    string? relative = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
root string? null The folder everything must stay inside.
relative string? null The stored or received relative path.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering "", when the root is missing, the relative path fails the gate, or the join escapes the root.

Returns

When Default With throwIfInvalid: true
The relative path passes RelativePath's gate and the join stays inside the root The full path, in the operating system's separators The path
Rejected: no root, a path the gate refuses, a join that escapes "" Throws CkFileException — carries Input, Expected and Reason

The traversal-safe form of Path.Combine, which happily follows "../" out of the folder. The gate runs first, then the joined path is resolved and checked to start inside the root, so no spelling escapes.

CkFile.Combine(uploads, "2026/report.pdf");   // -> "/srv/uploads/2026/report.pdf"
CkFile.Combine(uploads, "../secrets.txt");     // -> ""

SizeToString

1.5 MB, as people read it.

string SizeToString(
    object? bytes = null,
    int decimals = 1,
    string fallback = "",
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
bytes object? null The byte count: a number of any type, or text.
decimals int 1 The most decimal places to show, 0 to 3.
fallback string "" What comes back when the count cannot be read.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering the fallback, when the count is not a whole, non-negative number.

Returns

When Default With throwIfInvalid: true
A whole, non-negative count "512 B", "1.5 KB", "1 MB" — steps of 1024, halves rounded up, trailing zeros dropped The size
Rejected: negative, a fraction, junk The fallback Throws CkFileException — carries Input, Expected and Reason

The convention operating systems and browsers show.

CkFile.SizeToString(1536);       // -> "1.5 KB"
CkFile.SizeToString(5242880);    // -> "5 MB"

MediaType

The Content-Type a file name implies.

string MediaType(
    string? input = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw file name or path.
fallback string? null The media type to answer for an unknown extension; "application/octet-stream" is the usual choice. Held to the bare type/subtype shape: parameters reject it rather than being dropped.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering "", when no media type can be answered.

Returns

When Default With throwIfInvalid: true
The extension is in the table The media type, lowercase: "application/pdf" The type
It is not, and a well-formed fallback is given The fallback The fallback
Rejected: no extension, an unknown one with no fallback, a fallback that is not a media type "" Throws CkFileException — carries Input, Expected and Reason

A fixed table of the common document, image, audio, video, archive, font and web formats, read the way Extension reads the extension.

CkFile.MediaType("Report.PDF");                                          // -> "application/pdf"
CkFile.MediaType("data.xyz", fallback: "application/octet-stream");      // -> "application/octet-stream"

ToDataUri

Bytes as a data URI.

string ToDataUri(
    byte[]? bytes = null,
    string? type = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
bytes byte[]? null The file bytes.
type string? null The media type, e.g. "image/png", bare — no parameters — see MediaType.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkFileException that says why, instead of answering "", when there are no bytes or the type is not well-formed.

Returns

When Default With throwIfInvalid: true
Bytes and a well-formed type "data:image/png;base64,…" The URI
Rejected: no bytes, a malformed type "" Throws CkFileException — carries Input, Expected and Reason

The reverse of FromBase64, for embedding a file in JSON, HTML or a mail.

CkFile.ToDataUri(bytes, CkFile.MediaType("photo.png"));   // -> "data:image/png;base64,…"

Explain

Why not — in words, without an exception.

string Explain(
    string? input = null,
    string? what = nameof(RelativePath)
)
Parameter Type Default What it does
input string? null The raw value.
what string? nameof(RelativePath) The reader to judge by: FileName, Extension, RelativePath, FromBase64 or DataUriType, case-insensitive.

Returns

When You get
The value passes the reader ""
It does not The reason, the same words the exception would carry — an answer, not a failure; it never throws

The form-friendly twin of throwIfInvalid: the message to show beside the field, without a try and catch. Every class with a reader has one, on the reader's own rules, so the reason is exactly what the reader would say. This class has several readers, so what names the one to judge by; a name that is no reader answers a note saying so.

CkFile.Explain("a/../secrets.txt");                        // -> the gate's reason
CkFile.Explain("AAAAA", nameof(CkFile.FromBase64));        // -> "not valid base64"

The fallback

FileName, Extension and RelativePath take a fallback — a spare value used when the input is rejected. The fallback goes through the exact same processing; a bad fallback answers "" too. FromBase64, DataUriType, HasExtension and the validators take no fallback — the validators' answer is false.

CkFile.FileName(
    input: upload.ClientFileName,
    fallback: "upload.bin"
);   // -> the cleaned name, or "upload.bin" when nothing usable survives

Good to know

  • Sanitizer vs gate. FileName repairs ("who?.pdf" comes back as "who_.pdf"); RelativePath rejects (a hostile path "fixed" is still hostile intent). Pick by what the value claims to be: a name, or a path.
  • Reserved names stay defused, even after truncation. Cutting an over-long name can expose a reserved name the first check never saw; FileName checks again after the cut, so CON and friends never slip through.
  • RelativePath rejects three ways out: rooted paths, drive or stream colons, and ".." climbs. Any one of them rejects the whole value.
  • FromBase64 is generous about the text, strict about the verdict. Plain, URL-safe or unpadded base64 and data URIs all decode; whitespace is ignored; input is capped at CkSettings.MaxBase64InputLength characters (about twelve megabytes of file). An empty array always means rejected.
  • DataUriType is shape-checked, not verified. A malformed type answers "", but a well-shaped lie ("image/png" on a ZIP file) passes — check the bytes when it matters.
  • The upload trio. For a data URI upload, run IsDataUri (well-formed, with a payload), InMediaTypes (a type you accept) and IsWithinSize (small enough), then FromBase64 for the bytes. The first three are cheap and never decode.
  • IsSafeFileName rejects where FileName repairs. Same rules, opposite policy: use the validator when a client must send a clean name, the sanitizer when you will clean it yourself.
  • Non-empty in, empty out means rejected. The standard CommonKit signal for the string methods — check for "" where it matters. To pull a file name out of a payload first, see CkDictionary.

If you want errors instead

Pass throwIfInvalid: true and a value that cannot be used raises a CkFileException (from SafeCoder.CommonKit.Exceptions) instead of answering empty. The exception carries the Input, what was Expected and a short Reason, flattened into a one-line message. Two exceptions to the rule: HasExtension and InMediaTypes take no throwIfInvalid (their allow-list must be the last parameter) — call Extension or DataUriType with it first when you need the error.

CkFile.RelativePath(
    input: "a/../secrets.txt",
    throwIfInvalid: true
);   // throws CkFileException — the message says why