feat: 完善 staros 系统能力并更新 wincmd 发布版依赖
- 重构 sysconf 为文档模型 INI Parser 与 Config Framework - 强化 hosts 解析、插入校验、写回与异常输入处理 - 完善 StarCmd 生命周期、等待 API、流式输出与 IO 重定向 - 扩展跨平台文件时间、文件锁、内存、进程与网络能力 - 将 Windows 进程适配更新到 b612.me/wincmd v0.1.0 - 移除本地 wincmd/win32api replace,改用发布版依赖 - 将最低 Go 版本提升到 1.18 - 补充 hosts、sysconf、FileLock、StarCmd 与平台适配回归测试
This commit is contained in:
+487
-256
@@ -188,6 +188,79 @@ func NewHosts() *Host {
|
||||
}
|
||||
}
|
||||
|
||||
func cloneHostNode(node *HostNode) *HostNode {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *node
|
||||
cloned.host = append([]string(nil), node.host...)
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneHostNodes(nodes []*HostNode) []*HostNode {
|
||||
if len(nodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
cloned := make([]*HostNode, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
cloned = append(cloned, cloneHostNode(node))
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func copyHostNodeState(dst, src *HostNode) {
|
||||
if dst == nil || src == nil {
|
||||
return
|
||||
}
|
||||
dst.uid = src.uid
|
||||
dst.nextuid = src.nextuid
|
||||
dst.lastuid = src.lastuid
|
||||
dst.ip = src.ip
|
||||
dst.host = append(dst.host[:0], src.host...)
|
||||
dst.comment = src.comment
|
||||
dst.original = src.original
|
||||
dst.onlyComment = src.onlyComment
|
||||
dst.valid = src.valid
|
||||
}
|
||||
|
||||
func normalizeEditableNode(node *HostNode) (*HostNode, error) {
|
||||
if node == nil {
|
||||
return nil, fmt.Errorf("node not exists")
|
||||
}
|
||||
candidate := cloneHostNode(node)
|
||||
if candidate.comment != "" && !strings.HasPrefix(strings.TrimSpace(candidate.comment), "#") {
|
||||
candidate.comment = "#" + candidate.comment
|
||||
}
|
||||
if len(candidate.host) > 0 {
|
||||
hosts, err := normalizeHostTokens(candidate.host...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidate.host = hosts
|
||||
}
|
||||
if candidate.ip != "" || len(candidate.host) > 0 {
|
||||
ip, err := normalizeIPToken(candidate.ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidate.ip = ip
|
||||
if len(candidate.host) == 0 {
|
||||
return nil, fmt.Errorf("empty host")
|
||||
}
|
||||
} else {
|
||||
candidate.ip = strings.TrimSpace(candidate.ip)
|
||||
}
|
||||
candidate.onlyComment = candidate.ip == "" && len(candidate.host) == 0 && candidate.comment != ""
|
||||
if !candidate.onlyComment && candidate.ip == "" && len(candidate.host) == 0 {
|
||||
return nil, fmt.Errorf("empty node")
|
||||
}
|
||||
candidate.valid = candidate.CheckValid()
|
||||
if !candidate.valid {
|
||||
return nil, fmt.Errorf("invalid hosts node")
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
func (h *Host) Parse(hostPath string) error {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
@@ -210,41 +283,27 @@ func (h *Host) parse() error {
|
||||
buf := bufio.NewReader(f)
|
||||
for {
|
||||
line, err := buf.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
if h.idx-1 >= 0 {
|
||||
h.fulldata[h.idx].nextuid = 0
|
||||
h.lastUid = h.idx
|
||||
}
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("read hosts file error: %s", err)
|
||||
}
|
||||
h.idx++
|
||||
line = strings.TrimSpace(line)
|
||||
data, _ := h.parseLine(line)
|
||||
data.uid = h.idx
|
||||
data.lastuid = h.idx - 1
|
||||
data.nextuid = h.idx + 1
|
||||
h.fulldata[data.uid] = &data
|
||||
if h.firstUid == 0 {
|
||||
h.firstUid = h.idx
|
||||
raw := strings.TrimRight(line, "\r\n")
|
||||
if err == nil || (err == io.EOF && raw != "") {
|
||||
data, _ := h.parseLine(raw)
|
||||
h.appendNodeLocked(&data)
|
||||
}
|
||||
if data.valid {
|
||||
for _, v := range data.host {
|
||||
h.hostData[v] = append(h.hostData[v], &data)
|
||||
}
|
||||
h.ipData[data.ip] = append(h.ipData[data.ip], &data)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Host) parseLine(data string) (HostNode, error) {
|
||||
func (h *Host) parseLine(raw string) (HostNode, error) {
|
||||
var res = HostNode{
|
||||
original: data,
|
||||
original: raw,
|
||||
}
|
||||
data := strings.TrimSpace(raw)
|
||||
if len(data) == 0 {
|
||||
return res, fmt.Errorf("empty line")
|
||||
}
|
||||
@@ -311,7 +370,7 @@ func (h *Host) List() []*HostNode {
|
||||
if nextUid == 0 {
|
||||
break
|
||||
}
|
||||
res = append(res, h.fulldata[nextUid])
|
||||
res = append(res, cloneHostNode(h.fulldata[nextUid]))
|
||||
nextUid = h.fulldata[nextUid].nextuid
|
||||
}
|
||||
return res
|
||||
@@ -353,7 +412,7 @@ func (h *Host) ListByHost(host string) []*HostNode {
|
||||
if h.hostData == nil {
|
||||
return nil
|
||||
}
|
||||
return h.hostData[host]
|
||||
return cloneHostNodes(h.hostData[host])
|
||||
}
|
||||
|
||||
func (h *Host) ListIPsByHost(host string) []string {
|
||||
@@ -387,7 +446,7 @@ func (h *Host) ListByIP(ip string) []*HostNode {
|
||||
if h.ipData == nil {
|
||||
return nil
|
||||
}
|
||||
return h.ipData[ip]
|
||||
return cloneHostNodes(h.ipData[ip])
|
||||
}
|
||||
|
||||
func (h *Host) ListHostsByIP(ip string) []string {
|
||||
@@ -443,43 +502,27 @@ cntfor:
|
||||
for _, v := range ipInfo {
|
||||
for _, host := range hosts {
|
||||
if len(v.host) == 1 && v.host[0] == host {
|
||||
delete(h.ipData, ip)
|
||||
if v.lastuid != 0 {
|
||||
h.fulldata[v.lastuid].nextuid = v.nextuid
|
||||
} else {
|
||||
h.firstUid = v.nextuid
|
||||
}
|
||||
if v.nextuid != 0 {
|
||||
h.fulldata[v.nextuid].lastuid = v.lastuid
|
||||
} else {
|
||||
h.lastUid = v.lastuid
|
||||
}
|
||||
var newHostData []*HostNode
|
||||
for _, vv := range h.hostData[v.host[0]] {
|
||||
if vv.uid != v.uid {
|
||||
newHostData = append(newHostData, vv)
|
||||
}
|
||||
}
|
||||
h.hostData[host] = newHostData
|
||||
delete(h.fulldata, v.uid)
|
||||
h.removeNodeFromIPDataLocked(ip, v.uid)
|
||||
h.removeNodeFromHostDataLocked(host, v.uid)
|
||||
h.unlinkNodeLocked(v)
|
||||
v = nil
|
||||
continue cntfor
|
||||
}
|
||||
if len(v.host) > 1 {
|
||||
var newHosts []string
|
||||
removed := false
|
||||
for _, vv := range v.host {
|
||||
if vv != host {
|
||||
newHosts = append(newHosts, vv)
|
||||
} else {
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
if !removed {
|
||||
continue
|
||||
}
|
||||
v.host = newHosts
|
||||
var newHostData []*HostNode
|
||||
for _, vv := range h.hostData[host] {
|
||||
if vv.uid != v.uid {
|
||||
newHostData = append(newHostData, vv)
|
||||
}
|
||||
}
|
||||
h.hostData[host] = newHostData
|
||||
h.removeNodeFromHostDataLocked(host, v.uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,33 +536,7 @@ func (h *Host) RemoveIPs(ips ...string) error {
|
||||
return fmt.Errorf("hosts data not initialized")
|
||||
}
|
||||
for _, ip := range ips {
|
||||
ipInfo := h.ipData[ip]
|
||||
if len(ipInfo) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, v := range ipInfo {
|
||||
delete(h.ipData, ip)
|
||||
delete(h.fulldata, v.uid)
|
||||
if v.lastuid != 0 {
|
||||
h.fulldata[v.lastuid].nextuid = v.nextuid
|
||||
} else {
|
||||
h.firstUid = v.nextuid
|
||||
}
|
||||
if v.nextuid != 0 {
|
||||
h.fulldata[v.nextuid].lastuid = v.lastuid
|
||||
} else {
|
||||
h.lastUid = v.lastuid
|
||||
}
|
||||
for _, host := range v.host {
|
||||
var newHostData []*HostNode
|
||||
for _, vv := range h.hostData[host] {
|
||||
if vv.uid != v.uid {
|
||||
newHostData = append(newHostData, vv)
|
||||
}
|
||||
}
|
||||
h.hostData[host] = newHostData
|
||||
}
|
||||
}
|
||||
h.removeIPLocked(ip)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -531,64 +548,67 @@ func (h *Host) RemoveHosts(hosts ...string) error {
|
||||
return fmt.Errorf("hosts data not initialized")
|
||||
}
|
||||
for _, host := range hosts {
|
||||
hostInfo := h.hostData[host]
|
||||
if len(hostInfo) == 0 {
|
||||
continue
|
||||
}
|
||||
delete(h.hostData, host)
|
||||
for _, v := range hostInfo {
|
||||
var newHosts []string
|
||||
for _, vv := range v.host {
|
||||
if vv != host {
|
||||
newHosts = append(newHosts, vv)
|
||||
}
|
||||
}
|
||||
v.host = newHosts
|
||||
if len(v.host) == 0 {
|
||||
delete(h.ipData, v.ip)
|
||||
if v.lastuid != 0 {
|
||||
h.fulldata[v.lastuid].nextuid = v.nextuid
|
||||
} else {
|
||||
h.firstUid = v.nextuid
|
||||
}
|
||||
if v.nextuid != 0 {
|
||||
h.fulldata[v.nextuid].lastuid = v.lastuid
|
||||
} else {
|
||||
h.lastUid = v.lastuid
|
||||
}
|
||||
delete(h.fulldata, v.uid)
|
||||
}
|
||||
}
|
||||
h.removeHostLocked(host)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Host) SetIPHosts(ip string, hosts ...string) error {
|
||||
info := h.ListByIP(ip)
|
||||
if len(info) == 0 {
|
||||
return h.AddHosts(ip, hosts...)
|
||||
} else if len(info) == 1 {
|
||||
info[0].host = hosts
|
||||
info[0].comment = ""
|
||||
return nil
|
||||
}
|
||||
err := h.RemoveIPs(ip)
|
||||
var err error
|
||||
ip, err = normalizeIPToken(ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return h.AddHosts(ip, hosts...)
|
||||
hosts, err = normalizeHostTokens(hosts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(hosts) == 0 {
|
||||
return fmt.Errorf("empty host")
|
||||
}
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
if h.ipData == nil {
|
||||
return fmt.Errorf("hosts data not initialized")
|
||||
}
|
||||
info := h.ipData[ip]
|
||||
if len(info) == 0 {
|
||||
return h.addHostsLocked("", ip, hosts...)
|
||||
} else if len(info) == 1 {
|
||||
node := *info[0]
|
||||
node.host = append([]string(nil), hosts...)
|
||||
node.comment = ""
|
||||
return h.updateNodeLocked(&node)
|
||||
}
|
||||
h.removeIPLocked(ip)
|
||||
return h.addHostsLocked("", ip, hosts...)
|
||||
}
|
||||
|
||||
func (h *Host) SetHostIPs(host string, ips ...string) error {
|
||||
if len(ips) == 0 {
|
||||
return fmt.Errorf("no ip address")
|
||||
}
|
||||
err := h.RemoveHosts(host)
|
||||
var err error
|
||||
host, err = normalizeHostToken(host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
normalizedIPs := make([]string, 0, len(ips))
|
||||
for _, ip := range ips {
|
||||
err := h.AddHosts(ip, host)
|
||||
normalizedIP, err := normalizeIPToken(ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
normalizedIPs = append(normalizedIPs, normalizedIP)
|
||||
}
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
if h.hostData == nil {
|
||||
return fmt.Errorf("hosts data not initialized")
|
||||
}
|
||||
h.removeHostLocked(host)
|
||||
for _, ip := range normalizedIPs {
|
||||
err := h.addHostsLocked("", ip, host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -599,14 +619,27 @@ func (h *Host) SetHostIPs(host string, ips ...string) error {
|
||||
func (h *Host) addHosts(comment string, ip string, hosts ...string) error {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
return h.addHostsLocked(comment, ip, hosts...)
|
||||
}
|
||||
|
||||
func (h *Host) addHostsLocked(comment string, ip string, hosts ...string) error {
|
||||
if h.hostData == nil {
|
||||
return fmt.Errorf("hosts data not initialized")
|
||||
}
|
||||
var err error
|
||||
ip, err = normalizeIPToken(ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ipInfo := h.listHostsByIP(ip)
|
||||
var needAddHosts []string
|
||||
for _, v := range hosts {
|
||||
if !inArray(ipInfo, v) {
|
||||
needAddHosts = append(needAddHosts, v)
|
||||
host, err := normalizeHostToken(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !inArray(ipInfo, host) && !inArray(needAddHosts, host) {
|
||||
needAddHosts = append(needAddHosts, host)
|
||||
}
|
||||
}
|
||||
if len(needAddHosts) == 0 {
|
||||
@@ -616,39 +649,88 @@ func (h *Host) addHosts(comment string, ip string, hosts ...string) error {
|
||||
comment = "#" + comment
|
||||
}
|
||||
hostNode := HostNode{
|
||||
uid: h.idx + 1,
|
||||
nextuid: 0,
|
||||
lastuid: h.lastUid,
|
||||
ip: ip,
|
||||
host: needAddHosts,
|
||||
valid: true,
|
||||
comment: comment,
|
||||
}
|
||||
h.idx++
|
||||
h.fulldata[h.lastUid].nextuid = h.idx
|
||||
h.lastUid = h.idx
|
||||
h.fulldata[h.idx] = &hostNode
|
||||
h.ipData[ip] = append(h.ipData[ip], &hostNode)
|
||||
for _, v := range needAddHosts {
|
||||
h.hostData[v] = append(h.hostData[v], &hostNode)
|
||||
if !hostNode.CheckValid() {
|
||||
return fmt.Errorf("invalid hosts node")
|
||||
}
|
||||
h.appendNodeLocked(&hostNode)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Host) removeHostLocked(host string) {
|
||||
hostInfo := h.hostData[host]
|
||||
if len(hostInfo) == 0 {
|
||||
return
|
||||
}
|
||||
delete(h.hostData, host)
|
||||
for _, v := range hostInfo {
|
||||
var newHosts []string
|
||||
for _, vv := range v.host {
|
||||
if vv != host {
|
||||
newHosts = append(newHosts, vv)
|
||||
}
|
||||
}
|
||||
v.host = newHosts
|
||||
if len(v.host) == 0 {
|
||||
h.removeNodeFromIPDataLocked(v.ip, v.uid)
|
||||
h.unlinkNodeLocked(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Host) removeIPLocked(ip string) {
|
||||
ipInfo := h.ipData[ip]
|
||||
if len(ipInfo) == 0 {
|
||||
return
|
||||
}
|
||||
delete(h.ipData, ip)
|
||||
for _, v := range ipInfo {
|
||||
for _, host := range v.host {
|
||||
h.removeNodeFromHostDataLocked(host, v.uid)
|
||||
}
|
||||
h.unlinkNodeLocked(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Host) InsertNodeByData(node *HostNode, before bool, comment string, ip string, hosts ...string) error {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
if h.hostData == nil {
|
||||
return fmt.Errorf("hosts data not initialized")
|
||||
}
|
||||
if _, ok := h.fulldata[node.uid]; !ok {
|
||||
if node == nil {
|
||||
return fmt.Errorf("node not exists")
|
||||
}
|
||||
anchor, ok := h.fulldata[node.uid]
|
||||
if !ok {
|
||||
return fmt.Errorf("node not exists")
|
||||
}
|
||||
if comment != "" && !strings.HasPrefix(strings.TrimSpace(comment), "#") {
|
||||
comment = "#" + comment
|
||||
}
|
||||
ip = strings.TrimSpace(ip)
|
||||
var err error
|
||||
hosts, err = normalizeHostTokens(hosts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ip != "" || len(hosts) > 0 {
|
||||
ip, err = normalizeIPToken(ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(hosts) == 0 {
|
||||
return fmt.Errorf("empty host")
|
||||
}
|
||||
}
|
||||
if ip == "" && len(hosts) == 0 && comment == "" {
|
||||
return fmt.Errorf("empty node")
|
||||
}
|
||||
hostNode := HostNode{
|
||||
uid: h.idx + 1,
|
||||
ip: ip,
|
||||
host: hosts,
|
||||
valid: true,
|
||||
@@ -657,26 +739,43 @@ func (h *Host) InsertNodeByData(node *HostNode, before bool, comment string, ip
|
||||
if ip == "" && len(hosts) == 0 && comment != "" {
|
||||
hostNode.onlyComment = true
|
||||
}
|
||||
if before {
|
||||
hostNode.nextuid = node.uid
|
||||
hostNode.lastuid = node.lastuid
|
||||
if node.lastuid != 0 {
|
||||
h.fulldata[node.lastuid].nextuid = h.idx
|
||||
} else {
|
||||
h.firstUid = h.idx
|
||||
}
|
||||
node.lastuid = h.idx
|
||||
} else {
|
||||
hostNode.lastuid = node.uid
|
||||
hostNode.nextuid = node.nextuid
|
||||
if node.nextuid != 0 {
|
||||
h.fulldata[node.nextuid].lastuid = h.idx
|
||||
} else {
|
||||
h.lastUid = h.idx
|
||||
}
|
||||
node.nextuid = h.idx
|
||||
return h.insertNodeRelativeLocked(anchor, before, &hostNode)
|
||||
}
|
||||
|
||||
func normalizeIPToken(ip string) (string, error) {
|
||||
ip = strings.TrimSpace(ip)
|
||||
if net.ParseIP(ip) == nil {
|
||||
return "", fmt.Errorf("invalid ip address")
|
||||
}
|
||||
return nil
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func normalizeHostToken(host string) (string, error) {
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
return "", fmt.Errorf("empty host")
|
||||
}
|
||||
if strings.ContainsAny(host, " \t\r\n") || strings.HasPrefix(host, "#") {
|
||||
return "", fmt.Errorf("invalid host")
|
||||
}
|
||||
return host, nil
|
||||
}
|
||||
|
||||
func normalizeHostTokens(hosts ...string) ([]string, error) {
|
||||
out := make([]string, 0, len(hosts))
|
||||
seen := make(map[string]struct{}, len(hosts))
|
||||
for _, host := range hosts {
|
||||
normalized, err := normalizeHostToken(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := seen[normalized]; ok {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = struct{}{}
|
||||
out = append(out, normalized)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (h *Host) DeleteNode(node *HostNode) error {
|
||||
@@ -685,49 +784,38 @@ func (h *Host) DeleteNode(node *HostNode) error {
|
||||
if h.hostData == nil {
|
||||
return fmt.Errorf("hosts data not initialized")
|
||||
}
|
||||
if _, ok := h.fulldata[node.uid]; !ok {
|
||||
if node == nil {
|
||||
return fmt.Errorf("node not exists")
|
||||
}
|
||||
if node.lastuid != 0 {
|
||||
h.fulldata[node.lastuid].nextuid = node.nextuid
|
||||
} else {
|
||||
h.firstUid = node.nextuid
|
||||
current, ok := h.fulldata[node.uid]
|
||||
if !ok {
|
||||
return fmt.Errorf("node not exists")
|
||||
}
|
||||
if node.nextuid != 0 {
|
||||
h.fulldata[node.nextuid].lastuid = node.lastuid
|
||||
} else {
|
||||
h.lastUid = node.lastuid
|
||||
for _, v := range current.host {
|
||||
h.removeNodeFromHostDataLocked(v, current.uid)
|
||||
}
|
||||
for _, v := range node.host {
|
||||
var newHostData []*HostNode
|
||||
for _, vv := range h.hostData[v] {
|
||||
if vv.uid != node.uid {
|
||||
newHostData = append(newHostData, vv)
|
||||
}
|
||||
}
|
||||
h.hostData[v] = newHostData
|
||||
if current.ip != "" {
|
||||
h.removeNodeFromIPDataLocked(current.ip, current.uid)
|
||||
}
|
||||
ipInfo := h.ipData[node.ip]
|
||||
var newIPData []*HostNode
|
||||
for _, v := range ipInfo {
|
||||
if v.uid != node.uid {
|
||||
newIPData = append(newIPData, v)
|
||||
}
|
||||
}
|
||||
h.ipData[node.ip] = newIPData
|
||||
delete(h.fulldata, node.uid)
|
||||
h.unlinkNodeLocked(current)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Host) DeleteNodeByUID(uid uint64) error {
|
||||
h.RLock()
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
node, ok := h.fulldata[uid]
|
||||
if !ok {
|
||||
h.RUnlock()
|
||||
return fmt.Errorf("node not exists")
|
||||
}
|
||||
h.RUnlock()
|
||||
return h.DeleteNode(node)
|
||||
for _, v := range node.host {
|
||||
h.removeNodeFromHostDataLocked(v, node.uid)
|
||||
}
|
||||
if node.ip != "" {
|
||||
h.removeNodeFromIPDataLocked(node.ip, node.uid)
|
||||
}
|
||||
h.unlinkNodeLocked(node)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Host) AddNode(node *HostNode) error {
|
||||
@@ -736,26 +824,12 @@ func (h *Host) AddNode(node *HostNode) error {
|
||||
if h.hostData == nil {
|
||||
return fmt.Errorf("hosts data not initialized")
|
||||
}
|
||||
if node.comment != "" && !strings.HasPrefix(strings.TrimSpace(node.comment), "#") {
|
||||
node.comment = "#" + node.comment
|
||||
}
|
||||
if node.ip == "" && len(node.host) == 0 && node.comment != "" {
|
||||
node.onlyComment = true
|
||||
}
|
||||
h.idx++
|
||||
node.uid = h.idx
|
||||
node.lastuid = h.lastUid
|
||||
node.nextuid = 0
|
||||
h.fulldata[h.lastUid].nextuid = node.uid
|
||||
h.lastUid = node.uid
|
||||
h.fulldata[node.uid] = node
|
||||
node.valid = node.CheckValid()
|
||||
if node.valid {
|
||||
h.ipData[node.ip] = append(h.ipData[node.ip], node)
|
||||
for _, v := range node.host {
|
||||
h.hostData[v] = append(h.hostData[v], node)
|
||||
}
|
||||
prepared, err := normalizeEditableNode(node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.appendNodeLocked(prepared)
|
||||
copyHostNodeState(node, prepared)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -768,28 +842,157 @@ func (h *Host) InsertNode(node *HostNode) error {
|
||||
if h.hostData == nil {
|
||||
return fmt.Errorf("hosts data not initialized")
|
||||
}
|
||||
if node.comment != "" && !strings.HasPrefix(strings.TrimSpace(node.comment), "#") {
|
||||
node.comment = "#" + node.comment
|
||||
prepared, err := normalizeEditableNode(node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node.ip == "" && len(node.host) == 0 && node.comment != "" {
|
||||
node.onlyComment = true
|
||||
prepared.uid = h.idx + 1
|
||||
lastNode := h.fulldata[prepared.lastuid]
|
||||
nextNode := h.fulldata[prepared.nextuid]
|
||||
if lastNode == nil || nextNode == nil {
|
||||
return fmt.Errorf("node link not exists")
|
||||
}
|
||||
node.uid = h.idx + 1
|
||||
if h.fulldata[node.lastuid].nextuid != node.nextuid {
|
||||
if lastNode.nextuid != prepared.nextuid || nextNode.lastuid != prepared.lastuid {
|
||||
return fmt.Errorf("node lastuid nextuid not match")
|
||||
}
|
||||
h.idx++
|
||||
h.fulldata[node.lastuid].nextuid = node.uid
|
||||
h.fulldata[node.nextuid].lastuid = node.uid
|
||||
h.fulldata[node.uid] = node
|
||||
lastNode.nextuid = prepared.uid
|
||||
nextNode.lastuid = prepared.uid
|
||||
h.storeNodeLocked(prepared)
|
||||
copyHostNodeState(node, prepared)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Host) appendNodeLocked(node *HostNode) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
h.idx++
|
||||
node.uid = h.idx
|
||||
node.lastuid = h.lastUid
|
||||
node.nextuid = 0
|
||||
if h.lastUid != 0 {
|
||||
if last := h.fulldata[h.lastUid]; last != nil {
|
||||
last.nextuid = node.uid
|
||||
}
|
||||
} else {
|
||||
h.firstUid = node.uid
|
||||
}
|
||||
if h.firstUid == 0 {
|
||||
h.firstUid = node.uid
|
||||
}
|
||||
h.lastUid = node.uid
|
||||
h.storeNodeLocked(node)
|
||||
}
|
||||
|
||||
func (h *Host) insertNodeRelativeLocked(anchor *HostNode, before bool, node *HostNode) error {
|
||||
if anchor == nil || node == nil {
|
||||
return fmt.Errorf("node not exists")
|
||||
}
|
||||
h.idx++
|
||||
node.uid = h.idx
|
||||
if before {
|
||||
node.nextuid = anchor.uid
|
||||
node.lastuid = anchor.lastuid
|
||||
if anchor.lastuid != 0 {
|
||||
prev := h.fulldata[anchor.lastuid]
|
||||
if prev == nil {
|
||||
return fmt.Errorf("node lastuid not exists")
|
||||
}
|
||||
prev.nextuid = node.uid
|
||||
} else {
|
||||
h.firstUid = node.uid
|
||||
}
|
||||
anchor.lastuid = node.uid
|
||||
} else {
|
||||
node.lastuid = anchor.uid
|
||||
node.nextuid = anchor.nextuid
|
||||
if anchor.nextuid != 0 {
|
||||
next := h.fulldata[anchor.nextuid]
|
||||
if next == nil {
|
||||
return fmt.Errorf("node nextuid not exists")
|
||||
}
|
||||
next.lastuid = node.uid
|
||||
} else {
|
||||
h.lastUid = node.uid
|
||||
}
|
||||
anchor.nextuid = node.uid
|
||||
}
|
||||
if h.firstUid == 0 {
|
||||
h.firstUid = node.uid
|
||||
}
|
||||
if node.nextuid == 0 {
|
||||
h.lastUid = node.uid
|
||||
}
|
||||
node.valid = node.CheckValid()
|
||||
if node.valid {
|
||||
h.ipData[node.ip] = append(h.ipData[node.ip], node)
|
||||
for _, v := range node.host {
|
||||
h.hostData[v] = append(h.hostData[v], node)
|
||||
h.storeNodeLocked(node)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Host) storeNodeLocked(node *HostNode) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
stored := cloneHostNode(node)
|
||||
h.fulldata[stored.uid] = stored
|
||||
if !stored.valid {
|
||||
return
|
||||
}
|
||||
if stored.ip != "" {
|
||||
h.ipData[stored.ip] = append(h.ipData[stored.ip], stored)
|
||||
}
|
||||
for _, v := range stored.host {
|
||||
h.hostData[v] = append(h.hostData[v], stored)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Host) removeNodeFromIPDataLocked(ip string, uid uint64) {
|
||||
var newIPData []*HostNode
|
||||
for _, node := range h.ipData[ip] {
|
||||
if node.uid != uid {
|
||||
newIPData = append(newIPData, node)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
if len(newIPData) == 0 {
|
||||
delete(h.ipData, ip)
|
||||
return
|
||||
}
|
||||
h.ipData[ip] = newIPData
|
||||
}
|
||||
|
||||
func (h *Host) removeNodeFromHostDataLocked(host string, uid uint64) {
|
||||
var newHostData []*HostNode
|
||||
for _, node := range h.hostData[host] {
|
||||
if node.uid != uid {
|
||||
newHostData = append(newHostData, node)
|
||||
}
|
||||
}
|
||||
if len(newHostData) == 0 {
|
||||
delete(h.hostData, host)
|
||||
return
|
||||
}
|
||||
h.hostData[host] = newHostData
|
||||
}
|
||||
|
||||
func (h *Host) unlinkNodeLocked(node *HostNode) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
if node.lastuid != 0 {
|
||||
if last := h.fulldata[node.lastuid]; last != nil {
|
||||
last.nextuid = node.nextuid
|
||||
}
|
||||
} else {
|
||||
h.firstUid = node.nextuid
|
||||
}
|
||||
if node.nextuid != 0 {
|
||||
if next := h.fulldata[node.nextuid]; next != nil {
|
||||
next.lastuid = node.lastuid
|
||||
}
|
||||
} else {
|
||||
h.lastUid = node.lastuid
|
||||
}
|
||||
delete(h.fulldata, node.uid)
|
||||
}
|
||||
|
||||
func inArray(arr []string, v string) bool {
|
||||
@@ -812,7 +1015,7 @@ func (h *Host) Save() error {
|
||||
func (h *Host) save(path string) error {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
if h.firstUid == 0 || h.fulldata == nil {
|
||||
if h.fulldata == nil {
|
||||
return fmt.Errorf("no data")
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
@@ -820,13 +1023,30 @@ func (h *Host) save(path string) error {
|
||||
return fmt.Errorf("open hosts file %s error: %s", h.hostPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
if h.firstUid == 0 {
|
||||
return nil
|
||||
}
|
||||
node := h.fulldata[h.firstUid]
|
||||
if node == nil {
|
||||
return fmt.Errorf("no data")
|
||||
}
|
||||
for {
|
||||
if node.onlyComment {
|
||||
if _, err := f.WriteString(node.comment + "\n"); err != nil {
|
||||
if node.original != "" && strings.TrimSpace(node.original) == node.comment {
|
||||
if _, err := f.WriteString(node.original + lineBreaker); err != nil {
|
||||
return fmt.Errorf("write hosts file error: %s", err)
|
||||
}
|
||||
} else if _, err := f.WriteString(node.comment + lineBreaker); err != nil {
|
||||
return fmt.Errorf("write hosts file error: %s", err)
|
||||
}
|
||||
if node.nextuid == 0 {
|
||||
break
|
||||
}
|
||||
node = h.fulldata[node.nextuid]
|
||||
continue
|
||||
}
|
||||
if !node.valid {
|
||||
if _, err := f.WriteString(node.original + lineBreaker); err != nil {
|
||||
return fmt.Errorf("write hosts file error: %s", err)
|
||||
}
|
||||
if node.nextuid == 0 {
|
||||
@@ -858,11 +1078,14 @@ func (h *Host) save(path string) error {
|
||||
}
|
||||
|
||||
func (h *Host) Build() ([]byte, error) {
|
||||
if h.firstUid == 0 || h.fulldata == nil {
|
||||
return nil, fmt.Errorf("no data")
|
||||
}
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
if h.fulldata == nil {
|
||||
return nil, fmt.Errorf("no data")
|
||||
}
|
||||
if h.firstUid == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
var f bytes.Buffer
|
||||
node := h.fulldata[h.firstUid]
|
||||
if node == nil {
|
||||
@@ -870,7 +1093,21 @@ func (h *Host) Build() ([]byte, error) {
|
||||
}
|
||||
for {
|
||||
if node.onlyComment {
|
||||
if _, err := f.WriteString(node.comment + "\n"); err != nil {
|
||||
if node.original != "" && strings.TrimSpace(node.original) == node.comment {
|
||||
if _, err := f.WriteString(node.original + lineBreaker); err != nil {
|
||||
return nil, fmt.Errorf("write hosts file error: %s", err)
|
||||
}
|
||||
} else if _, err := f.WriteString(node.comment + lineBreaker); err != nil {
|
||||
return nil, fmt.Errorf("write hosts file error: %s", err)
|
||||
}
|
||||
if node.nextuid == 0 {
|
||||
break
|
||||
}
|
||||
node = h.fulldata[node.nextuid]
|
||||
continue
|
||||
}
|
||||
if !node.valid {
|
||||
if _, err := f.WriteString(node.original + lineBreaker); err != nil {
|
||||
return nil, fmt.Errorf("write hosts file error: %s", err)
|
||||
}
|
||||
if node.nextuid == 0 {
|
||||
@@ -910,7 +1147,7 @@ func (h *Host) GetFirstNode() (*HostNode, error) {
|
||||
if h.fulldata == nil {
|
||||
return nil, fmt.Errorf("no data")
|
||||
}
|
||||
return h.fulldata[h.firstUid], nil
|
||||
return cloneHostNode(h.fulldata[h.firstUid]), nil
|
||||
}
|
||||
|
||||
func (h *Host) GetNode(nodeID uint64) (*HostNode, error) {
|
||||
@@ -920,7 +1157,7 @@ func (h *Host) GetNode(nodeID uint64) (*HostNode, error) {
|
||||
return nil, fmt.Errorf("no data")
|
||||
}
|
||||
if node, ok := h.fulldata[nodeID]; ok {
|
||||
return node, nil
|
||||
return cloneHostNode(node), nil
|
||||
}
|
||||
return nil, fmt.Errorf("node not exists")
|
||||
}
|
||||
@@ -935,7 +1172,7 @@ func (h *Host) GetNextNode(nodeID uint64) (*HostNode, error) {
|
||||
if node.nextuid == 0 {
|
||||
return nil, fmt.Errorf("no next node")
|
||||
}
|
||||
return h.fulldata[node.nextuid], nil
|
||||
return cloneHostNode(h.fulldata[node.nextuid]), nil
|
||||
}
|
||||
return nil, fmt.Errorf("node not exists")
|
||||
}
|
||||
@@ -950,7 +1187,7 @@ func (h *Host) GetLastNode(nodeID uint64) (*HostNode, error) {
|
||||
if node.lastuid == 0 {
|
||||
return nil, fmt.Errorf("no last node")
|
||||
}
|
||||
return h.fulldata[node.lastuid], nil
|
||||
return cloneHostNode(h.fulldata[node.lastuid]), nil
|
||||
}
|
||||
return nil, fmt.Errorf("node not exists")
|
||||
}
|
||||
@@ -964,50 +1201,44 @@ func (h *Host) GetLatestNode() (*HostNode, error) {
|
||||
if h.fulldata == nil {
|
||||
return nil, fmt.Errorf("no data")
|
||||
}
|
||||
return h.fulldata[h.lastUid], nil
|
||||
return cloneHostNode(h.fulldata[h.lastUid]), nil
|
||||
}
|
||||
|
||||
func (h *Host) UpdateNode(node *HostNode) error {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
return h.updateNodeLocked(node)
|
||||
}
|
||||
|
||||
func (h *Host) updateNodeLocked(node *HostNode) error {
|
||||
if node == nil {
|
||||
return fmt.Errorf("node not exists")
|
||||
}
|
||||
if h.fulldata == nil {
|
||||
return fmt.Errorf("no data")
|
||||
}
|
||||
if _, ok := h.fulldata[node.uid]; !ok {
|
||||
current, ok := h.fulldata[node.uid]
|
||||
if !ok {
|
||||
return fmt.Errorf("node not exists")
|
||||
}
|
||||
if node.comment != "" && !strings.HasPrefix(strings.TrimSpace(node.comment), "#") {
|
||||
node.comment = "#" + node.comment
|
||||
candidate, err := normalizeEditableNode(node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node.ip == "" && len(node.host) == 0 && node.comment != "" {
|
||||
node.onlyComment = true
|
||||
candidate.uid = current.uid
|
||||
candidate.lastuid = current.lastuid
|
||||
candidate.nextuid = current.nextuid
|
||||
if candidate.original == "" {
|
||||
candidate.original = current.original
|
||||
}
|
||||
h.fulldata[node.uid] = node
|
||||
node.valid = node.CheckValid()
|
||||
for k, v := range h.ipData {
|
||||
var newIPData []*HostNode
|
||||
for _, vv := range v {
|
||||
if vv.uid != node.uid {
|
||||
newIPData = append(newIPData, vv)
|
||||
}
|
||||
}
|
||||
h.ipData[k] = newIPData
|
||||
for _, host := range current.host {
|
||||
h.removeNodeFromHostDataLocked(host, current.uid)
|
||||
}
|
||||
for k, v := range h.hostData {
|
||||
var newHostData []*HostNode
|
||||
for _, vv := range v {
|
||||
if vv.uid != node.uid {
|
||||
newHostData = append(newHostData, vv)
|
||||
}
|
||||
}
|
||||
h.hostData[k] = newHostData
|
||||
}
|
||||
if node.valid {
|
||||
h.ipData[node.ip] = append(h.ipData[node.ip], node)
|
||||
for _, v := range node.host {
|
||||
h.hostData[v] = append(h.hostData[v], node)
|
||||
}
|
||||
if current.ip != "" {
|
||||
h.removeNodeFromIPDataLocked(current.ip, current.uid)
|
||||
}
|
||||
h.storeNodeLocked(candidate)
|
||||
copyHostNodeState(node, candidate)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+580
-2
@@ -2,11 +2,15 @@ package hosts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_Hosts(t *testing.T) {
|
||||
var h = NewHosts()
|
||||
tmpDir := t.TempDir()
|
||||
err := h.Parse("./test_hosts.txt")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -89,7 +93,7 @@ func Test_Hosts(t *testing.T) {
|
||||
t.Log(data)
|
||||
}
|
||||
|
||||
err = h.SaveAs("./test_hosts_01.txt")
|
||||
err = h.SaveAs(filepath.Join(tmpDir, "test_hosts_01.txt"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -133,7 +137,7 @@ func Test_Hosts(t *testing.T) {
|
||||
t.Error("Expected 1 got ", data)
|
||||
}
|
||||
|
||||
err = h.SaveAs("./test_hosts_02.txt")
|
||||
err = h.SaveAs(filepath.Join(tmpDir, "test_hosts_02.txt"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -152,3 +156,577 @@ func BenchmarkAddHosts(b *testing.B) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHandlesEmptyAndNoTrailingNewline(t *testing.T) {
|
||||
t.Run("empty file", func(t *testing.T) {
|
||||
h := NewHosts()
|
||||
path := filepath.Join(t.TempDir(), "hosts.empty")
|
||||
if err := os.WriteFile(path, nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.Parse(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := h.List(); len(got) != 0 {
|
||||
t.Fatalf("expected empty hosts list, got %d entries", len(got))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("last line without newline", func(t *testing.T) {
|
||||
h := NewHosts()
|
||||
path := filepath.Join(t.TempDir(), "hosts.nonewline")
|
||||
if err := os.WriteFile(path, []byte("1.2.3.4 example.test"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.Parse(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := h.ListIPsByHost("example.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
||||
t.Fatalf("expected last line to be parsed, got %v", got)
|
||||
}
|
||||
node, err := h.GetLatestNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if node.NextUID() != 0 {
|
||||
t.Fatalf("expected last node next uid 0, got %d", node.NextUID())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddHostsAndAddNodeWorkOnEmptyModel(t *testing.T) {
|
||||
t.Run("add hosts", func(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := h.ListFirstIPByHost("example.test"); got != "1.2.3.4" {
|
||||
t.Fatalf("expected inserted host ip, got %q", got)
|
||||
}
|
||||
out, err := h.Build()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(out), "1.2.3.4 example.test") {
|
||||
t.Fatalf("unexpected build output: %q", out)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("add node", func(t *testing.T) {
|
||||
h := NewHosts()
|
||||
node := &HostNode{}
|
||||
node.SetIP("5.6.7.8")
|
||||
node.SetHosts("node.test")
|
||||
if err := h.AddNode(node); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if node.UID() == 0 {
|
||||
t.Fatal("expected node uid to be assigned")
|
||||
}
|
||||
if got := h.ListFirstIPByHost("node.test"); got != "5.6.7.8" {
|
||||
t.Fatalf("expected inserted node ip, got %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInsertNodeByDataInsertsAndLinksNode(t *testing.T) {
|
||||
h := NewHosts()
|
||||
path := filepath.Join(t.TempDir(), "hosts.insert")
|
||||
if err := os.WriteFile(path, []byte("2.2.2.2 anchor.test\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.Parse(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
anchor, err := h.GetFirstNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.InsertNodeByData(anchor, true, "before", "1.1.1.1", "before.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.InsertNodeByData(anchor, false, "after", "3.3.3.3", "after.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
nodes := h.List()
|
||||
if len(nodes) != 3 {
|
||||
t.Fatalf("expected 3 nodes after insert, got %d", len(nodes))
|
||||
}
|
||||
if nodes[0].IP() != "1.1.1.1" || nodes[1].IP() != "2.2.2.2" || nodes[2].IP() != "3.3.3.3" {
|
||||
t.Fatalf("unexpected node order: %q, %q, %q", nodes[0].IP(), nodes[1].IP(), nodes[2].IP())
|
||||
}
|
||||
if got := h.ListFirstIPByHost("before.test"); got != "1.1.1.1" {
|
||||
t.Fatalf("expected before node to be indexed, got %q", got)
|
||||
}
|
||||
if got := h.ListFirstIPByHost("after.test"); got != "3.3.3.3" {
|
||||
t.Fatalf("expected after node to be indexed, got %q", got)
|
||||
}
|
||||
if nodes[0].NextUID() != nodes[1].UID() || nodes[1].LastUID() != nodes[0].UID() {
|
||||
t.Fatalf("before/anchor linkage broken: before.next=%d anchor.uid=%d anchor.last=%d", nodes[0].NextUID(), nodes[1].UID(), nodes[1].LastUID())
|
||||
}
|
||||
if nodes[1].NextUID() != nodes[2].UID() || nodes[2].LastUID() != nodes[1].UID() {
|
||||
t.Fatalf("anchor/after linkage broken: anchor.next=%d after.uid=%d after.last=%d", nodes[1].NextUID(), nodes[2].UID(), nodes[2].LastUID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertNodeByDataRejectsNilAnchor(t *testing.T) {
|
||||
h := NewHosts()
|
||||
path := filepath.Join(t.TempDir(), "hosts.insert.nil")
|
||||
if err := os.WriteFile(path, []byte("2.2.2.2 anchor.test\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.Parse(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.InsertNodeByData(nil, true, "before", "1.1.1.1", "before.test"); err == nil {
|
||||
t.Fatal("expected nil anchor error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetIPHostsUpdatesReverseIndex(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.SetIPHosts("1.2.3.4", "new.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := h.ListIPsByHost("new.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
||||
t.Fatalf("expected new reverse index, got %v", got)
|
||||
}
|
||||
if got := h.ListIPsByHost("old.test"); len(got) != 0 {
|
||||
t.Fatalf("expected old reverse index to be removed, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetIPHostsDeduplicatesHosts(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.SetIPHosts("1.2.3.4", "new.test", "new.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "new.test" {
|
||||
t.Fatalf("expected deduplicated ip mapping, got %v", got)
|
||||
}
|
||||
if got := h.ListIPsByHost("new.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
||||
t.Fatalf("expected deduplicated reverse index, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetIPHostsReplacesMultipleSameIPNodes(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.AddHosts("5.6.7.8", "tail.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.SetIPHosts("1.2.3.4", "new.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "new.test" {
|
||||
t.Fatalf("expected replaced same-ip mappings, got %v", got)
|
||||
}
|
||||
if got := h.ListIPsByHost("first.test"); len(got) != 0 {
|
||||
t.Fatalf("expected first old host to disappear, got %v", got)
|
||||
}
|
||||
if got := h.ListIPsByHost("second.test"); len(got) != 0 {
|
||||
t.Fatalf("expected second old host to disappear, got %v", got)
|
||||
}
|
||||
if got := h.ListFirstIPByHost("tail.test"); got != "5.6.7.8" {
|
||||
t.Fatalf("expected tail node to remain linked, got %q", got)
|
||||
}
|
||||
nodes := h.List()
|
||||
if len(nodes) != 2 || nodes[0].IP() != "5.6.7.8" || nodes[1].IP() != "1.2.3.4" {
|
||||
t.Fatalf("unexpected node list after SetIPHosts: %#v", nodes)
|
||||
}
|
||||
if nodes[0].NextUID() != nodes[1].UID() || nodes[1].LastUID() != nodes[0].UID() {
|
||||
t.Fatalf("remaining node linkage broken: first.next=%d second.uid=%d second.last=%d", nodes[0].NextUID(), nodes[1].UID(), nodes[1].LastUID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetHostIPsReplacesMappingsInOneOperation(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.SetHostIPs("old.test", "2.2.2.2", "3.3.3.3"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := h.ListIPsByHost("old.test"); len(got) != 2 || got[0] != "2.2.2.2" || got[1] != "3.3.3.3" {
|
||||
t.Fatalf("expected replaced host ip mappings, got %v", got)
|
||||
}
|
||||
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 0 {
|
||||
t.Fatalf("expected old ip mapping to be removed, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetIPHostsRejectsInvalidInputWithoutMutating(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
hosts []string
|
||||
}{
|
||||
{name: "bad ip", ip: "bad-ip", hosts: []string{"new.test"}},
|
||||
{name: "empty host", ip: "1.2.3.4", hosts: []string{""}},
|
||||
{name: "comment host", ip: "1.2.3.4", hosts: []string{"#bad.test"}},
|
||||
{name: "missing host", ip: "1.2.3.4"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.SetIPHosts(tt.ip, tt.hosts...); err == nil {
|
||||
t.Fatal("expected invalid SetIPHosts input to fail")
|
||||
}
|
||||
if got := h.ListIPsByHost("old.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
||||
t.Fatalf("old host mapping should remain after failed SetIPHosts, got %v", got)
|
||||
}
|
||||
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "old.test" {
|
||||
t.Fatalf("ip index should remain after failed SetIPHosts, got %v", got)
|
||||
}
|
||||
if got := h.ListIPsByHost("new.test"); len(got) != 0 {
|
||||
t.Fatalf("failed SetIPHosts should not add new host, got %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetHostIPsRejectsInvalidInputWithoutMutating(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
ips []string
|
||||
}{
|
||||
{name: "empty host", host: "", ips: []string{"2.2.2.2"}},
|
||||
{name: "comment host", host: "#old.test", ips: []string{"2.2.2.2"}},
|
||||
{name: "bad ip", host: "old.test", ips: []string{"2.2.2.2", "bad-ip"}},
|
||||
{name: "missing ip", host: "old.test"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.SetHostIPs(tt.host, tt.ips...); err == nil {
|
||||
t.Fatal("expected invalid SetHostIPs input to fail")
|
||||
}
|
||||
if got := h.ListIPsByHost("old.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
||||
t.Fatalf("old host mapping should remain after failed SetHostIPs, got %v", got)
|
||||
}
|
||||
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "old.test" {
|
||||
t.Fatalf("ip index should remain after failed SetHostIPs, got %v", got)
|
||||
}
|
||||
if got := h.ListHostsByIP("2.2.2.2"); len(got) != 0 {
|
||||
t.Fatalf("failed SetHostIPs should not add partial ip mapping, got %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveIPHostsKeepsSameIPOtherNodes(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.RemoveIPHosts("1.2.3.4", "first.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := h.ListIPsByHost("first.test"); len(got) != 0 {
|
||||
t.Fatalf("expected removed host to disappear, got %v", got)
|
||||
}
|
||||
if got := h.ListIPsByHost("second.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
||||
t.Fatalf("expected same-ip sibling node to stay indexed, got %v", got)
|
||||
}
|
||||
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "second.test" {
|
||||
t.Fatalf("expected ip index to keep sibling host, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveHostsKeepsSameIPOtherNodes(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.RemoveHosts("first.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "second.test" {
|
||||
t.Fatalf("expected ip index to keep sibling host after RemoveHosts, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveIPsUnlinksAdjacentNodes(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.AddHosts("5.6.7.8", "tail.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.RemoveIPs("1.2.3.4"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nodes := h.List()
|
||||
if len(nodes) != 1 || nodes[0].IP() != "5.6.7.8" || nodes[0].LastUID() != 0 || nodes[0].NextUID() != 0 {
|
||||
t.Fatalf("expected only tail node with clean links, got %#v", nodes)
|
||||
}
|
||||
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 0 {
|
||||
t.Fatalf("expected removed ip index to be empty, got %v", got)
|
||||
}
|
||||
if got := h.ListFirstIPByHost("tail.test"); got != "5.6.7.8" {
|
||||
t.Fatalf("expected tail reverse index to remain, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddHostsRejectsInvalidInput(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("not-an-ip", "bad.test"); err == nil {
|
||||
t.Fatal("expected invalid ip error")
|
||||
}
|
||||
if got := h.ListHostsByIP("not-an-ip"); len(got) != 0 {
|
||||
t.Fatalf("invalid ip should not be indexed, got %v", got)
|
||||
}
|
||||
if err := h.AddHosts("1.2.3.4", ""); err == nil {
|
||||
t.Fatal("expected empty host error")
|
||||
}
|
||||
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 0 {
|
||||
t.Fatalf("empty host should not be indexed, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertNodeByDataRejectsInvalidHostDataWithoutMutating(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("2.2.2.2", "anchor.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
anchor, err := h.GetFirstNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
hosts []string
|
||||
}{
|
||||
{name: "bad ip", ip: "not-an-ip", hosts: []string{"bad.test"}},
|
||||
{name: "empty host", ip: "1.1.1.1", hosts: []string{""}},
|
||||
{name: "comment host", ip: "1.1.1.1", hosts: []string{"#bad.test"}},
|
||||
{name: "missing host", ip: "1.1.1.1"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if err := h.InsertNodeByData(anchor, false, "", tt.ip, tt.hosts...); err == nil {
|
||||
t.Fatalf("%s: expected error", tt.name)
|
||||
}
|
||||
}
|
||||
nodes := h.List()
|
||||
if len(nodes) != 1 || nodes[0].IP() != "2.2.2.2" {
|
||||
t.Fatalf("invalid insert should not mutate node list: %#v", nodes)
|
||||
}
|
||||
if got := h.ListHostsByIP("1.1.1.1"); len(got) != 0 {
|
||||
t.Fatalf("invalid insert should not mutate ip index: %v", got)
|
||||
}
|
||||
|
||||
if err := h.InsertNodeByData(anchor, true, "comment-only", ""); err != nil {
|
||||
t.Fatalf("comment-only insert should remain valid: %v", err)
|
||||
}
|
||||
nodes = h.List()
|
||||
if len(nodes) != 2 || !nodes[0].OnlyComment() || nodes[1].IP() != "2.2.2.2" {
|
||||
t.Fatalf("comment-only insert mismatch: %#v", nodes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertNodeByDataRejectsEmptyNodeWithoutMutating(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("2.2.2.2", "anchor.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
anchor, err := h.GetFirstNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := h.InsertNodeByData(anchor, true, "", ""); err == nil {
|
||||
t.Fatal("expected empty insert to fail")
|
||||
}
|
||||
nodes := h.List()
|
||||
if len(nodes) != 1 || nodes[0].IP() != "2.2.2.2" {
|
||||
t.Fatalf("empty insert should not mutate node list: %#v", nodes)
|
||||
}
|
||||
out, err := h.Build()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := string(out), "2.2.2.2 anchor.test"+lineBreaker; got != want {
|
||||
t.Fatalf("empty insert should not change output: got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyHostsBuildAndSaveAs(t *testing.T) {
|
||||
h := NewHosts()
|
||||
path := filepath.Join(t.TempDir(), "hosts.empty")
|
||||
if err := os.WriteFile(path, nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.Parse(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := h.Build()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(out) != 0 {
|
||||
t.Fatalf("expected empty build output, got %q", out)
|
||||
}
|
||||
outPath := filepath.Join(t.TempDir(), "hosts.out")
|
||||
if err := h.SaveAs(outPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved, err := os.ReadFile(outPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(saved) != 0 {
|
||||
t.Fatalf("expected empty saved file, got %q", saved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePreservesBlankAndRawLines(t *testing.T) {
|
||||
h := NewHosts()
|
||||
path := filepath.Join(t.TempDir(), "hosts.raw")
|
||||
input := []byte("127.0.0.1 localhost\n\nbadline\n# tail comment\n")
|
||||
if err := os.WriteFile(path, input, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.Parse(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := h.Build()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(out)
|
||||
if !strings.Contains(got, "127.0.0.1 localhost"+lineBreaker+lineBreaker+"badline"+lineBreaker) {
|
||||
t.Fatalf("expected blank/raw lines to be preserved, got %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "# tail comment"+lineBreaker) {
|
||||
t.Fatalf("expected comment line to be preserved, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostAccessorsReturnDetachedCopies(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node, err := h.GetFirstNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node.SetIP("9.9.9.9")
|
||||
node.SetHosts("mutated.test")
|
||||
|
||||
if got := h.ListFirstIPByHost("example.test"); got != "1.2.3.4" {
|
||||
t.Fatalf("detached copy mutated internal host index: %q", got)
|
||||
}
|
||||
if got := h.ListFirstIPByHost("mutated.test"); got != "" {
|
||||
t.Fatalf("detached copy should not create new host index: %q", got)
|
||||
}
|
||||
out, err := h.Build()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(out), "9.9.9.9 mutated.test") {
|
||||
t.Fatalf("detached copy leaked into build output: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNodeRejectsInvalidMutationAndPreservesState(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node, err := h.GetFirstNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node.SetIP("bad-ip")
|
||||
if err := h.UpdateNode(node); err == nil {
|
||||
t.Fatal("expected invalid update to fail")
|
||||
}
|
||||
if got := h.ListFirstIPByHost("example.test"); got != "1.2.3.4" {
|
||||
t.Fatalf("failed update should preserve previous index, got %q", got)
|
||||
}
|
||||
out, err := h.Build()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(out), "1.2.3.4 example.test") || strings.Contains(string(out), "bad-ip") {
|
||||
t.Fatalf("failed update should preserve previous output, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNodeCommentOnlyStateReindexesCleanly(t *testing.T) {
|
||||
h := NewHosts()
|
||||
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node, err := h.GetFirstNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node.SetIP("")
|
||||
node.SetHosts()
|
||||
node.SetComment("note")
|
||||
if err := h.UpdateNode(node); err != nil {
|
||||
t.Fatalf("comment-only update failed: %v", err)
|
||||
}
|
||||
if got := h.ListByIP(""); len(got) != 0 {
|
||||
t.Fatalf("comment-only node should not be indexed under empty ip: %#v", got)
|
||||
}
|
||||
updated, err := h.GetNode(node.UID())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !updated.OnlyComment() {
|
||||
t.Fatalf("comment-only node should keep onlyComment state: %#v", updated)
|
||||
}
|
||||
node = updated
|
||||
node.SetIP("2.2.2.2")
|
||||
node.SetHosts("restored.test")
|
||||
if err := h.UpdateNode(node); err != nil {
|
||||
t.Fatalf("restoring host entry failed: %v", err)
|
||||
}
|
||||
updated, err = h.GetNode(node.UID())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if updated.OnlyComment() {
|
||||
t.Fatalf("host entry should clear onlyComment after restore: %#v", updated)
|
||||
}
|
||||
if got := h.ListFirstIPByHost("restored.test"); got != "2.2.2.2" {
|
||||
t.Fatalf("restored host index mismatch: %q", got)
|
||||
}
|
||||
if got := h.ListByIP(""); len(got) != 0 {
|
||||
t.Fatalf("restored host should still avoid empty ip index: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user