// 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; } }