package main import ( "encoding/json" "regexp" "strconv" "strings" ) // loadJSON parses smartctl -j output. If leading noise precedes the object // (rare, but some controllers emit warnings before the JSON), it retries from // the first '{'. func loadJSON(raw string) map[string]interface{} { raw = strings.TrimSpace(raw) if raw == "" { return nil } var m map[string]interface{} if err := json.Unmarshal([]byte(raw), &m); err == nil { return m } if i := strings.IndexByte(raw, '{'); i >= 0 { if err := json.Unmarshal([]byte(raw[i:]), &m); err == nil { return m } } return nil } // jObj navigates nested maps by key path, returning the leaf map or nil. func jObj(m map[string]interface{}, keys ...string) map[string]interface{} { cur := m for _, k := range keys { if cur == nil { return nil } v, ok := cur[k].(map[string]interface{}) if !ok { return nil } cur = v } return cur } // jInt returns an *int for a numeric leaf (JSON numbers decode as float64). func jInt(m map[string]interface{}, keys ...string) *int { v := jLeaf(m, keys...) switch t := v.(type) { case float64: n := int(t) return &n case string: if n, err := strconv.Atoi(strings.TrimSpace(t)); err == nil { return &n } } return nil } // jStr returns a trimmed string leaf, or "". func jStr(m map[string]interface{}, keys ...string) string { if s, ok := jLeaf(m, keys...).(string); ok { return strings.TrimSpace(s) } return "" } // jBoolPtr returns *bool for a boolean leaf. func jBoolPtr(m map[string]interface{}, keys ...string) *bool { if b, ok := jLeaf(m, keys...).(bool); ok { return &b } return nil } // jLeaf returns the raw value at the key path (the final key looked up in its // parent map), or nil when any segment along the path is missing. func jLeaf(m map[string]interface{}, keys ...string) interface{} { if len(keys) == 0 { return nil } parent := jObj(m, keys[:len(keys)-1]...) if parent == nil { return nil } return parent[keys[len(keys)-1]] } var leadingInt = regexp.MustCompile(`^\s*(\d+)`) // firstInt extracts the leading run of digits from a string ("345 hours" -> 345). // It stops at the first non-digit, so for comma-grouped numbers ("12,345") use // parseIntLoose, which strips separators first. func firstInt(s string) (int, bool) { m := leadingInt.FindStringSubmatch(s) if m == nil { return 0, false } n, err := strconv.Atoi(m[1]) return n, err == nil } // parseIntLoose strips commas/spaces and parses an integer anywhere in s. func parseIntLoose(s string) (int, bool) { s = strings.TrimSpace(strings.ReplaceAll(s, ",", "")) // Take the leading run of digits (and optional sign). m := regexp.MustCompile(`-?\d+`).FindString(s) if m == "" { return 0, false } n, err := strconv.Atoi(m) return n, err == nil }