This commit is contained in:
jarek committed 2026-06-15 14:56:51 +02:00
1 parent d465ecfe96
commit 83c3a5ea09
85 files changed
+10118 -1046

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
export type ChangelogToken =
| { kind: 'text'; value: string }
| { kind: 'issue'; num: number }
| { kind: 'pr'; num: number }
| { kind: 'user'; name: string };
const PATTERN = /PR#(\d+)|#(\d+)|@([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38}))/g;
export function parseChangelogTokens(text: string): ChangelogToken[] {
const tokens: ChangelogToken[] = [];
let lastIndex = 0;
for (const match of text.matchAll(PATTERN)) {
const start = match.index ?? 0;
if (start > lastIndex) {
tokens.push({ kind: 'text', value: text.slice(lastIndex, start) });
}
if (match[1]) {
tokens.push({ kind: 'pr', num: Number(match[1]) });
} else if (match[2]) {
tokens.push({ kind: 'issue', num: Number(match[2]) });
} else if (match[3]) {
tokens.push({ kind: 'user', name: match[3] });
}
lastIndex = start + match[0].length;
}
if (lastIndex < text.length) {
tokens.push({ kind: 'text', value: text.slice(lastIndex) });
}
return tokens;
}
export const GITHUB_REPO = 'Finsys/dockhand';
export function tokenHref(token: ChangelogToken): string | null {
switch (token.kind) {
case 'issue':
return `https://github.com/${GITHUB_REPO}/issues/${token.num}`;
case 'pr':
return `https://github.com/${GITHUB_REPO}/pull/${token.num}`;
case 'user':
return `https://github.com/${token.name}`;
default:
return null;
}
}
+57
View File
@@ -0,0 +1,57 @@
/**
* Container resource-string parsers, shared between the create modal, the
* edit modal, and the in-place update Apply buttons in ContainerSettingsTab.
*
* Two of these previously lived in CreateContainerModal AND EditContainerModal
* as identical copies — moved here to prevent the drift that started when one
* gained `tb` support and the other didn't.
*/
/**
* Parse a memory string like "512m", "1g", "2.5gb" into bytes.
* Bare numbers are treated as bytes. Returns undefined for empty/garbage input.
*
* Units: k/kb, m/mb, g/gb, t/tb — base 1024 (binary), matching Docker's CLI.
*/
export function parseMemory(value: string | number | null | undefined): number | undefined {
if (value === null || value === undefined) return undefined;
if (typeof value === 'number') return value > 0 ? Math.floor(value) : undefined;
const trimmed = value.trim();
if (!trimmed) return undefined;
const match = trimmed.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*([kmgt]?b?)?$/);
if (!match) return undefined;
const num = parseFloat(match[1]);
const unit = match[2] || '';
switch (unit) {
case 'k': case 'kb': return Math.floor(num * 1024);
case 'm': case 'mb': return Math.floor(num * 1024 * 1024);
case 'g': case 'gb': return Math.floor(num * 1024 * 1024 * 1024);
case 't': case 'tb': return Math.floor(num * 1024 * 1024 * 1024 * 1024);
default: return Math.floor(num);
}
}
/**
* Parse a CPU-limit string like "0.5", "1.5", "2" into NanoCpus (1e9 = 1 CPU).
* Returns undefined for empty/garbage input.
*/
export function parseNanoCpus(value: string | number | null | undefined): number | undefined {
if (value === null || value === undefined) return undefined;
if (typeof value === 'number') return value > 0 ? Math.floor(value) : undefined;
const trimmed = value.trim();
if (!trimmed) return undefined;
const num = parseFloat(trimmed);
if (isNaN(num) || num <= 0) return undefined;
return Math.floor(num * 1e9);
}
/** Parse a bare positive integer string. Returns undefined for empty/garbage. */
export function parsePositiveInt(value: string | number | null | undefined): number | undefined {
if (value === null || value === undefined) return undefined;
if (typeof value === 'number') return value > 0 ? Math.floor(value) : undefined;
const trimmed = value.trim();
if (!trimmed) return undefined;
const num = parseInt(trimmed, 10);
if (isNaN(num) || num <= 0) return undefined;
return num;
}
+51
View File
@@ -0,0 +1,51 @@
/**
* Environment-name validation (#1179).
*
* Environment names end up in filesystem paths (e.g. $DATA_DIR/stacks/<name>/...
* in stacks.ts:344) and occasionally in shell-formatted strings, so the name
* has to be quote-safe and glob-free.
*
* The rule is permissive on purpose — strict alphanumeric would orphan
* existing real-world env names like "rambo (ARM)" or "docker-websites".
* Anything currently in the wild passes; only NEW characters that actively
* cause breakage are rejected.
*/
// Allowed characters: ASCII letters/digits + _ . - ( ) space + @
// First char: any allowed except space (no leading whitespace).
// Last char: any allowed except space and dot (no trailing whitespace,
// no trailing dot to avoid Windows-style "hidden" trailing-dot issues).
// Length: 1..64.
export const ENV_NAME_RE = /^(?! )[A-Za-z0-9_.\-() +@](?:[A-Za-z0-9_.\-() +@]{0,62}[A-Za-z0-9_\-)+@])?$/;
export const ENV_NAME_MAX_LENGTH = 64;
export interface ValidationResult {
ok: boolean;
reason?: string;
}
/**
* Validate an environment name. Returns a structured result so callers can
* decide how to surface the error (server: 400 body, client: inline message).
*/
export function validateEnvName(name: unknown): ValidationResult {
if (typeof name !== 'string') return { ok: false, reason: 'Name is required' };
if (name.length === 0) return { ok: false, reason: 'Name is required' };
if (name.length > ENV_NAME_MAX_LENGTH) {
return { ok: false, reason: `Name must be ${ENV_NAME_MAX_LENGTH} characters or fewer` };
}
if (!ENV_NAME_RE.test(name)) {
return {
ok: false,
reason:
'Name may contain letters, digits, spaces, and any of - _ . ( ) + @ (no leading/trailing whitespace, no trailing dot, no slashes, no wildcards)'
};
}
return { ok: true };
}
/** Convenience boolean variant. */
export function isValidEnvName(name: unknown): boolean {
return validateEnvName(name).ok;
}