Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ This CLI follows [Semantic Versioning](https://semver.org/) and targets the
stable `chunkdb` 1.x protocol; see the engine's
[compatibility policy](https://github.com/chunkdb/chunkdb/blob/main/docs/COMPATIBILITY.md).

## Unreleased

### Added
- `chunksetbin` and `chunksetbinstate`: binary chunk writes over the new
`CHUNKSETBIN` command (chunkdb server 1.3+). The payload is given as hex or
read from a file with `--in`, in the byte layouts `chunkbin` /
`chunkbinstate` print, so `chunkbin --out` output can be written back as is.
Also available in `shell`

## 1.1.0 - 2026-07-18

### Added
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Targets the stable `chunkdb` 1.x protocol; see the engine's
- `chunkset`
- `chunkstate`
- `chunksetstate`
- `chunksetbin`
- `chunksetbinstate`
- `chunk`
- `chunkbin`
- `chunkbinstate`
Expand Down Expand Up @@ -82,6 +84,8 @@ chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunksetstate 0 0 <payload_bits>
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunk 0 0
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunkbin 0 0
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunkbinstate 0 0
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunksetbin 0 0 <hex_payload_bytes>
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunksetbinstate --in state.bin 0 0
```

Block-state note:
Expand All @@ -99,6 +103,7 @@ Chunk-state note:
- `chunksetstate <cx> <cy> <payload_bits>|<presence_bits>` writes mixed present/absent block state
- `chunkbinstate <cx> <cy>` prints exact chunk-state bytes as `[payload_bytes][presence_bytes]`
- `chunkbinc <cx> <cy>` / `chunkbincstate <cx> <cy>` fetch the same bytes as `chunkbin`/`chunkbinstate` over the compressed `CHUNKBINC` transfer and decompress client-side; pass `--raw` to keep the compressed payload
- `chunksetbin <cx> <cy> <hex>` / `chunksetbinstate <cx> <cy> <hex>` write raw chunk bytes in the layouts `chunkbin`/`chunkbinstate` print; `--in <file>` reads the bytes from a file (for example one written by `chunkbin --out`). Requires chunkdb server 1.3+
- `chunkradius <cx> <cy> <radius_chunks>` reads populated chunks within a chunk-space radius (disc), like `chunkrange` but circular

## Interactive Shell
Expand All @@ -124,6 +129,8 @@ The shell prompt is `chunk>`. Supported shell commands:
- `chunkset <cx> <cy> <bits>`
- `chunkstate <cx> <cy>`
- `chunksetstate <cx> <cy> <payload_bits>|<presence_bits>`
- `chunksetbin <cx> <cy> <hex> | --in <file>`
- `chunksetbinstate <cx> <cy> <hex> | --in <file>`
- `chunk <cx> <cy>`
- `chunkbin [--out <file>] <cx> <cy>`
- `chunkbinstate [--out <file>] <cx> <cy>`
Expand Down Expand Up @@ -233,6 +240,8 @@ Auth behavior:
- `chunkstate <cx> <cy>`
- sends `CHUNK ... STATE`; prints `<payload_bits>|<presence_bits>`
- `chunksetstate <cx> <cy> <payload_bits>|<presence_bits>`
- `chunksetbin <cx> <cy> <hex> | --in <file>`
- `chunksetbinstate <cx> <cy> <hex> | --in <file>`
- sends `CHUNKSET ... STATE`; validates both halves as binary before request
- `chunk <cx> <cy>`
- sends `CHUNK`, prints text chunk payload
Expand Down
93 changes: 92 additions & 1 deletion cmd/chunk-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func main() {
case "help", "-h", "--help":
printUsage()
return
case "ping", "info", "auth", "get", "exists", "set", "unset", "mset", "mget", "chunkexists", "chunkset", "chunkstate", "chunksetstate", "chunk", "chunkbin", "chunkbinstate", "chunkbinc", "chunkbincstate", "chunkscan", "chunkrange", "chunkradius", "chunkver", "chunkcas", "chunkbatch", "walflush", "metrics", "shell":
case "ping", "info", "auth", "get", "exists", "set", "unset", "mset", "mget", "chunkexists", "chunkset", "chunkstate", "chunksetstate", "chunksetbin", "chunksetbinstate", "chunk", "chunkbin", "chunkbinstate", "chunkbinc", "chunkbincstate", "chunkscan", "chunkrange", "chunkradius", "chunkver", "chunkcas", "chunkbatch", "walflush", "metrics", "shell":
// network command
default:
fatal(fmt.Errorf("unknown command %q", cmd))
Expand Down Expand Up @@ -189,6 +189,14 @@ func main() {
fatal(err)
}
printTextPayload(os.Stdout, payload)
case "chunksetbin":
if err := runChunkSetBin(client, false, "chunksetbin", cmdArgs, os.Stdout, os.Stderr); err != nil {
fatal(err)
}
case "chunksetbinstate":
if err := runChunkSetBin(client, true, "chunksetbinstate", cmdArgs, os.Stdout, os.Stderr); err != nil {
fatal(err)
}
case "chunkbin":
if err := runChunkBinLike(client, "CHUNKBIN", false, "chunkbin", cmdArgs, os.Stdout, os.Stderr); err != nil {
fatal(err)
Expand Down Expand Up @@ -411,6 +419,12 @@ func validateCommandArgs(cmd string, cmdArgs []string) error {
if err := validateIntArg(cmdArgs[1], "cy"); err != nil {
return err
}
case "chunksetbin":
_, _, _, err := parseChunkSetBinArgs("chunksetbin", cmdArgs, stderrDiscard{})
return err
case "chunksetbinstate":
_, _, _, err := parseChunkSetBinArgs("chunksetbinstate", cmdArgs, stderrDiscard{})
return err
case "chunkbin":
return validateChunkBinLikeArgs("chunkbin", cmdArgs, stderrDiscard{})
case "chunkbinstate":
Expand Down Expand Up @@ -681,6 +695,73 @@ func runChunkBinLike(client *chunkclient.Client, command string, state bool, nam
return nil
}

// parseChunkSetBinArgs validates "<cx> <cy> <hex>" or "--in <file> <cx> <cy>"
// and returns the coordinates plus the raw payload bytes (nil when the payload
// is only known at run time from --in).
func parseChunkSetBinArgs(name string, cmdArgs []string, stderr io.Writer) (cx string, cy string, payload []byte, err error) {
fs := flag.NewFlagSet(name, flag.ContinueOnError)
fs.SetOutput(stderr)
inPath := fs.String("in", "", "read the raw payload bytes from file")
if err := fs.Parse(cmdArgs); err != nil {
return "", "", nil, err
}
remaining := fs.Args()
usage := fmt.Errorf("usage: %s <cx> <cy> <hex> | %s --in <file> <cx> <cy>", name, name)
switch {
case *inPath != "" && len(remaining) == 2:
case *inPath == "" && len(remaining) == 3:
default:
return "", "", nil, usage
}
cx, cy = remaining[0], remaining[1]
if err := validateIntArg(cx, "cx"); err != nil {
return "", "", nil, err
}
if err := validateIntArg(cy, "cy"); err != nil {
return "", "", nil, err
}
if *inPath != "" {
data, err := os.ReadFile(*inPath)
if err != nil {
return "", "", nil, fmt.Errorf("read %s: %w", *inPath, err)
}
return cx, cy, data, nil
}
data, err := hex.DecodeString(remaining[2])
if err != nil {
return "", "", nil, fmt.Errorf("payload must be hex: %w", err)
}
if len(data) == 0 {
return "", "", nil, fmt.Errorf("payload must not be empty")
}
return cx, cy, data, nil
}

// runChunkSetBin sends CHUNKSETBIN [STATE] with the raw payload bytes taken
// from a hex argument or a file; the byte layout is what chunkbin /
// chunkbinstate print.
func runChunkSetBin(client *chunkclient.Client, state bool, name string, cmdArgs []string, stdout io.Writer, stderr io.Writer) error {
cx, cy, payload, err := parseChunkSetBinArgs(name, cmdArgs, stderr)
if err != nil {
return err
}
request := fmt.Sprintf("CHUNKSETBIN %s %s", cx, cy)
if state {
request += " STATE"
}
request += fmt.Sprintf(" %d", len(payload))

resp, err := client.CommandWithPayload(request, payload)
if err != nil {
return fmt.Errorf("%s failed: %w", name, err)
}
if resp.Kind != chunkclient.ResponseSimple {
return fmt.Errorf("%s failed: expected simple response", name)
}
fmt.Fprintln(stdout, resp.Simple)
return nil
}

// runChunkBinCompressed sends CHUNKBINC, decompresses the zrle payload
// client-side, and presents the same decompressed bytes as chunkbin so
// compressed reads are usable without a separate tool.
Expand Down Expand Up @@ -940,6 +1021,14 @@ func runShell(
if err := runChunkBinLike(client, "CHUNKBIN", false, "chunkbin", cmdArgs, stdout, stderr); err != nil {
fmt.Fprintf(stderr, "error: %v\n", err)
}
case "chunksetbin":
if err := runChunkSetBin(client, false, "chunksetbin", cmdArgs, stdout, stderr); err != nil {
fmt.Fprintf(stderr, "error: %v\n", err)
}
case "chunksetbinstate":
if err := runChunkSetBin(client, true, "chunksetbinstate", cmdArgs, stdout, stderr); err != nil {
fmt.Fprintf(stderr, "error: %v\n", err)
}
case "chunkstate":
if err := validateCommandArgs(cmd, cmdArgs); err != nil {
fmt.Fprintf(stderr, "error: %v\n", err)
Expand Down Expand Up @@ -1126,6 +1215,8 @@ Commands:
chunk <cx> <cy>
chunkbin [--out <file>] <cx> <cy>
chunkbinstate [--out <file>] <cx> <cy>
chunksetbin <cx> <cy> <hex> | --in <file>
chunksetbinstate <cx> <cy> <hex> | --in <file>
chunkbinc [--out <file>] [--raw] <cx> <cy>
chunkbincstate [--out <file>] [--raw] <cx> <cy>
chunkscan <limit> [<cursor_cx> <cursor_cy>]
Expand Down
6 changes: 6 additions & 0 deletions cmd/chunk-cli/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ func TestValidateCommandArgs(t *testing.T) {
{name: "chunk ok", cmd: "chunk", args: []string{"0", "0"}, wantErr: false},
{name: "auth too many", cmd: "auth", args: []string{"a", "b"}, wantErr: true},
{name: "chunkbin passthrough", cmd: "chunkbin", args: []string{"--out", "x", "1", "2"}, wantErr: false},
{name: "chunksetbin hex ok", cmd: "chunksetbin", args: []string{"1", "2", "a0a1"}, wantErr: false},
{name: "chunksetbin bad hex", cmd: "chunksetbin", args: []string{"1", "2", "zz"}, wantErr: true},
{name: "chunksetbin bad int", cmd: "chunksetbin", args: []string{"x", "2", "a0"}, wantErr: true},
{name: "chunksetbin missing payload", cmd: "chunksetbin", args: []string{"1", "2"}, wantErr: true},
{name: "chunksetbin in and hex", cmd: "chunksetbin", args: []string{"--in", "x", "1", "2", "a0"}, wantErr: true},
{name: "chunksetbinstate hex ok", cmd: "chunksetbinstate", args: []string{"1", "2", "a0a1ff"}, wantErr: false},
{name: "shell ok", cmd: "shell", args: nil, wantErr: false},
{name: "shell extra", cmd: "shell", args: []string{"ping"}, wantErr: true},
{name: "chunkscan ok", cmd: "chunkscan", args: []string{"10"}, wantErr: false},
Expand Down
14 changes: 14 additions & 0 deletions internal/chunkclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ func (c *Client) Close() error {
}

func (c *Client) Command(command string) (Response, error) {
return c.CommandWithPayload(command, nil)
}

// CommandWithPayload sends a request line followed by raw payload bytes and an
// empty line (the CHUNKSETBIN framing). A nil payload sends the line alone.
func (c *Client) CommandWithPayload(command string, payload []byte) (Response, error) {
if c.conn == nil {
return Response{}, fmt.Errorf("connection is closed")
}
Expand All @@ -114,6 +120,14 @@ func (c *Client) Command(command string) (Response, error) {
if _, err := c.writer.WriteString(command + "\r\n"); err != nil {
return Response{}, fmt.Errorf("write command: %w", err)
}
if payload != nil {
if _, err := c.writer.Write(payload); err != nil {
return Response{}, fmt.Errorf("write payload: %w", err)
}
if _, err := c.writer.WriteString("\r\n"); err != nil {
return Response{}, fmt.Errorf("write payload terminator: %w", err)
}
}
if err := c.writer.Flush(); err != nil {
return Response{}, fmt.Errorf("flush command: %w", err)
}
Expand Down