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:
2026-06-09 18:10:19 +08:00
parent 0b6373e9e3
commit d93a851d1b
44 changed files with 9774 additions and 2260 deletions
+487 -256
View File
@@ -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
View File
@@ -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)
}
}