UNPKG

@adguard/agtree

Version:
131 lines (128 loc) 4.79 kB
/* * AGTree v3.2.2 (build date: Tue, 08 Jul 2025 13:39:47 GMT) * (c) 2025 Adguard Software Ltd. * Released under the MIT license * https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme */ import { getDomain, getHostname } from 'tldts'; import isIp from 'is-ip'; import { StringUtils } from '../../utils/string.js'; import { NetworkRuleType, RuleCategory } from '../../nodes/index.js'; import { defaultParserOptions } from '../options.js'; import { BaseParser } from '../base-parser.js'; import { AdblockSyntax } from '../../utils/adblockers.js'; import { ValueParser } from '../misc/value-parser.js'; /* eslint-disable no-param-reassign */ /** * `HostRuleParser` is responsible for parsing hosts-like rules. * * HostRule is a structure for simple host-level rules (i.e. /etc/hosts syntax). * It also supports "just domain" syntax. In this case, the IP will be set to 0.0.0.0. * * Rules syntax looks like this: * ```text * IP_address canonical_hostname [aliases...] * ``` * * @example * `192.168.1.13 bar.mydomain.org bar` -- ipv4 * `ff02::1 ip6-allnodes` -- ipv6 * `::1 localhost ip6-localhost ip6-loopback` -- ipv6 aliases * `example.org` -- "just domain" syntax * @see {@link http://man7.org/linux/man-pages/man5/hosts.5.html} */ class HostRuleParser extends BaseParser { static NULL_IP = '0.0.0.0'; static COMMENT_MARKER = '#'; /** * Parses an etc/hosts-like rule. * * @param raw Raw input to parse. * @param options Global parser options. * @param baseOffset Starting offset of the input. Node locations are calculated relative to this offset. * @returns Host rule node. * * @throws If the input contains invalid data. */ static parse(raw, options = defaultParserOptions, baseOffset = 0) { let offset = StringUtils.skipWS(raw, 0); const parts = []; let lastPartStartIndex = offset; let comment = null; const rawLength = raw.length; const parsePartIfNeeded = (startIndex, endIndex) => { if (startIndex < endIndex) { parts.push(ValueParser.parse(raw.slice(startIndex, endIndex), options, baseOffset + startIndex)); } }; while (offset < rawLength) { if (StringUtils.isWhitespace(raw[offset])) { parsePartIfNeeded(lastPartStartIndex, offset); offset = StringUtils.skipWS(raw, offset); lastPartStartIndex = offset; } else if (raw[offset] === HostRuleParser.COMMENT_MARKER) { const commentStart = offset; offset = StringUtils.skipWS(raw, offset + 1); comment = ValueParser.parse(raw.slice(offset), options, baseOffset + commentStart); offset = rawLength; lastPartStartIndex = offset; } else { offset += 1; } } parsePartIfNeeded(lastPartStartIndex, offset); const partsLength = parts.length; if (partsLength < 1) { throw new Error('Host rule must have at least one domain name or an IP address and a domain name'); } const result = { category: RuleCategory.Network, type: NetworkRuleType.HostRule, syntax: AdblockSyntax.Common, }; if (partsLength === 1) { // "Just domain" syntax, e.g. `example.org` // In this case, domain should be valid and IP will be set to 0.0.0.0 by default if (getDomain(parts[0].value) !== parts[0].value) { throw new Error(`Not a valid domain: ${parts[0].value}`); } result.ip = { type: 'Value', value: HostRuleParser.NULL_IP, }; result.hostnames = { type: 'HostnameList', children: parts, }; } else if (partsLength > 1) { // IP + domain list syntax const [ip, ...hostnames] = parts; if (!isIp(ip.value)) { throw new Error(`Invalid IP address: ${ip.value}`); } for (const { value } of hostnames) { if (getHostname(value) !== value) { throw new Error(`Not a valid hostname: ${value}`); } } result.ip = ip; result.hostnames = { type: 'HostnameList', children: hostnames, }; } if (comment) { result.comment = comment; } if (options.includeRaws) { result.raws = { text: raw, }; } return result; } } export { HostRuleParser };