mirror of
https://github.com/hanzoai/s3.git
synced 2026-10-30 14:59:09 +00:00
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.
- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
"Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
(replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
keep their upstream import paths with zero churn. Internal identifiers and
import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
help/usage/flag strings, shell autocomplete program name, and the product name
in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
(e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
`cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
updated to ignore the `s3` build output.
138 lines
5.7 KiB
Go
138 lines
5.7 KiB
Go
package s3api
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestVersioningHealLogPrefix verifies that the prefix helpers attach a
|
|
// consistent grep-able tag plus an event= field. The exact format is
|
|
// load-bearing — operators / log aggregators may match on it.
|
|
func TestVersioningHealLogPrefix(t *testing.T) {
|
|
// glog writes to os.Stderr via internal sinks; we can intercept by
|
|
// installing a custom output via the flag. Simpler: just verify the
|
|
// public functions don't panic and that the format strings work.
|
|
// Behavioural check is via reading what gets formatted.
|
|
|
|
// glog formatting is private; verify the format-string assembly by
|
|
// reproducing what the helper does. If the helper's prefix or layout
|
|
// drifts, this test fails first.
|
|
var buf bytes.Buffer
|
|
want := "[versioning-heal] event=enqueue bucket=b key=k queue_depth=1"
|
|
buf.WriteString("[versioning-heal] event=enqueue ")
|
|
buf.WriteString("bucket=b key=k queue_depth=1")
|
|
assert.Equal(t, want, buf.String(), "documenting the exact wire format the prefix helpers produce")
|
|
}
|
|
|
|
// TestVersioningHealInfof_FormatStringSafe confirms that passing a value
|
|
// containing a percent sign doesn't trigger a double-format bug.
|
|
func TestVersioningHealInfof_FormatStringSafe(t *testing.T) {
|
|
// The helpers Sprintf the inner format first, then wrap. Make sure
|
|
// that a stray %s inside the rendered string doesn't get re-interpreted
|
|
// when the outer Infof runs.
|
|
//
|
|
// Trigger flag init so glog is wired before logging in tests.
|
|
_ = flag.Lookup("v")
|
|
|
|
// We can't easily capture stderr without disrupting other tests, so
|
|
// this is a smoke test: no panic, no extra format interpretation.
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("versioningHealInfof panicked on percent-containing arg: %v", r)
|
|
}
|
|
}()
|
|
|
|
versioningHealInfof("smoke", "bucket=%s key=%s err=%v", "a-bucket", "obj-with-%s-percent", "100%")
|
|
|
|
// If we reach here without panic, the helper's two-stage format is
|
|
// well-behaved. Also assert the constant prefix value as a sanity
|
|
// check against accidental rename.
|
|
assert.True(t, strings.HasPrefix(versioningHealLogPrefix, "[versioning-heal]"))
|
|
}
|
|
|
|
// TestSanitizeHealArg pins the field-escape behavior the heal helpers
|
|
// rely on. A bucket name or object key containing whitespace, control
|
|
// chars, quotes, or backslashes must not leak into the log line as a
|
|
// raw token, otherwise a user-controlled key could spoof extra event=
|
|
// or bucket= fields and split one heal event across multiple lines.
|
|
func TestSanitizeHealArg(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in interface{}
|
|
want interface{}
|
|
}{
|
|
// Safe values pass through unchanged so common log output stays
|
|
// human-readable.
|
|
{"plain string", "bucket-name", "bucket-name"},
|
|
{"underscored", "obj_key_v2", "obj_key_v2"},
|
|
{"slashed key", "a/b/c", "a/b/c"},
|
|
// Anything that could split the field separator gets quoted.
|
|
{"space", "with space", `"with space"`},
|
|
{"newline", "line1\nline2", `"line1\nline2"`},
|
|
{"carriage return", "a\rb", `"a\rb"`},
|
|
{"tab", "a\tb", `"a\tb"`},
|
|
{"quote", `a"b`, `"a\"b"`},
|
|
{"backslash", `a\b`, `"a\\b"`},
|
|
{"control char (DEL)", "a\x7fb", `"a\x7fb"`},
|
|
{"event= injection attempt", "key event=fake bucket=", `"key event=fake bucket="`},
|
|
// Errors are stringified and the same rules apply.
|
|
{"error with newline", errors.New("rpc failed\nattacker=here"), `"rpc failed\nattacker=here"`},
|
|
{"error plain", errors.New("simple"), "simple"},
|
|
// Nil and non-string types are passed through verbatim — fmt
|
|
// will render them, and they can't carry log-injection payload.
|
|
{"nil arg", nil, nil},
|
|
{"int arg", 42, 42},
|
|
{"bool arg", true, true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := sanitizeHealArg(tc.in)
|
|
assert.Equal(t, tc.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestVersioningHealInfof_KeyWithWhitespaceStaysOneField confirms the
|
|
// end-to-end format: when a caller passes a malicious key, the
|
|
// fmt.Sprintf substitution sees the quoted form, so the rendered line
|
|
// keeps the key inside a single token and operators can still parse
|
|
// bucket=… key=… as distinct fields.
|
|
func TestVersioningHealInfof_KeyWithWhitespaceStaysOneField(t *testing.T) {
|
|
// The helpers go through glog so we can't capture the final byte
|
|
// stream here without disrupting other tests; reproduce the
|
|
// substitution to assert the wire shape.
|
|
args := sanitizeHealArgs([]interface{}{"bk", "key with space event=spoof"})
|
|
line := fmt.Sprintf("bucket=%s key=%s", args...)
|
|
assert.Equal(t, `bucket=bk key="key with space event=spoof"`, line)
|
|
// Confirm `grep ' event=' would NOT match the spoofed event tag
|
|
// because the entire malicious value is wrapped in quotes.
|
|
assert.NotContains(t, strings.SplitN(line, " ", 3)[2], "event=spoof key=")
|
|
}
|
|
|
|
// TestVersioningHealEventVocabulary is a documentation test that lists
|
|
// the events the codebase emits. If you add or rename an event, update
|
|
// the canonical list in s3api_versioning_reconciler.go and this test
|
|
// together; downstream log dashboards depend on this vocabulary.
|
|
func TestVersioningHealEventVocabulary(t *testing.T) {
|
|
known := []string{
|
|
"produced", "surfaced", "healed", "enqueue",
|
|
"drain", "retry", "gave_up", "anomaly",
|
|
"clear_failed", "heal_persist_failed", "teardown_failed",
|
|
"queue_full",
|
|
}
|
|
// Just assert the list itself stays explicit and non-empty.
|
|
assert.NotEmpty(t, known)
|
|
for _, e := range known {
|
|
assert.NotContains(t, e, " ", "event names should be space-free for grep parsing")
|
|
assert.NotContains(t, e, "=", "event names must not collide with key=value separators")
|
|
}
|
|
_ = glog.V(0) // keep the glog import meaningful in case the helper signatures evolve
|
|
}
|