init: dec-music 项目初始化
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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:]
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user