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>
111 lines
5.3 KiB
TypeScript
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;
|
|
}
|
|
}
|