/** * GEO 数据库的四类文件。名字和默认下载地址都来自内核源码, * 界面(资源一节)和服务端(查文件大小和时间)共用这一份。 */ export interface GeoFileStatus { /** 界面上显示的名字 */ key: string /** 在配置 geox-url 段落里的键名(config.go:363-368) */ configKey: string /** 下载地址:配置里写了用配置的,没写是内核默认 */ url: string /** 文件字节数。文件还没下载过是 null */ size: number | null /** 文件修改时间,ISO 格式。没下载过是 null */ updatedAt: string | null } /** * 文件名与内核一致(constant/path.go:18-20、144)。 * GeoIP.dat 和 GeoSite.dat 内核在目录里大小写不敏感地找(path.go:193-224), * 服务端查文件时也要同样处理。 */ export const GEO_FILES = [ { key: 'MMDB', configKey: 'mmdb', file: 'geoip.metadb' }, { key: 'ASN', configKey: 'asn', file: 'ASN.mmdb' }, { key: 'GEOIP', configKey: 'geoip', file: 'GeoIP.dat' }, { key: 'GEOSITE', configKey: 'geosite', file: 'GeoSite.dat' }, ] as const /** * 下载内容的形状检查,挡住 HTML 错误页、截断的下载这类明显的坏文件。 * Patchbay 自己下载单个 GEO 文件时用(内核的更新接口是全量的,单文件 * 下载只能自己来,也就拿不到内核那套完整校验)。 */ /** MaxMind 格式:元数据标记按规范出现在文件末尾 128KB 内。MMDB 和 ASN 都是这个格式。 */ export function looksLikeMmdb(bytes: Uint8Array): boolean { const marker = [0xab, 0xcd, 0xef, ...Array.from('MaxMind.com', (c) => c.charCodeAt(0))] const from = Math.max(0, bytes.length - 128 * 1024) outer: for (let i = from; i <= bytes.length - marker.length; i++) { for (let j = 0; j < marker.length; j++) { if (bytes[i + j] !== marker[j]) continue outer } return true } return false } /** * geoip.dat / geosite.dat:GeoIPList 和 GeoSiteList 都是「repeated 消息字段 1」, * 首字节必然是 0x0a。弱校验,但足够挡住以 '<' 开头的错误页和空文件。 */ export function looksLikeGeodata(bytes: Uint8Array): boolean { return bytes.length > 1024 && bytes[0] === 0x0a } /** 内核的默认下载地址(config.go:578-583)。配置里没写 geox-url 时就是这些。 */ export const DEFAULT_GEOX_URLS: Record = { mmdb: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb', asn: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb', geoip: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat', geosite: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat', }