diff --git a/algo/common/dispatch.go b/algo/common/dispatch.go index c164c6f..0758f79 100644 --- a/algo/common/dispatch.go +++ b/algo/common/dispatch.go @@ -6,8 +6,24 @@ import ( "strings" "go.uber.org/zap" + "unlock-music.dev/cli/internal/utils" ) +type QMCKeys map[string]string + +type CryptoParams struct { + // KuGou kgg database path + KggDbPath string + + // QMC Crypto config + QmcKeys QMCKeys +} + +func (k QMCKeys) Get(key string) (string, bool) { + value, ok := k[utils.NormalizeUnicode(key)] + return value, ok +} + type DecoderParams struct { Reader io.ReadSeeker // required Extension string // required, source extension, eg. ".mp3" @@ -16,8 +32,7 @@ type DecoderParams struct { Logger *zap.Logger // required - // KuGou - KggDatabasePath string + CryptoParams CryptoParams } type NewDecoderFunc func(p *DecoderParams) Decoder diff --git a/algo/kgm/kgm.go b/algo/kgm/kgm.go index c1cf4ea..85a2ef4 100644 --- a/algo/kgm/kgm.go +++ b/algo/kgm/kgm.go @@ -19,7 +19,7 @@ type Decoder struct { } func NewDecoder(p *common.DecoderParams) common.Decoder { - return &Decoder{rd: p.Reader, KggDatabasePath: p.KggDatabasePath} + return &Decoder{rd: p.Reader, KggDatabasePath: p.CryptoParams.KggDbPath} } // Validate checks if the file is a valid Kugou (.kgm, .vpr, .kgma) file. diff --git a/algo/qmc/key_mmkv.go b/algo/qmc/key_mmkv.go index 63571f0..0c1aaa9 100644 --- a/algo/qmc/key_mmkv.go +++ b/algo/qmc/key_mmkv.go @@ -1,118 +1,75 @@ package qmc import ( - "errors" "fmt" "os" "path/filepath" - "runtime" - "github.com/samber/lo" - mmkv "github.com/unlock-music/go-mmkv" + go_mmkv "github.com/unlock-music/go-mmkv" "go.uber.org/zap" - "golang.org/x/exp/slices" - "golang.org/x/text/unicode/norm" + "unlock-music.dev/cli/algo/common" + "unlock-music.dev/cli/internal/utils" ) -var streamKeyVault mmkv.Vault - -// TODO: move to factory -func readKeyFromMMKV(file string, logger *zap.Logger) ([]byte, error) { - if file == "" { - return nil, errors.New("file path is required while reading key from mmkv") - } - - //goland:noinspection GoBoolExpressions - if runtime.GOOS != "darwin" { - return nil, errors.New("mmkv vault not supported on this platform") - } - - if streamKeyVault == nil { - mmkvDir, err := getRelativeMMKVDir(file) - if err != nil { - mmkvDir, err = getDefaultMMKVDir() - if err != nil { - return nil, fmt.Errorf("mmkv key valut not found: %w", err) - } +func mergeMMKVKeys(keys ...common.QMCKeys) common.QMCKeys { + result := make(common.QMCKeys) + for _, k := range keys { + for key, value := range k { + result[utils.NormalizeUnicode(key)] = utils.NormalizeUnicode(value) } - - mgr, err := mmkv.NewManager(mmkvDir) - if err != nil { - return nil, fmt.Errorf("init mmkv manager: %w", err) - } - - streamKeyVault, err = mgr.OpenVault("MMKVStreamEncryptId") - if err != nil { - return nil, fmt.Errorf("open mmkv vault: %w", err) - } - - logger.Debug("mmkv vault opened", zap.Strings("keys", streamKeyVault.Keys())) } - - _, partName := filepath.Split(file) - partName = normalizeUnicode(partName) - - buf, err := streamKeyVault.GetBytes(file) - if buf == nil { - filePaths := streamKeyVault.Keys() - fileNames := lo.Map(filePaths, func(filePath string, _ int) string { - _, name := filepath.Split(filePath) - return normalizeUnicode(name) - }) - - for _, key := range fileNames { // fallback: match filename only - if key != partName { - continue - } - idx := slices.Index(fileNames, key) - buf, err = streamKeyVault.GetBytes(filePaths[idx]) - if err != nil { - logger.Warn("read key from mmkv", zap.String("key", filePaths[idx]), zap.Error(err)) - } - } - - } - - if len(buf) == 0 { - return nil, errors.New("key not found in mmkv vault") - } - - return deriveKey(buf) + return result } -func OpenMMKV(mmkvPath string, key string, logger *zap.Logger) error { - filePath, fileName := filepath.Split(mmkvPath) - mgr, err := mmkv.NewManager(filepath.Dir(filePath)) +func LoadMMKV(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) { + mmkv_path := path + mmkv_crc := path + ".crc" + + mr, err := os.Open(mmkv_path) if err != nil { - return fmt.Errorf("init mmkv manager: %w", err) + logger.Error("LoadMMKV: Could not open mmkv file", zap.Error(err)) + return nil, fmt.Errorf("LoadMMKV: open error: %w", err) + } + defer mr.Close() + + cr, err := os.Open(mmkv_crc) + if err == nil { + // crc is optional + cr = nil + defer cr.Close() } - // If `vaultKey` is empty, the key is ignored. - streamKeyVault, err = mgr.OpenVaultCrypto(fileName, key) - + var password []byte = nil + if key != "" { + password = make([]byte, 16) + copy(password, []byte(key)) + } + mmkv, err := go_mmkv.NewMMKVReader(mr, password, cr) if err != nil { - return fmt.Errorf("open mmkv vault: %w", err) + logger.Error("LoadMMKV: failed to create reader", zap.Error(err)) + return nil, fmt.Errorf("LoadMMKV: NewMMKVReader error: %w", err) } - logger.Debug("mmkv vault opened", zap.Strings("keys", streamKeyVault.Keys())) - return nil + result = make(common.QMCKeys) + for !mmkv.IsEOF() { + key, err := mmkv.ReadKey() + if err != nil { + logger.Error("LoadMMKV: read key error", zap.Error(err)) + return nil, fmt.Errorf("LoadMMKV: read key error: %w", err) + } + value, err := mmkv.ReadStringValue() + if err != nil { + logger.Error("LoadMMKV: read value error", zap.Error(err)) + return nil, fmt.Errorf("LoadMMKV: read value error: %w", err) + } + logger.Debug("LoadMMKV: read", zap.String("key", key), zap.String("value", value)) + result[utils.NormalizeUnicode(key)] = utils.NormalizeUnicode(value) + } + + return result, nil } -// / -func readKeyFromMMKVCustom(mid string) ([]byte, error) { - if streamKeyVault == nil { - return nil, fmt.Errorf("mmkv vault not loaded") - } - - // get ekey from mmkv vault - eKey, err := streamKeyVault.GetBytes(mid) - if err != nil { - return nil, fmt.Errorf("get eKey error: %w", err) - } - return deriveKey(eKey) -} - -// / getRelativeMMKVDir get mmkv dir relative to file (legacy QQMusic for macOS behaviour) +// getRelativeMMKVDir get mmkv dir relative to file (legacy QQMusic for macOS behaviour) func getRelativeMMKVDir(file string) (string, error) { mmkvDir := filepath.Join(filepath.Dir(file), "../mmkv") if _, err := os.Stat(mmkvDir); err != nil { @@ -149,10 +106,3 @@ func getDefaultMMKVDir() (string, error) { return mmkvDir, nil } - -// normalizeUnicode normalizes unicode string to NFC. -// since macOS may change some characters in the file name. -// e.g. "ぜ"(e3 81 9c) -> "ぜ"(e3 81 9b e3 82 99) -func normalizeUnicode(str string) string { - return norm.NFC.String(str) -} diff --git a/algo/qmc/key_mmkv_loader_darwin.go b/algo/qmc/key_mmkv_loader_darwin.go new file mode 100644 index 0000000..2554702 --- /dev/null +++ b/algo/qmc/key_mmkv_loader_darwin.go @@ -0,0 +1,65 @@ +package qmc + +import ( + "fmt" + "os" + "path/filepath" + + "go.uber.org/zap" + "unlock-music.dev/cli/algo/common" +) + +func LoadMMKVOrDefault(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) { + key1, err := loadMacKeysV8(logger) + if err != nil { + key1 = nil + logger.Warn("LoadMMKVOrDefault: could not read QQMusic v8.8.0 keys", zap.Error(err)) + } + + key2, err := loadMacKeysV10(logger) + if err != nil { + key2 = nil + logger.Warn("LoadMMKVOrDefault: could not read QQMusic v10.x keys", zap.Error(err)) + } + + userKeys := make(common.QMCKeys) + if path != "" { + logger.Info("Using user mmkv") + userKeys, err = LoadMMKV(path, key, logger) + if err != nil { + userKeys = nil + logger.Warn("LoadMMKVOrDefault: could not read user keys", zap.Error(err)) + } + } + + allKeys := mergeMMKVKeys(key1, key2, userKeys) + + logger.Debug("Keys loaded", zap.Any("keys", allKeys), zap.Int("len", len(allKeys))) + + return allKeys, nil +} + +func loadMacKeysV8(logger *zap.Logger) (keys common.QMCKeys, err error) { + homeDir, err := os.UserHomeDir() + if err != nil { + logger.Warn("Failed to get home dir") + return nil, fmt.Errorf("loadMacKeysV8: failed to get home: %w", err) + } + p := filepath.Join( + homeDir, + "Library/Containers/com.tencent.QQMusicMac/Data", + "Library/Application Support/QQMusicMac/mmkv", + "MMKVStreamEncryptId", + ) + if f, err := os.Stat(p); err == nil && !f.IsDir() { + logger.Info("Using QQMusic 8.x mmkv", zap.String("mmkv", p)) + return LoadMMKV(p, "", logger) + } + return nil, nil +} + +func loadMacKeysV10(logger *zap.Logger) (common.QMCKeys, error) { + // TODO: stub only + var _ = logger + return nil, nil +} diff --git a/algo/qmc/key_mmkv_loader_default.go b/algo/qmc/key_mmkv_loader_default.go new file mode 100644 index 0000000..c85e191 --- /dev/null +++ b/algo/qmc/key_mmkv_loader_default.go @@ -0,0 +1,13 @@ +//go:build !darwin + +package qmc + +import ( + "go.uber.org/zap" + "unlock-music.dev/cli/algo/common" +) + +func LoadMMKVOrDefault(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) { + // Stub: do nothing + return nil, nil +} diff --git a/algo/qmc/qmc.go b/algo/qmc/qmc.go index 3bfff21..a72f382 100644 --- a/algo/qmc/qmc.go +++ b/algo/qmc/qmc.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "runtime" "strconv" "strings" @@ -133,14 +132,15 @@ func (d *Decoder) searchKey() (err error) { } fileSize := int(fileSizeM4) + 4 - //goland:noinspection GoBoolExpressions - if runtime.GOOS == "darwin" && !strings.HasPrefix(d.params.Extension, ".qmc") { - d.decodedKey, err = readKeyFromMMKV(d.params.FilePath, d.logger) + if key, ok := d.params.CryptoParams.QmcKeys.Get(d.params.FilePath); ok { + d.logger.Debug("QQMusic Mac Legacy file", zap.String("file", d.params.FilePath), zap.String("key", key)) + d.decodedKey, err = deriveKey([]byte(key)) if err == nil { d.audioLen = fileSize - return + return nil } - d.logger.Warn("read key from mmkv failed", zap.Error(err)) + d.decodedKey = nil + d.logger.Warn("could not derive key, skip", zap.Error(err)) } suffixBuf := make([]byte, 4) @@ -153,17 +153,17 @@ func (d *Decoder) searchKey() (err error) { return d.readRawMetaQTag() case "STag": return errors.New("qmc: file with 'STag' suffix doesn't contains media key") + // MusicEx\0 case "cex\x00": footer, err := NewMusicExTag(d.raw) if err != nil { return err } d.audioLen = fileSize - int(footer.TagSize) - d.decodedKey, err = readKeyFromMMKVCustom(footer.MediaFileName) - if err != nil { - return err + if key, ok := d.params.CryptoParams.QmcKeys.Get(footer.MediaFileName); ok { + d.decodedKey, err = deriveKey([]byte(key)) } - return nil + return err default: size := binary.LittleEndian.Uint32(suffixBuf) diff --git a/cmd/um/main.go b/cmd/um/main.go index b615181..d748e7b 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -49,8 +49,8 @@ func main() { Flags: []cli.Flag{ &cli.StringFlag{Name: "input", Aliases: []string{"i"}, Usage: "path to input file or dir", Required: false}, &cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "path to output dir", Required: false}, - &cli.StringFlag{Name: "qmc-mmkv", Aliases: []string{"db"}, Usage: "path to qmc mmkv (.crc file also required)", Required: false}, - &cli.StringFlag{Name: "qmc-mmkv-key", Aliases: []string{"key"}, Usage: "mmkv password (16 ascii chars)", Required: false}, + &cli.StringFlag{Name: "qmc-mmkv", Aliases: []string{"db"}, Usage: "path to QQMusic mmkv path", Required: false}, + &cli.StringFlag{Name: "qmc-mmkv-key", Aliases: []string{"key"}, Usage: "QQMusic mmkv password (16 ascii chars)", Required: false}, &cli.StringFlag{Name: "kgg-db", Usage: "path to kgg db (win32 kugou v11)", Required: false}, &cli.BoolFlag{Name: "remove-source", Aliases: []string{"rs"}, Usage: "remove source file", Required: false, Value: false}, &cli.BoolFlag{Name: "skip-noop", Aliases: []string{"n"}, Usage: "skip noop decoder", Required: false, Value: true}, @@ -176,13 +176,10 @@ func appMain(c *cli.Context) (err error) { return errors.New("output should be a writable directory") } - if mmkv := c.String("qmc-mmkv"); mmkv != "" { - // If key is not set, the mmkv vault will be treated as unencrypted. - key := c.String("qmc-mmkv-key") - err := qmc.OpenMMKV(mmkv, key, logger) - if err != nil { - return err - } + // QMC: Load keys + qmcKeys, err := qmc.LoadMMKVOrDefault(c.String("qmc-mmkv"), c.String("qmc-mmkv-key"), logger) + if err != nil { + return err } kggDbPath := c.String("kgg-db") @@ -194,11 +191,18 @@ func appMain(c *cli.Context) (err error) { logger: logger, inputDir: inputDir, outputDir: output, - kggDbPath: kggDbPath, skipNoopDecoder: c.Bool("skip-noop"), removeSource: c.Bool("remove-source"), updateMetadata: c.Bool("update-metadata"), overwriteOutput: c.Bool("overwrite"), + + crypto: common.CryptoParams{ + // KuGou + KggDbPath: kggDbPath, + + // QQMusic + QmcKeys: qmcKeys, + }, } if inputStat.IsDir() { @@ -219,12 +223,12 @@ type processor struct { inputDir string outputDir string - kggDbPath string - skipNoopDecoder bool removeSource bool updateMetadata bool overwriteOutput bool + + crypto common.CryptoParams } func (p *processor) watchDir(inputDir string) error { @@ -352,11 +356,11 @@ func (p *processor) process(inputFile string, allDec []common.DecoderFactory) er logger := logger.With(zap.String("source", inputFile)) pDec, decoderFactory, err := p.findDecoder(allDec, &common.DecoderParams{ - Reader: file, - Extension: filepath.Ext(inputFile), - FilePath: inputFile, - Logger: logger, - KggDatabasePath: p.kggDbPath, + Reader: file, + Extension: filepath.Ext(inputFile), + FilePath: inputFile, + Logger: logger, + CryptoParams: p.crypto, }) if err != nil { return err diff --git a/go.mod b/go.mod index d10766d..912571b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module unlock-music.dev/cli -go 1.23.3 +go 1.25.1 require ( github.com/fsnotify/fsnotify v1.8.0 @@ -8,12 +8,13 @@ require ( github.com/go-flac/flacvorbis v0.2.0 github.com/go-flac/go-flac v1.0.0 github.com/samber/lo v1.47.0 - github.com/unlock-music/go-mmkv v0.1.0-github + github.com/unlock-music/go-mmkv v0.1.1 github.com/urfave/cli/v2 v2.27.5 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.29.0 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 golang.org/x/text v0.20.0 + modernc.org/sqlite v1.37.0 ) require ( @@ -27,9 +28,7 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/sys v0.31.0 // indirect - google.golang.org/protobuf v1.35.2 // indirect modernc.org/libc v1.62.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.9.1 // indirect - modernc.org/sqlite v1.37.0 // indirect ) diff --git a/go.sum b/go.sum index a9f3855..07a1a5a 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,9 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-flac/flacpicture v0.3.0 h1:LkmTxzFLIynwfhHiZsX0s8xcr3/u33MzvV89u+zOT8I= @@ -16,6 +12,8 @@ github.com/go-flac/flacvorbis v0.2.0 h1:KH0xjpkNTXFER4cszH4zeJxYcrHbUobz/RticWGO github.com/go-flac/flacvorbis v0.2.0/go.mod h1:uIysHOtuU7OLGoCRG92bvnkg7QEqHx19qKRV6K1pBrI= github.com/go-flac/go-flac v1.0.0 h1:6qI9XOVLcO50xpzm3nXvO31BgDgHhnr/p/rER/K/doY= github.com/go-flac/go-flac v1.0.0/go.mod h1:WnZhcpmq4u1UdZMNn9LYSoASpWOCMOoxXxcWEHSzkW8= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -28,22 +26,14 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= -github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/unlock-music/go-mmkv v0.1.0-github h1:3N47M/1HvfbmNgrY7xSks1Vbb6e6kdTBjVAy9tYGpT8= -github.com/unlock-music/go-mmkv v0.1.0-github/go.mod h1:bs4ItI9YHKa73pjHIwV1GR9HsXm11IiwuJ9O61XLoMw= -github.com/unlock-music/go-mmkv v0.1.1-0.20250902154031-d495634ce9bb h1:b52+FaxoR4P6fViYlVyMZZOojU12rE/KgfHLgvHMbQ0= -github.com/unlock-music/go-mmkv v0.1.1-0.20250902154031-d495634ce9bb/go.mod h1:bs4ItI9YHKa73pjHIwV1GR9HsXm11IiwuJ9O61XLoMw= -github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= -github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/unlock-music/go-mmkv v0.1.1 h1:1w3shjaBMp58jRrRkxPghnMpvyUoP4ZkA++W9ueFpE8= +github.com/unlock-music/go-mmkv v0.1.1/go.mod h1:E1h/HBpWPOKnCQw6LSQZkMpzxQojGDe+T5f/DRDXBsc= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -52,34 +42,44 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= +modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= +modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/utils/unicode.go b/internal/utils/unicode.go new file mode 100644 index 0000000..97eec99 --- /dev/null +++ b/internal/utils/unicode.go @@ -0,0 +1,10 @@ +package utils + +import "golang.org/x/text/unicode/norm" + +// normalizeUnicode normalizes unicode string to NFC. +// since macOS may change some characters in the file name. +// e.g. "ぜ"(e3 81 9c) -> "ぜ"(e3 81 9b e3 82 99) +func NormalizeUnicode(str string) string { + return norm.NFC.String(str) +}