// Package patch implements the simple Leviathan patch script language. // // set OFFSET HEX... — write hex bytes at OFFSET // fill START LEN BYTE — fill a range with a single byte // xor START LEN KEY — XOR a range with an ASCII or hex: key // copy SRC LEN DST — copy bytes from SRC to DST // # comments and blank lines are allowed. package patch import ( "encoding/hex" "errors" "fmt" "strconv" "strings" "github.com/andydixon/nexus/internal/leviathan/document" ) // Step is one parsed instruction. type Step struct { Op string Args []string Line int Source string } // Parse parses the script into steps. func Parse(src string) ([]Step, error) { out := []Step{} for i, raw := range strings.Split(src, "\n") { line := strings.TrimSpace(raw) if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { continue } fields := strings.Fields(line) op := strings.ToLower(fields[0]) out = append(out, Step{ Op: op, Args: fields[1:], Line: i + 1, Source: raw, }) } return out, nil } // Plan validates all steps against document size without applying. // Returns per-step info or an error on validation failure. func Plan(d *document.Document, steps []Step) ([]map[string]any, error) { size := d.Size() info := make([]map[string]any, 0, len(steps)) for _, s := range steps { switch s.Op { case "set": if len(s.Args) < 2 { return info, fmt.Errorf("line %d: set needs OFFSET and HEX", s.Line) } off, err := parseOffset(s.Args[0]) if err != nil { return info, fmt.Errorf("line %d: %v", s.Line, err) } b, err := parseHexRest(s.Args[1:]) if err != nil { return info, fmt.Errorf("line %d: %v", s.Line, err) } if !validRange(size, off, int64(len(b))) { return info, fmt.Errorf("line %d: out of range", s.Line) } info = append(info, map[string]any{ "op": "set", "line": s.Line, "offset": off, "length": len(b), }) case "fill": if len(s.Args) != 3 { return info, fmt.Errorf("line %d: fill needs START LEN BYTE", s.Line) } off, err := parseOffset(s.Args[0]) if err != nil { return info, fmt.Errorf("line %d: %v", s.Line, err) } n, err := parseOffset(s.Args[1]) if err != nil { return info, fmt.Errorf("line %d: %v", s.Line, err) } by, err := parseHexByte(s.Args[2]) if err != nil { return info, fmt.Errorf("line %d: %v", s.Line, err) } if !validRange(size, off, n) { return info, fmt.Errorf("line %d: out of range", s.Line) } info = append(info, map[string]any{ "op": "fill", "line": s.Line, "offset": off, "length": n, "byte": by, }) case "xor": if len(s.Args) != 3 { return info, fmt.Errorf("line %d: xor needs START LEN KEY", s.Line) } off, err := parseOffset(s.Args[0]) if err != nil { return info, fmt.Errorf("line %d: %v", s.Line, err) } n, err := parseOffset(s.Args[1]) if err != nil { return info, fmt.Errorf("line %d: %v", s.Line, err) } key, err := parseKey(s.Args[2]) if err != nil { return info, fmt.Errorf("line %d: %v", s.Line, err) } if !validRange(size, off, n) { return info, fmt.Errorf("line %d: out of range", s.Line) } info = append(info, map[string]any{ "op": "xor", "line": s.Line, "offset": off, "length": n, "keyLen": len(key), }) case "copy": if len(s.Args) != 3 { return info, fmt.Errorf("line %d: copy needs SRC LEN DST", s.Line) } src, err := parseOffset(s.Args[0]) if err != nil { return info, fmt.Errorf("line %d: %v", s.Line, err) } n, err := parseOffset(s.Args[1]) if err != nil { return info, fmt.Errorf("line %d: %v", s.Line, err) } dst, err := parseOffset(s.Args[2]) if err != nil { return info, fmt.Errorf("line %d: %v", s.Line, err) } if !validRange(size, src, n) || !validRange(size, dst, n) { return info, fmt.Errorf("line %d: out of range", s.Line) } info = append(info, map[string]any{ "op": "copy", "line": s.Line, "src": src, "dst": dst, "length": n, }) default: return info, fmt.Errorf("line %d: unknown op %q", s.Line, s.Op) } } return info, nil } // Apply runs the script against the document. Callers may want to // first snapshot a checkpoint so apply can be reverted. func Apply(d *document.Document, steps []Step) error { if _, err := Plan(d, steps); err != nil { return err } for _, s := range steps { switch s.Op { case "set": off, _ := parseOffset(s.Args[0]) b, _ := parseHexRest(s.Args[1:]) for i, bb := range b { if _, err := d.Set(off+int64(i), bb); err != nil { return err } } case "fill": off, _ := parseOffset(s.Args[0]) n, _ := parseOffset(s.Args[1]) by, _ := parseHexByte(s.Args[2]) if err := d.Fill(off, n, by); err != nil { return err } case "xor": off, _ := parseOffset(s.Args[0]) n, _ := parseOffset(s.Args[1]) key, _ := parseKey(s.Args[2]) if err := d.XOR(off, n, key); err != nil { return err } case "copy": src, _ := parseOffset(s.Args[0]) n, _ := parseOffset(s.Args[1]) dst, _ := parseOffset(s.Args[2]) if err := d.Copy(src, n, dst); err != nil { return err } } } return nil } func validRange(size, start, length int64) bool { return start >= 0 && start <= size && length >= 0 && length <= size-start } func parseOffset(s string) (int64, error) { s = strings.TrimSpace(s) if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { v, err := strconv.ParseInt(s[2:], 16, 64) return v, err } return strconv.ParseInt(s, 10, 64) } func parseHexRest(args []string) ([]byte, error) { joined := strings.Join(args, "") joined = strings.ReplaceAll(joined, " ", "") joined = strings.ReplaceAll(joined, ",", "") return hex.DecodeString(joined) } func parseHexByte(s string) (byte, error) { if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { v, err := strconv.ParseUint(s[2:], 16, 8) return byte(v), err } b, err := hex.DecodeString(s) if err != nil { v, err := strconv.ParseUint(s, 10, 8) if err != nil { return 0, errors.New("not a byte") } return byte(v), nil } if len(b) != 1 { return 0, errors.New("expected one byte") } return b[0], nil } func parseKey(s string) ([]byte, error) { if strings.HasPrefix(s, "hex:") { return hex.DecodeString(s[4:]) } if s == "" { return nil, errors.New("empty key") } return []byte(s), nil }