Files
mdg-filius/packages/doc/src/fields.ts
T
julian.kiedaischandClaude Opus 5.5 b4e0046a09 feat(doc): the field rule for a modem's code, which the sheet's commit left behind
87c09c6 uses rule "modemCode" and setModemCode through it; both lived in
these two files, which that commit did not take.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:35:03 +00:00

111 lines
5.3 KiB
TypeScript

// The device sheet's rules for what may be typed into a field: Java's
// EingabenUeberpruefung as FILIUS' settings panels use it
// (JHostKonfiguration, JSwitchKonfiguration, JModemKonfiguration), measured
// with tools/java-oracle's SheetRun (test/sheet.java.json).
//
// Java judges a text twice: while it is typed, the panel colours it red when
// the pattern does not match; when the field loses the focus, the model's
// setter takes it or keeps the old value — and the two disagree. Java takes
// red text ("10..0.0.1", "+10.0.0.1", "-0.0.0.1", Arabic-Indic digits, a
// retention time of "-5"), refuses black text (".0.0.1", a retention time
// past Long's range), and a modem takes any int as its port (0, 65536, -1),
// unchecked. Here one rule decides both: a text is taken exactly when it is
// not marked. What is not taken is not thrown away either (Java puts the old
// value back into the field without a word): the sheet keeps it, marked,
// until it is mended (plan I6).
//
// Spaces around an address or a number are left out (Java refuses
// " 10.0.0.1"; a phone's keyboard puts a space after a word). A name and an
// SSID are taken as typed, spaces included, as Java takes them.
import { DIRECTLY_CONNECTED_SOURCE_MARKER, IP_ADDRESS_PATTERN, javaIpCheck, modemCodeOf, parseIPv4 } from '@mdg-filius/core';
export type FieldRule =
| 'name'
| 'ipAddress'
| 'ipAddressOrEmpty'
| 'subnetMask'
| 'ssid'
| 'seconds'
| 'port'
| 'macAddress'
/** A firewall rule's source: empty, an address, or Java's marker for "a directly connected network". */
| 'firewallSource'
/** A firewall rule's mask: empty, or ones then zeros (EingabenUeberpruefung.isValidSubnetmask). */
| 'firewallMask'
/** A firewall rule's port: empty for all ports, or musterPort. */
| 'portOrEmpty'
/** The code of another project's modem (modemCodeOf: six characters, spaces, dashes and small letters allowed). */
| 'modemCode';
/** What a field's text comes to: the value the device takes, or that it takes none. */
export type CheckedField = { readonly value: string } | { readonly invalid: true };
/** EingabenUeberpruefung.musterSubNetz. */
const SUBNET_MASK_PATTERN = /^(0*([1-9]|1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.((0*([0-9]|1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.){2}(0*([0-9]|1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))$/;
/** EingabenUeberpruefung.musterServiceSetIdentifier: printable ASCII, at least one character. */
const SSID_PATTERN = /^[ -~]+$/;
/** EingabenUeberpruefung.musterPort: 1 to 65535, no leading zero. */
const PORT_PATTERN = /^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{1,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/;
/**
* EingabenUeberpruefung.musterMacAddress, whose class [0-9a-fA-f] also lets
* the six characters between Z and a through ("AA:BB:CC:DD:EE:_F"); here
* hexadecimal digits only.
*/
const MAC_ADDRESS_PATTERN = /^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}$/;
/** EingabenUeberpruefung.musterNurZahlen, at least one digit (Java's Long.parseLong refuses none). */
const DIGITS_PATTERN = /^[0-9]+$/;
const INVALID: CheckedField = { invalid: true };
/**
* An address as the panel's pattern and the model's IP.ipCheck both accept
* it: neither alone (the pattern lets ".0.0.1" through, ipCheck "10..0.0.1"
* and "+10.0.0.1"); the device keeps ipCheck's form ("010.0.0.1" is 10.0.0.1).
*/
function address(text: string, pattern: RegExp): CheckedField {
const canonical = javaIpCheck(text);
return canonical !== null && pattern.test(text) ? { value: canonical } : INVALID;
}
export function checkField(rule: FieldRule, text: string): CheckedField {
switch (rule) {
case 'name':
return { value: text };
case 'ssid':
return SSID_PATTERN.test(text) ? { value: text } : INVALID;
case 'ipAddress':
return address(text.trim(), IP_ADDRESS_PATTERN);
case 'ipAddressOrEmpty':
return text.trim() === '' ? { value: '' } : address(text.trim(), IP_ADDRESS_PATTERN);
case 'subnetMask':
return address(text.trim(), SUBNET_MASK_PATTERN);
case 'seconds': {
const digits = text.trim();
// In milliseconds the device's number must stay exact.
return DIGITS_PATTERN.test(digits) && Number(digits) * 1000 <= Number.MAX_SAFE_INTEGER ? { value: String(Number(digits)) } : INVALID;
}
case 'port':
return PORT_PATTERN.test(text.trim()) ? { value: text.trim() } : INVALID;
case 'firewallSource':
if (text.trim() === DIRECTLY_CONNECTED_SOURCE_MARKER) return { value: DIRECTLY_CONNECTED_SOURCE_MARKER };
return checkField('ipAddressOrEmpty', text);
case 'firewallMask': {
if (text.trim() === '') return { value: '' };
const checked = address(text.trim(), SUBNET_MASK_PATTERN);
if ('invalid' in checked) return checked;
// Ones, then zeros, at least one one (Java: the 32 bits match "^11*0*$").
return /^1+0*$/.test(parseIPv4(checked.value)!.toString(2).padStart(32, '0')) ? checked : INVALID;
}
case 'portOrEmpty':
return text.trim() === '' ? { value: '' } : checkField('port', text);
case 'modemCode': {
const code = modemCodeOf(text);
return code === undefined ? INVALID : { value: code };
}
case 'macAddress':
// Upper case, as the model keeps MAC addresses (Java keeps them as typed and compares ignoring case).
return MAC_ADDRESS_PATTERN.test(text.trim()) ? { value: text.trim().toUpperCase() } : INVALID;
}
}