Skip to content

Commit 22a2062

Browse files
committed
feat(cli): add chunk-level presence commands
1 parent 45b200d commit 22a2062

4 files changed

Lines changed: 172 additions & 8 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ It provides direct terminal access to the chunk protocol for operational checks,
1919
- `exists`
2020
- `set`
2121
- `unset`
22+
- `chunkexists`
23+
- `chunkset`
2224
- `chunk`
2325
- `chunkbin`
2426
- `shell`
@@ -55,6 +57,8 @@ chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ get 0 0
5557
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ exists 0 0
5658
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ set 0 0 10110011
5759
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ unset 0 0
60+
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunkexists 0 0
61+
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunkset 0 0 <full_chunk_bits>
5862
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunk 0 0
5963
chunk-cli --uri chunk://mytoken@127.0.0.1:4242/ chunkbin 0 0
6064
```
@@ -65,6 +69,12 @@ Block-state note:
6569
- `exists <x> <y>` prints `1` when the block is explicitly present, `0` when it is unset
6670
- `set <x> <y> 000...0` is distinct from `unset <x> <y>`
6771

72+
Chunk-state note:
73+
74+
- `chunk <cx> <cy>` still prints zero bits for an absent chunk
75+
- `chunkexists <cx> <cy>` prints `1` when any explicit chunk presence exists, `0` when the chunk is unset/absent
76+
- `chunkset <cx> <cy> 000...0` is distinct from an absent chunk, but `<bits>` must be a full chunk-sized payload
77+
6878
## Interactive Shell
6979

7080
Start the interactive shell:
@@ -82,6 +92,8 @@ The shell prompt is `chunk>`. Supported shell commands:
8292
- `exists <x> <y>`
8393
- `set <x> <y> <bits>`
8494
- `unset <x> <y>`
95+
- `chunkexists <cx> <cy>`
96+
- `chunkset <cx> <cy> <bits>`
8597
- `chunk <cx> <cy>`
8698
- `chunkbin [--out <file>] <cx> <cy>`
8799
- `quit`
@@ -102,6 +114,14 @@ chunk> get 0 0
102114
1111000011110000
103115
chunk> unset 0 0
104116
OK
117+
chunk> chunkexists 0 0
118+
0
119+
chunk> chunkset 0 0 <full_chunk_bits>
120+
OK
121+
chunk> chunkexists 0 0
122+
1
123+
chunk> chunk 0 0
124+
<full_chunk_bits>
105125
chunk> get 0 0
106126
0000000000000000
107127
chunk> info
@@ -153,6 +173,10 @@ Auth behavior:
153173
- sends `SET`; validates `bits` as binary (`0`/`1`) before request
154174
- `unset <x> <y>`
155175
- sends `UNSET`, clears explicit block presence, prints simple response
176+
- `chunkexists <cx> <cy>`
177+
- sends `CHUNKEXISTS`, prints `1` when the chunk has explicit presence and `0` when absent
178+
- `chunkset <cx> <cy> <bits>`
179+
- sends `CHUNKSET`; validates `bits` as binary (`0`/`1`) before request
156180
- `chunk <cx> <cy>`
157181
- sends `CHUNK`, prints text chunk payload
158182
- `chunkbin [--out <file>] <cx> <cy>`

cmd/chunk-cli/main.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func main() {
5151
case "help", "-h", "--help":
5252
printUsage()
5353
return
54-
case "ping", "info", "auth", "get", "exists", "set", "unset", "chunk", "chunkbin", "shell":
54+
case "ping", "info", "auth", "get", "exists", "set", "unset", "chunkexists", "chunkset", "chunk", "chunkbin", "shell":
5555
// network command
5656
default:
5757
fatal(fmt.Errorf("unknown command %q", cmd))
@@ -143,6 +143,18 @@ func main() {
143143
fatal(err)
144144
}
145145
fmt.Println(text)
146+
case "chunkexists":
147+
text, err := runSimple(client, fmt.Sprintf("CHUNKEXISTS %s %s", cmdArgs[0], cmdArgs[1]))
148+
if err != nil {
149+
fatal(err)
150+
}
151+
fmt.Println(text)
152+
case "chunkset":
153+
text, err := runSimple(client, fmt.Sprintf("CHUNKSET %s %s %s", cmdArgs[0], cmdArgs[1], cmdArgs[2]))
154+
if err != nil {
155+
fatal(err)
156+
}
157+
fmt.Println(text)
146158
case "chunk":
147159
cx, cy := cmdArgs[0], cmdArgs[1]
148160
payload, err := runBulk(client, fmt.Sprintf("CHUNK %s %s", cx, cy))
@@ -217,6 +229,32 @@ func validateCommandArgs(cmd string, cmdArgs []string) error {
217229
if err := validateIntArg(cmdArgs[1], "y"); err != nil {
218230
return err
219231
}
232+
case "chunkexists":
233+
if len(cmdArgs) != 2 {
234+
return fmt.Errorf("usage: chunkexists <cx> <cy>")
235+
}
236+
if err := validateIntArg(cmdArgs[0], "cx"); err != nil {
237+
return err
238+
}
239+
if err := validateIntArg(cmdArgs[1], "cy"); err != nil {
240+
return err
241+
}
242+
case "chunkset":
243+
if len(cmdArgs) != 3 {
244+
return fmt.Errorf("usage: chunkset <cx> <cy> <bits>")
245+
}
246+
if err := validateIntArg(cmdArgs[0], "cx"); err != nil {
247+
return err
248+
}
249+
if err := validateIntArg(cmdArgs[1], "cy"); err != nil {
250+
return err
251+
}
252+
if cmdArgs[2] == "" {
253+
return fmt.Errorf("bits must not be empty")
254+
}
255+
if err := validateBits(cmdArgs[2]); err != nil {
256+
return err
257+
}
220258
case "chunk":
221259
if len(cmdArgs) != 2 {
222260
return fmt.Errorf("usage: chunk <cx> <cy>")
@@ -420,6 +458,28 @@ func runShell(
420458
continue
421459
}
422460
fmt.Fprintln(stdout, text)
461+
case "chunkexists":
462+
if err := validateCommandArgs(cmd, cmdArgs); err != nil {
463+
fmt.Fprintf(stderr, "error: %v\n", err)
464+
continue
465+
}
466+
text, err := runSimple(client, fmt.Sprintf("CHUNKEXISTS %s %s", cmdArgs[0], cmdArgs[1]))
467+
if err != nil {
468+
fmt.Fprintf(stderr, "error: %v\n", err)
469+
continue
470+
}
471+
fmt.Fprintln(stdout, text)
472+
case "chunkset":
473+
if err := validateCommandArgs(cmd, cmdArgs); err != nil {
474+
fmt.Fprintf(stderr, "error: %v\n", err)
475+
continue
476+
}
477+
text, err := runSimple(client, fmt.Sprintf("CHUNKSET %s %s %s", cmdArgs[0], cmdArgs[1], cmdArgs[2]))
478+
if err != nil {
479+
fmt.Fprintf(stderr, "error: %v\n", err)
480+
continue
481+
}
482+
fmt.Fprintln(stdout, text)
423483
case "chunk":
424484
if err := validateCommandArgs(cmd, cmdArgs); err != nil {
425485
fmt.Fprintf(stderr, "error: %v\n", err)
@@ -517,6 +577,8 @@ Commands:
517577
exists <x> <y>
518578
set <x> <y> <bits>
519579
unset <x> <y>
580+
chunkexists <cx> <cy>
581+
chunkset <cx> <cy> <bits>
520582
chunk <cx> <cy>
521583
chunkbin [--out <file>] <cx> <cy>
522584
shell
@@ -533,6 +595,8 @@ Examples:
533595
chunk-cli --uri chunk://token@127.0.0.1:4242/ ping
534596
chunk-cli --uri chunk://token@127.0.0.1:4242/ get 0 0
535597
chunk-cli --uri chunk://token@127.0.0.1:4242/ exists 0 0
598+
chunk-cli --uri chunk://token@127.0.0.1:4242/ chunkexists 0 0
599+
chunk-cli --uri chunk://token@127.0.0.1:4242/ chunkset 0 0 <full_chunk_bits>
536600
chunk-cli --uri chunk://token@127.0.0.1:4242/ shell
537601
chunk-cli --uri chunks://token@127.0.0.1:4242/ --tls-insecure info
538602
`)

