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: "report.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("report.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.
FileNamerepairs ("who?.pdf"comes back as"who_.pdf");RelativePathrejects (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;
FileNamechecks again after the cut, soCONand friends never slip through. RelativePathrejects three ways out: rooted paths, drive or stream colons, and".."climbs. Any one of them rejects the whole value.FromBase64is 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 atCkSettings.MaxBase64InputLengthcharacters (about twelve megabytes of file). An empty array always means rejected.DataUriTypeis 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) andIsWithinSize(small enough), thenFromBase64for the bytes. The first three are cheap and never decode. IsSafeFileNamerejects whereFileNamerepairs. 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