init: dec-music 项目初始化

This commit is contained in:
2026-05-23 12:55:48 +08:00
commit 21045d0aad
50 changed files with 3662 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
package mmkv
type Manager interface {
// OpenVault opens a vault with the given id.
// If the vault does not exist, it will be created.
// If id is empty, DefaultVaultID will be used.
OpenVault(id string) (Vault, error)
OpenVaultCrypto(id string, cryptoKey string) (Vault, error)
}
type Vault interface {
Keys() []string
GetRaw(key string) ([]byte, bool)
GetBytes(key string) ([]byte, error)
GetString(key string) (string, error)
}
+41
View File
@@ -0,0 +1,41 @@
package internal
// ProtoBuffer decodes a subset of the protobuf wire format for MMKV payloads.
type ProtoBuffer struct {
buf []byte
idx int
}
// NewProtoBuffer returns a buffer whose unread portion is buf.
func NewProtoBuffer(buf []byte) *ProtoBuffer {
return &ProtoBuffer{buf: buf}
}
// DecodeStringBytes consumes a length-prefixed string.
func (b *ProtoBuffer) DecodeStringBytes() (string, error) {
v, n := ConsumeString(b.buf[b.idx:])
if n < 0 {
return "", parseError(n)
}
b.idx += n
return v, nil
}
// DecodeRawBytes consumes a length-prefixed bytes field.
func (b *ProtoBuffer) DecodeRawBytes(alloc bool) ([]byte, error) {
v, n := ConsumeBytes(b.buf[b.idx:])
if n < 0 {
return nil, parseError(n)
}
b.idx += n
if alloc {
v = append([]byte(nil), v...)
}
return v, nil
}
// Unread returns the unread portion of the buffer.
func (b *ProtoBuffer) Unread() []byte {
return b.buf[b.idx:]
}
+65
View File
@@ -0,0 +1,65 @@
package internal
// Minimal protobuf wire helpers (bytes/string fields only).
const (
errCodeOverflow = -2
errCodeTruncated = -1
)
func consumeVarint(b []byte) (uint64, int) {
var x uint64
var s uint
for i, c := range b {
if c < 0x80 {
if i > 9 || i == 9 && c > 1 {
return 0, errCodeOverflow
}
return x | uint64(c)<<s, i + 1
}
x |= uint64(c&0x7f) << s
s += 7
}
return 0, errCodeTruncated
}
// ConsumeBytes parses a length-delimited bytes field.
func ConsumeBytes(b []byte) ([]byte, int) {
v, n := consumeVarint(b)
if n < 0 {
return nil, n
}
if v > uint64(len(b)-n) {
return nil, errCodeOverflow
}
return b[n : n+int(v)], n + int(v)
}
// ConsumeString parses a length-delimited string field.
func ConsumeString(b []byte) (string, int) {
v, n := ConsumeBytes(b)
if n < 0 {
return "", n
}
return string(v), n
}
func parseError(code int) error {
switch code {
case errCodeOverflow:
return errOverflow
case errCodeTruncated:
return errTruncated
default:
return errTruncated
}
}
var (
errOverflow = &parseErr{"overflow"}
errTruncated = &parseErr{"truncated"}
)
type parseErr struct{ msg string }
func (e *parseErr) Error() string { return "protowire: " + e.msg }
+96
View File
@@ -0,0 +1,96 @@
package mmkv
import (
"fmt"
"os"
"path"
)
const (
DefaultVaultID = "mmkv.default"
)
type manager struct {
dir string
vaults map[string]Vault
}
// NewManager creates a new MMKV Manager.
func NewManager(dir string) (Manager, error) {
// check dir exists
info, err := os.Stat(dir)
if err != nil {
return nil, fmt.Errorf("failed to stat dir: %w", err)
}
if !info.IsDir() {
return nil, fmt.Errorf("not a directory")
}
return &manager{
dir: dir,
vaults: make(map[string]Vault),
}, nil
}
func (m *manager) OpenVault(id string) (Vault, error) {
if id == "" {
id = DefaultVaultID
}
if v, ok := m.vaults[id]; ok {
return v, nil
}
vault, err := m.openVault(id, "")
if err != nil {
return nil, fmt.Errorf("failed to open vault: %w", err)
}
m.vaults[id] = vault
return vault, nil
}
func (m *manager) OpenVaultCrypto(id string, cryptoKey string) (Vault, error) {
if id == "" {
id = DefaultVaultID
}
if v, ok := m.vaults[id]; ok {
return v, nil
}
vault, err := m.openVault(id, cryptoKey)
if err != nil {
return nil, fmt.Errorf("failed to open vault: %w", err)
}
m.vaults[id] = vault
return vault, nil
}
func (m *manager) openVault(id string, cryptoKey string) (Vault, error) {
metaFile, err := os.Open(path.Join(m.dir, id+".crc"))
if err != nil {
return nil, fmt.Errorf("failed to open metadata file: %w", err)
}
defer metaFile.Close()
vaultFile, err := os.Open(path.Join(m.dir, id))
if err != nil {
return nil, fmt.Errorf("failed to open vault file: %w", err)
}
defer vaultFile.Close()
meta, err := loadMetadata(metaFile)
if err != nil {
return nil, fmt.Errorf("failed to load metadata: %w", err)
}
v, err := loadVault(vaultFile, meta, cryptoKey)
if err != nil {
return nil, fmt.Errorf("failed to load vault: %w", err)
}
return v, nil
}
+39
View File
@@ -0,0 +1,39 @@
package mmkv
import "testing"
func TestNewManager(t *testing.T) {
t.Run("Default", func(t *testing.T) {
mgr, err := NewManager("./testdata")
if err != nil {
t.Fatal(err)
}
vault, err := mgr.OpenVault("")
if err != nil {
t.Fatal(err)
}
if vault == nil {
t.Fatal("vault is nil")
}
})
t.Run("Crypto", func(t *testing.T) {
mgr, err := NewManager("./testdata")
if err != nil {
t.Fatal(err)
}
vault, err := mgr.OpenVaultCrypto("crypto", "123456")
if err != nil {
t.Fatal(err)
}
val, err := vault.GetString("world")
if err != nil {
t.Fatal(err)
}
if val != "hello" {
t.Fatalf("world = %q, want hello", val)
}
if _, err = vault.GetBytes("foo"); err == nil {
t.Fatal("expected error for missing key foo")
}
})
}
+56
View File
@@ -0,0 +1,56 @@
package mmkv
import (
"encoding/binary"
"fmt"
"io"
)
type metadata struct {
// added in version 0
crc32 uint32
// added in version 1
version uint32
sequence uint32 // full write back count
// added in version 2
aesVector []byte // random iv for encryption, aes.BlockSize (16 bytes)
// added in version 3, try to reduce file corruption
actualSize uint32
lastActualSize uint32
lastCRC32 uint32
//_reversed []byte // 64 bytes
}
func loadMetadata(rd io.Reader) (*metadata, error) {
buf := make([]byte, 0x68)
_, err := io.ReadFull(rd, buf)
if err != nil {
return nil, fmt.Errorf("failed to read metadata: %w", err)
}
m := &metadata{}
m.crc32 = binary.LittleEndian.Uint32(buf[0:4])
m.version = binary.LittleEndian.Uint32(buf[4:8])
if m.version >= 1 {
m.sequence = binary.LittleEndian.Uint32(buf[8:12])
}
if m.version >= 2 {
m.aesVector = buf[12:28]
}
if m.version >= 3 {
m.actualSize = binary.LittleEndian.Uint32(buf[28:32])
m.lastActualSize = binary.LittleEndian.Uint32(buf[32:36])
m.lastCRC32 = binary.LittleEndian.Uint32(buf[36:40])
}
//m._reversed = buf[40:104]
return m, nil
}
+42
View File
@@ -0,0 +1,42 @@
package mmkv
import (
"bytes"
"os"
"testing"
)
func Test_loadMetadata(t *testing.T) {
file, err := os.Open("./testdata/mmkv.default.crc")
if err != nil {
t.Fatal(err)
}
meta, err := loadMetadata(file)
if err != nil {
t.Fatal(err)
}
if meta.version != 3 {
t.Fatalf("version = %d, want 3", meta.version)
}
if meta.sequence != 1 {
t.Fatalf("sequence = %d, want 1", meta.sequence)
}
if meta.actualSize != 28 {
t.Fatalf("actualSize = %d, want 28", meta.actualSize)
}
if meta.crc32 != 197326043 {
t.Fatalf("crc32 = %d, want 197326043", meta.crc32)
}
if meta.lastActualSize != 4 {
t.Fatalf("lastActualSize = %d, want 4", meta.lastActualSize)
}
if meta.lastCRC32 != 1285129681 {
t.Fatalf("lastCRC32 = %d, want 1285129681", meta.lastCRC32)
}
wantIV := bytes.Repeat([]byte{0x00}, 16)
if !bytes.Equal(meta.aesVector, wantIV) {
t.Fatalf("aesVector = %v, want zeros", meta.aesVector)
}
}
+105
View File
@@ -0,0 +1,105 @@
package mmkv
import (
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"fmt"
"hash/crc32"
"io"
"jsuse.com/dev/dec-music/mmkv/internal"
)
type vault map[string][]byte
func (v vault) keys() []string {
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
return keys
}
func (v vault) Keys() []string {
return v.keys()
}
func (v vault) GetRaw(key string) ([]byte, bool) {
val, ok := v[key]
return val, ok
}
func (v vault) GetBytes(key string) ([]byte, error) {
raw, ok := v[key]
if !ok {
return nil, fmt.Errorf("key not found: %s", key)
}
val, n := internal.ConsumeBytes(raw)
if n < 0 {
return nil, fmt.Errorf("invalid protobuf bytes")
}
return val, nil
}
func (v vault) GetString(key string) (string, error) {
val, err := v.GetBytes(key)
if err != nil {
return "", err
}
return string(val), nil
}
func loadVault(src io.Reader, m *metadata, cryptoKey string) (Vault, error) {
fileSizeBuf := make([]byte, 4)
_, err := io.ReadFull(src, fileSizeBuf)
if err != nil {
return nil, fmt.Errorf("failed to read file size: %w", err)
}
size := binary.LittleEndian.Uint32(fileSizeBuf)
if m != nil && size != m.actualSize {
return nil, fmt.Errorf("metadata and vault payload size mismatch")
}
buf := make([]byte, size)
_, err = io.ReadFull(src, buf)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
if m != nil && m.crc32 != crc32.ChecksumIEEE(buf) {
return nil, fmt.Errorf("metadata and vault payload crc32 mismatch")
}
if len(cryptoKey) > 0 {
mKey := make([]byte, aes.BlockSize)
copy(mKey, cryptoKey)
block, err := aes.NewCipher(mKey)
if err != nil {
return nil, fmt.Errorf("failed to create aes cipher")
}
stream := cipher.NewCFBDecrypter(block, m.aesVector)
stream.XORKeyStream(buf, buf)
}
v := make(vault)
rd := internal.NewProtoBuffer(buf[4:])
for len(rd.Unread()) > 0 {
key, err := rd.DecodeStringBytes()
if err != nil {
return nil, fmt.Errorf("failed to decode key: %w", err)
}
val, err := rd.DecodeRawBytes(false)
if err != nil {
return nil, fmt.Errorf("failed to decode value: %w", err)
}
v[key] = val
}
return v, nil
}
+34
View File
@@ -0,0 +1,34 @@
package mmkv
import (
"os"
"testing"
)
func Test_loadVault(t *testing.T) {
file, err := os.Open("./testdata/mmkv.default")
if err != nil {
t.Fatal(err)
}
v, err := loadVault(file, nil, "")
if err != nil {
t.Fatal(err)
}
if len(v.Keys()) != 2 {
t.Fatalf("keys len = %d, want 2", len(v.Keys()))
}
val, err := v.GetString("world")
if err != nil {
t.Fatal(err)
}
if val != "hello" {
t.Fatalf("world = %q, want hello", val)
}
if _, err = v.GetBytes("foo"); err == nil {
t.Fatal("expected error for missing key foo")
}
}