cmd/chunk-cli/main_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ func TestValidateCommandArgs(t *testing.T) {
3232
{name: "set bad bits", cmd: "set", args: []string{"1", "2", "01x1"}, wantErr: true},
3333
{name: "unset ok", cmd: "unset", args: []string{"1", "2"}, wantErr: false},
3434
{name: "unset bad int", cmd: "unset", args: []string{"1", "b"}, wantErr: true},
35+
{name: "chunkexists ok", cmd: "chunkexists", args: []string{"0", "0"}, wantErr: false},
36+
{name: "chunkexists bad int", cmd: "chunkexists", args: []string{"a", "0"}, wantErr: true},
37+
{name: "chunkset ok", cmd: "chunkset", args: []string{"0", "0", "0101"}, wantErr: false},
38+
{name: "chunkset bad bits", cmd: "chunkset", args: []string{"0", "0", "01x1"}, wantErr: true},
3539
{name: "chunk ok", cmd: "chunk", args: []string{"0", "0"}, wantErr: false},
3640
{name: "auth too many", cmd: "auth", args: []string{"a", "b"}, wantErr: true},
3741
{name: "chunkbin passthrough", cmd: "chunkbin", args: []string{"--out", "x", "1", "2"}, wantErr: false},

cmd/chunk-cli/shell_test.go

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ type shellServerState struct {
2121
commands []string
2222
blocks map[string]string
2323
present map[string]bool
24+
chunks map[string]string
25+
chunkSet map[string]bool
2426
}
2527

2628
func (s *shellServerState) record(cmd string) {
@@ -66,6 +68,29 @@ func (s *shellServerState) blockExists(x string, y string) bool {
6668
return s.present[x+":"+y]
6769
}
6870

71+
func (s *shellServerState) setChunk(cx string, cy string, bits string) {
72+
s.mu.Lock()
73+
defer s.mu.Unlock()
74+
key := cx + ":" + cy
75+
s.chunks[key] = bits
76+
s.chunkSet[key] = true
77+
}
78+
79+
func (s *shellServerState) getChunk(cx string, cy string) string {
80+
s.mu.Lock()
81+
defer s.mu.Unlock()
82+
if bits, ok := s.chunks[cx+":"+cy]; ok {
83+
return bits
84+
}
85+
return "0000"
86+
}
87+
88+
func (s *shellServerState) chunkExists(cx string, cy string) bool {
89+
s.mu.Lock()
90+
defer s.mu.Unlock()
91+
return s.chunkSet[cx+":"+cy]
92+
}
93+
6994
func startShellTestServer(t *testing.T, token string) (string, *shellServerState, func()) {
7095
t.Helper()
7196

@@ -75,8 +100,10 @@ func startShellTestServer(t *testing.T, token string) (string, *shellServerState
75100
}
76101

77102
state := &shellServerState{
78-
blocks: make(map[string]string),
79-
present: make(map[string]bool),
103+
blocks: make(map[string]string),
104+
present: make(map[string]bool),
105+
chunks: make(map[string]string),
106+
chunkSet: make(map[string]bool),
80107
}
81108
done := make(chan struct{})
82109

@@ -223,8 +250,53 @@ func startShellTestServer(t *testing.T, token string) (string, *shellServerState
223250
if err := writeBulk(writer, []byte("chunkdb_version=1\n")); err != nil {
224251
return
225252
}
253+
case "CHUNKEXISTS":
254+
if !authed {
255+
if err := writeError(writer, "AUTH_REQUIRED use AUTH <token>"); err != nil {
256+
return
257+
}
258+
continue
259+
}
260+
if len(fields) != 3 {
261+
if err := writeError(writer, "INVALID_ARGUMENT CHUNKEXISTS requires 2 args"); err != nil {
262+
return
263+
}
264+
continue
265+
}
266+
if state.chunkExists(fields[1], fields[2]) {
267+
if err := writeSimple(writer, "1"); err != nil {
268+
return
269+
}
270+
} else {
271+
if err := writeSimple(writer, "0"); err != nil {
272+
return
273+
}
274+
}
275+
case "CHUNKSET":
276+
if !authed {
277+
if err := writeError(writer, "AUTH_REQUIRED use AUTH <token>"); err != nil {
278+
return
279+
}
280+
continue
281+
}
282+
if len(fields) != 4 {
283+
if err := writeError(writer, "INVALID_ARGUMENT CHUNKSET requires 3 args"); err != nil {
284+
return
285+
}
286+
continue
287+
}
288+
if !isBits(fields[3]) {
289+
if err := writeError(writer, "INVALID_ARGUMENT invalid bits"); err != nil {
290+
return
291+
}
292+
continue
293+
}
294+
state.setChunk(fields[1], fields[2], fields[3])
295+
if err := writeSimple(writer, "OK"); err != nil {
296+
return
297+
}
226298
case "CHUNK":
227-
if err := writeBulk(writer, []byte("0000")); err != nil {
299+
if err := writeBulk(writer, []byte(state.getChunk(fields[1], fields[2]))); err != nil {
228300
return
229301
}
230302
case "CHUNKBIN":
@@ -315,7 +387,7 @@ func TestRunShellConnectAuthPingExistsGetSetUnsetQuit(t *testing.T) {
315387
}
316388
defer client.Close()
317389

318-
input := strings.NewReader("ping\nexists 1 2\nset 1 2 1010\nexists 1 2\nget 1 2\nunset 1 2\nexists 1 2\nget 1 2\nquit\n")
390+
input := strings.NewReader("ping\nexists 1 2\nset 1 2 1010\nexists 1 2\nget 1 2\nunset 1 2\nexists 1 2\nget 1 2\nchunkexists 0 0\nchunkset 0 0 1111000011110000\nchunkexists 0 0\nchunk 0 0\nquit\n")
319391
var out bytes.Buffer
320392
var errOut bytes.Buffer
321393

@@ -334,7 +406,7 @@ func TestRunShellConnectAuthPingExistsGetSetUnsetQuit(t *testing.T) {
334406
if !sawAuth {
335407
t.Fatalf("expected shell auto-auth to run AUTH")
336408
}
337-
expectedCommands := []string{"AUTH", "PING", "EXISTS", "SET", "EXISTS", "GET", "UNSET", "EXISTS", "GET", "QUIT"}
409+
expectedCommands := []string{"AUTH", "PING", "EXISTS", "SET", "EXISTS", "GET", "UNSET", "EXISTS", "GET", "CHUNKEXISTS", "CHUNKSET", "CHUNKEXISTS", "CHUNK", "QUIT"}
338410
if len(commands) != len(expectedCommands) {
339411
t.Fatalf("unexpected command count: got %v want %v", commands, expectedCommands)
340412
}
@@ -345,10 +417,10 @@ func TestRunShellConnectAuthPingExistsGetSetUnsetQuit(t *testing.T) {
345417
}
346418

347419
output := out.String()
348-
if strings.Count(output, "chunk> ") < 8 {
420+
if strings.Count(output, "chunk> ") < 12 {
349421
t.Fatalf("expected repeated prompt, got %q", output)
350422
}
351-
for _, expected := range []string{"PONG", "chunk> 0\n", "chunk> 1\n", "1010", "0000", "BYE"} {
423+
for _, expected := range []string{"PONG", "chunk> 0\n", "chunk> 1\n", "1010", "0000", "1111000011110000", "BYE"} {
352424
if !strings.Contains(output, expected) {
353425
t.Fatalf("expected %q in output %q", expected, output)
354426
}

0 commit comments

Comments
 (0)