diff --git a/data/lang/gen_lang.go b/data/lang/gen_lang.go index c7e4dae8..c11a8196 100644 --- a/data/lang/gen_lang.go +++ b/data/lang/gen_lang.go @@ -5,216 +5,41 @@ package main import ( - "encoding/json" "errors" - "fmt" - "io" "net/http" - "os" + stdos "os" "path/filepath" - "regexp" - "strings" - "sync" - "text/template" -) - -// language=gohtml -var langTmpl = `// Code generated by downloader.go; DO NOT EDIT. -package {{.Name}} -{{if ne .Name "en_us"}} -import "github.com/Tnze/go-mc/chat" - -func init() { chat.SetLanguage(Map) } -{{end}} -var Map = {{.LangMap | printf "%#v"}} -` + "github.com/hack-pad/hackpadfs/os" +) //go:generate go run $GOFILE //go:generate go fmt ./... func main() { - if len(os.Args) == 2 { - fmt.Println("generating en-us lang") - f, err := os.Open(os.Args[1]) - if err != nil { - panic(err) - } - defer f.Close() - readLang("en_us", f) - return - } else { - fmt.Println("generating langs except en-us") - fmt.Println("WARN: You should also set the secondary argument to en-us's json file") - } - - versionURL, err := assetIndexURL() - if err != nil { - panic(err) - } - - resp, err := http.Get(versionURL) - if err != nil { - panic(err) - } - defer resp.Body.Close() - - var list struct { - Objects map[string]struct { - Hash string `json:"hash"` - Size int64 `json:"size"` - } `json:"objects"` - } - - err = json.NewDecoder(resp.Body).Decode(&list) - if err != nil { - panic(err) - } - - tasks := make(chan string) - var wg sync.WaitGroup - for i := 0; i < 16; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for i := range tasks { - v := list.Objects[i] - if strings.HasPrefix(i, "minecraft/lang/") { - name := i[len("minecraft/lang/") : len(i)-len(".json")] - lang(name, v.Hash) - } - } - }() - } - for i := range list.Objects { - tasks <- i - } - close(tasks) - wg.Wait() -} - -func lang(name, hash string) { - // download language - LangURL := "https://resources.download.minecraft.net/" + hash[:2] + "/" + hash - fmt.Println(name, ":", LangURL) - resp, err := http.Get(LangURL) - if err != nil { - panic(err) - } - defer resp.Body.Close() - readLang(name, resp.Body) -} - -// read one language translation -func readLang(name string, r io.Reader) { - var LangMap map[string]string - err := json.NewDecoder(r).Decode(&LangMap) - if err != nil { - panic(err) - } - trans(LangMap) - - pName := strings.ReplaceAll(name, "_", "-") - - // mkdir - err = os.Mkdir(pName, 0o777) - if err != nil && !os.IsExist(err) { - panic(err) - } - - f, err := os.OpenFile(filepath.Join(pName, name+".go"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o666) + fsys, err := resolveFS(string(filepath.Separator)) if err != nil { panic(err) } - defer f.Close() - - genData := struct { - PkgName string - Name string - LangMap map[string]string - }{ - PkgName: pName, - Name: name, - LangMap: LangMap, - } - - tmpl := template.Must(template.New("").Parse(langTmpl)) - if err := tmpl.Execute(f, genData); err != nil { - panic(err) - } + run(fsys, http.Get, stdos.Args) } -var javaN = regexp.MustCompile(`%[0-9]\$s`) - -// Java use %2$s to refer to the second arg, but Golang use %2s, so we need this -func trans(m map[string]string) { - // replace "%[0-9]\$s" with "%[0-9]s" - for i := range m { - c := m[i] - if javaN.MatchString(c) { - m[i] = javaN.ReplaceAllStringFunc(c, func(s string) string { - var index int - _, err := fmt.Sscanf(s, "%%%d$s", &index) - if err != nil { - panic(err) - } - return fmt.Sprintf("%%[%d]s", index) - }) - } - } -} +func resolveFS(base string) (*os.FS, error) { + fs := os.NewFS() -func assetIndexURL() (string, error) { - // Pseudo code for get versionURL: - // $manifest = {https://piston-meta.mojang.com/mc/game/version_manifest_v2.json} - // $latest = $manifest.latest.release - // $versionURL = {$manifest.versions[where .id == $latest ].url} - // $assetIndexURL = $version.assetIndex.url - var manifest struct { - Latest struct { - Release string `json:"release"` - } `json:"latest"` - Versions []struct { - ID string `json:"id"` - URL string `json:"url"` - } `json:"versions"` - } - - manifestRes, err := http.Get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json") + baseDirectory, err := fs.FromOSPath(base) // Convert to an FS path if err != nil { - return "", fmt.Errorf("could not reach version manifest: %w", err) - } - defer manifestRes.Body.Close() - - if err := json.NewDecoder(manifestRes.Body).Decode(&manifest); err != nil { - return "", fmt.Errorf("could not decode manifest JSON: %w", err) - } - - var versionURL string - for _, v := range manifest.Versions { - if manifest.Latest.Release == v.ID { - versionURL = v.URL - break - } - } - if versionURL == "" { - return "", errors.New("could not determine versionURL") - } - - var version struct { - AssetIndex struct { - URL string `json:"url"` - } `json:"assetIndex"` + return nil, err } - versionRes, err := http.Get(versionURL) + baseDirFS, err := fs.Sub(baseDirectory) // Run all file system operations rooted at the current working directory if err != nil { - return "", fmt.Errorf("could not reach versionURL: %w", err) + return nil, err } - defer versionRes.Body.Close() - if err := json.NewDecoder(versionRes.Body).Decode(&version); err != nil { - return "", fmt.Errorf("could not decode version JSON: %w", err) + ofs, ok := baseDirFS.(*os.FS) + if !ok { + return nil, errors.New("sub FS not an OS instance FS") } - return version.AssetIndex.URL, nil + return ofs, nil } diff --git a/data/lang/lang.go b/data/lang/lang.go new file mode 100644 index 00000000..d82793b6 --- /dev/null +++ b/data/lang/lang.go @@ -0,0 +1,265 @@ +//go:build generate +// +build generate + +package main + +import ( + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "text/template" + + "github.com/Tnze/go-mc/internal/filesystem" +) + +// language=gohtml +var langTmpl = `// Code generated by lang.go; DO NOT EDIT. + +package {{.Name}} +{{if ne .Name "en_us"}} +import "github.com/Tnze/go-mc/chat" + +func init() { chat.SetLanguage(Map) } +{{end}} +var Map = {{.LangMap | printf "%#v"}} +` + +func run(fsys filesystem.FS, httpGetter func(url string) (resp *http.Response, err error), args []string) error { + flagsSet := flag.NewFlagSet(args[0], flag.ExitOnError) + enUSFile := flagsSet.String("en_us", "", "path to EN_US language JSON file") + mcVersion := flagsSet.String("version", "", "language version to generate with") + + if err := flagsSet.Parse(args[1:]); err != nil { + return err + } + + if enUSJson := *enUSFile; len(enUSJson) > 0 { + return genENUSLang(fsys, enUSJson) + } + + return downloadAndGenerateLangs(fsys, httpGetter, *mcVersion) +} + +func genENUSLang(fsys filesystem.FS, enUSJsonFile string) error { + fmt.Printf("generating en-us lang from %s\n", enUSJsonFile) + f, err := fsys.Open(enUSJsonFile) + if err != nil { + return err + } + defer f.Close() + return readLang(fsys, "en_us", f) +} + +func downloadAndGenerateLangs(fsys filesystem.FS, httpGetter func(url string) (resp *http.Response, err error), version string) error { + fmt.Println("generating langs except en-us") + fmt.Println("WARN: Provide argument value to en-us's json file and run it again to generate just en-us") + + versionURL, err := assetIndexURL(httpGetter, version) + if err != nil { + return err + } + + resp, err := httpGetter(versionURL) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("non OK status response %d fetching version data", resp.StatusCode) + } + + var list struct { + Objects map[string]struct { + Hash string `json:"hash"` + Size int64 `json:"size"` + } `json:"objects"` + } + + err = json.NewDecoder(resp.Body).Decode(&list) + if err != nil { + return err + } + + tasks := make(chan string) + var wg sync.WaitGroup + for i := 0; i < 16; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := range tasks { + v := list.Objects[i] + if strings.HasPrefix(i, "minecraft/lang/") { + name := i[len("minecraft/lang/") : len(i)-len(".json")] + lang(fsys, httpGetter, name, v.Hash) + } + } + }() + } + for i := range list.Objects { + tasks <- i + } + close(tasks) + wg.Wait() + + return nil +} + +func lang(fsys filesystem.FS, httpGetter func(url string) (*http.Response, error), name, hash string) error { + // download language + LangURL := "https://resources.download.minecraft.net/" + hash[:2] + "/" + hash + fmt.Println(name, ":", LangURL) + resp, err := httpGetter(LangURL) + if err != nil { + return err + } + defer resp.Body.Close() + return readLang(fsys, name, resp.Body) +} + +// read one language translation +func readLang(fsys filesystem.FS, name string, r io.Reader) error { + var LangMap map[string]string + err := json.NewDecoder(r).Decode(&LangMap) + if err != nil { + return err + } + + if err := trans(LangMap); err != nil { + return err + } + + pName := strings.ReplaceAll(name, "_", "-") + + err = fsys.Mkdir(pName, fs.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + + genData := struct { + PkgName string + Name string + LangMap map[string]string + }{ + PkgName: pName, + Name: name, + LangMap: LangMap, + } + + var buf bytes.Buffer + tmpl := template.Must(template.New("").Parse(langTmpl)) + if err := tmpl.Execute(&buf, genData); err != nil { + return err + } + + if err := fsys.WriteFile(filepath.Join(pName, name+".go"), buf.Bytes(), fs.ModePerm); err != nil { + return err + } + + return nil +} + +var javaN = regexp.MustCompile(`%[0-9]\$s`) + +// Java use %2$s to refer to the second arg, but Golang use %2s, so we need this +func trans(m map[string]string) error { + // replace "%[0-9]\$s" with "%[0-9]s" + var errx error + for i := range m { + c := m[i] + if javaN.MatchString(c) { + m[i] = javaN.ReplaceAllStringFunc(c, func(s string) string { + var index int + _, err := fmt.Sscanf(s, "%%%d$s", &index) + if err != nil { + errx = err + return "" + } + return fmt.Sprintf("%%[%d]s", index) + }) + } + } + + return errx +} + +func assetIndexURL(httpGetter func(url string) (*http.Response, error), requestedVersion string) (string, error) { + // Pseudo code for get versionURL: + // $manifest = {https://piston-meta.mojang.com/mc/game/version_manifest_v2.json} + // $latest = $manifest.latest.release + // $versionURL = {$manifest.versions[where .id == $latest ].url} + // $assetIndexURL = $version.assetIndex.url + var manifest struct { + Latest struct { + Release string `json:"release"` + } `json:"latest"` + Versions []struct { + ID string `json:"id"` + URL string `json:"url"` + } `json:"versions"` + } + + manifestRes, err := httpGetter("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json") + if err != nil { + return "", fmt.Errorf("could not reach version manifest: %w", err) + } + defer manifestRes.Body.Close() + + if manifestRes.StatusCode != http.StatusOK { + return "", fmt.Errorf("non OK status response %d fetching version manifest", manifestRes.StatusCode) + } + + if err := json.NewDecoder(manifestRes.Body).Decode(&manifest); err != nil { + return "", fmt.Errorf("could not decode manifest JSON: %w", err) + } + + var versionURL string + for _, v := range manifest.Versions { + if len(requestedVersion) > 0 { + if requestedVersion == v.ID { + versionURL = v.URL + break + } + } else { + if manifest.Latest.Release == v.ID { + versionURL = v.URL + break + } + } + } + if versionURL == "" { + return "", errors.New("could not determine version URL") + } + + var version struct { + AssetIndex struct { + URL string `json:"url"` + } `json:"assetIndex"` + } + + versionRes, err := httpGetter(versionURL) + if err != nil { + return "", fmt.Errorf("could not reach version URL: %w", err) + } + defer versionRes.Body.Close() + + if versionRes.StatusCode != http.StatusOK { + return "", fmt.Errorf("non OK status response %d fetching version JSON", versionRes.StatusCode) + } + + if err := json.NewDecoder(versionRes.Body).Decode(&version); err != nil { + return "", fmt.Errorf("could not decode version JSON: %w", err) + } + + return version.AssetIndex.URL, nil +} diff --git a/data/lang/lang_test.go b/data/lang/lang_test.go new file mode 100644 index 00000000..e9589e7b --- /dev/null +++ b/data/lang/lang_test.go @@ -0,0 +1,186 @@ +//go:build generate +// +build generate + +package main + +import ( + "encoding/base64" + "io" + "io/fs" + "net/http" + "os" + "strings" + "testing" + "testing/fstest" + + "github.com/matryer/is" +) + +const enUSJSON = `{"block.minecraft.acacia_button":"Acaia Button","block.minecraft.acacia_door":"Acacia Door","block.minecraft.acacia_fence":"Acacia Fence","block.minecraft.acacia_fence_gate":"Acacia Fence Gate","block.minecraft.acacia_hanging_sign":"Acacia Hanging Sign","block.minecraft.acacia_leaves":"Acacia Leaves","block.minecraft.acacia_log":"Acacia Log","block.minecraft.acacia_planks":"Acacia Planks","block.minecraft.acacia_pressure_plate":"Acacia Pressure Plate","block.minecraft.acacia_sapling":"Acacia Sapling","block.minecraft.acacia_sign":"Acacia Sign","block.minecraft.acacia_slab":"Acacia Slab","block.minecraft.acacia_stairs":"Acacia Stairs","block.minecraft.acacia_trapdoor":"Acacia Trapdoor","block.minecraft.acacia_wall_hanging_sign":"Acacia Wall Hanging Sign","block.minecraft.acacia_wall_sign":"Acacia Wall Sign","block.minecraft.acacia_wood":"Acacia Wood","block.minecraft.activator_rail":"Activator Rail","block.minecraft.air":"Air","block.minecraft.allium":"Allium","block.minecraft.amethyst_block":"Amethyst Block","block.minecraft.amethyst_cluster":"Amethyst Cluster","block.minecraft.ancient_debris":"ancient_debris","block.minecraft.andesite":"Andesite","block.minecraft.andesite_slab":"Andesite Slab","block.minecraft.andesite_stairs":"Andesite Stairs","block.minecraft.andesite_wall":"Andesite Wall","block.minecraft.anvil":"Anvil","block.minecraft.attached_melon_stem":"Attached Melon Stem","block.minecraft.attached_pumpkin_stem":"Attached Pumpkin Stem","block.minecraft.azalea":"Azaleya","block.minecraft.azalea_leaves":"Azaleya Leaves","block.minecraft.azure_bluet":"Azure Bluet"}` + +type mapFSWithMkdir struct { + files fstest.MapFS +} + +func (m mapFSWithMkdir) Open(name string) (fs.File, error) { + return m.files.Open(name) +} + +func (m mapFSWithMkdir) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { + createMode := flag & os.O_CREATE + truncMode := flag & os.O_TRUNC + if createMode != 0 { + m.files[name] = &fstest.MapFile{ + Mode: perm, + } + } + + if _, ok := m.files[name]; !ok { + return nil, os.ErrNotExist + } + + if truncMode != 0 { + m.files[name].Data = nil + } + + return m.files.Open(name) +} + +func (m mapFSWithMkdir) Mkdir(name string, perm fs.FileMode) error { + m.files[name] = &fstest.MapFile{Mode: fs.ModeDir | perm} + return nil +} + +func (m mapFSWithMkdir) ReadDir(name string) ([]fs.DirEntry, error) { + return m.files.ReadDir(name) +} + +func (m mapFSWithMkdir) Stat(name string) (fs.FileInfo, error) { + return m.files.Stat(name) +} +func (m mapFSWithMkdir) WriteFile(name string, data []byte, perm fs.FileMode) error { + m.files[name] = &fstest.MapFile{Data: data} + return nil +} + +func buildMockFS() mapFSWithMkdir { + return mapFSWithMkdir{ + files: fstest.MapFS{ + "enus.json": &fstest.MapFile{ + Data: []byte(enUSJSON), + }, + }, + } +} + +const manifestData = `{"latest":{"release":"1.20.1","snapshot":"1.20.1"},"versions":[{"id":"1.20.1","type":"release","url":"https://piston-meta.mojang.com/v1/packages/715ccf3330885e75b205124f09f8712542cbe7e0/1.20.1.json","time":"2023-06-12T13:32:21+00:00","releaseTime":"2023-06-12T13:25:51+00:00","sha1":"715ccf3330885e75b205124f09f8712542cbe7e0","complianceLevel":1},{"id":"1.20","type":"release","url":"https://piston-meta.mojang.com/v1/packages/52f6c28f40ee907d167a1f217d7a48cbec4936c5/1.20.json","time":"2023-06-12T10:37:38+00:00","releaseTime":"2023-06-02T08:36:17+00:00","sha1":"52f6c28f40ee907d167a1f217d7a48cbec4936c5","complianceLevel":1},{"id":"1.19.4","type":"release","url":"https://piston-meta.mojang.com/v1/packages/a4118bd311bc49c9ca298284c0055f25a007e4f8/1.19.4.json","time":"2023-06-12T10:18:09+00:00","releaseTime":"2023-03-14T12:56:18+00:00","sha1":"a4118bd311bc49c9ca298284c0055f25a007e4f8","complianceLevel":1}]}` + +const v1_20_1versionData = `base64:eyJhc3NldEluZGV4Ijp7ImlkIjoiNSIsInNoYTEiOiI5ZDU4ZmRkMjUzOGM2ODc3ZmI1YzVjNTU4ZWJjNjBlZTBiNmQwZTg0Iiwic2l6ZSI6NDExNTgxLCJ0b3RhbFNpemUiOjYxNzcxODc5OSwidXJsIjoiaHR0cHM6Ly9waXN0b24tbWV0YS5tb2phbmcuY29tL3YxL3BhY2thZ2VzLzlkNThmZGQyNTM4YzY4NzdmYjVjNWM1NThlYmM2MGVlMGI2ZDBlODQvNS5qc29uIn0sIm1haW5DbGFzcyI6Im5ldC5taW5lY3JhZnQuY2xpZW50Lm1haW4uTWFpbiIsIm1pbmltdW1MYXVuY2hlclZlcnNpb24iOjIxLCJyZWxlYXNlVGltZSI6IjIwMjMtMDYtMTJUMTM6MjU6NTErMDA6MDAiLCJ0aW1lIjoiMjAyMy0wNi0xMlQxMzoyNTo1MSswMDowMCIsInR5cGUiOiJyZWxlYXNlIn0K` +const v1_20_1versionHashesData = `base64:eyJvYmplY3RzIjp7Im1pbmVjcmFmdC9sYW5nL2ZpbF9waC5qc29uIjp7Imhhc2giOiI3MTJhMjM2Nzk0MzEzZTUxMmU0N2UxYzAwMGE5ZmNkMmNjMjQ0ZWRjIiwic2l6ZSI6NDMxODg3fX19Cg==` + +// language data did not change between v1.19.4 and v1.20.1 +const languageData = `base64:eyJibG9jay5taW5lY3JhZnQuYWNhY2lhX2J1dHRvbiI6IkFrYXN5YW5nIFBpbmR1dGFuIiwiYmxvY2subWluZWNyYWZ0LmFjYWNpYV9kb29yIjoiQWthc3lhbmcgUGludG8iLCJibG9jay5taW5lY3JhZnQuYWNhY2lhX2ZlbmNlIjoiQWthc3lhbmcgQmFrb2QiLCJibG9jay5taW5lY3JhZnQuYWNhY2lhX2ZlbmNlX2dhdGUiOiJBa2FzeWFuZyBUYXJhbmdrYWhhbiIsImJsb2NrLm1pbmVjcmFmdC5hY2FjaWFfaGFuZ2luZ19zaWduIjoiTmFrYXNhYml0IG5hIEthcmF0dWxhbmcgQWthc3lhIiwiYmxvY2subWluZWNyYWZ0LmFjYWNpYV9sZWF2ZXMiOiJEYWhvbmcgQWthc3lhIiwiYmxvY2subWluZWNyYWZ0LmFjYWNpYV9sb2ciOiJBa2FzeWFuZyBUcm9zbyIsImJsb2NrLm1pbmVjcmFmdC5hY2FjaWFfcGxhbmtzIjoiQWthc3lhbmcgVGFibGEiLCJibG9jay5taW5lY3JhZnQuYWNhY2lhX3ByZXNzdXJlX3BsYXRlIjoiQWthc3lhbmcgQXBha2FuIiwiYmxvY2subWluZWNyYWZ0LmFjYWNpYV9zYXBsaW5nIjoiSGFsYW1hbmcgQWthc3lhIiwiYmxvY2subWluZWNyYWZ0LmFjYWNpYV9zaWduIjoiQWthc3lhbmcgS2FyYXR1bGEiLCJibG9jay5taW5lY3JhZnQuYWNhY2lhX3NsYWIiOiJBa2FzeWFuZyBUaWxhZCIsImJsb2NrLm1pbmVjcmFmdC5hY2FjaWFfc3RhaXJzIjoiQWthc3lhbmcgSGFnZGFuYW4iLCJibG9jay5taW5lY3JhZnQuYWNhY2lhX3RyYXBkb29yIjoiTWFsaWl0IG5hIEFrYXN5YW5nIFBpbnRvIiwiYmxvY2subWluZWNyYWZ0LmFjYWNpYV93YWxsX2hhbmdpbmdfc2lnbiI6Ik5ha2FzYWJpdCBuYSBLYXJhdHVsYW5nIEFrYXN5YSIsImJsb2NrLm1pbmVjcmFmdC5hY2FjaWFfd2FsbF9zaWduIjoiQWthc3lhbmcgS2FyYXR1bGEgc2EgUGFkZXIiLCJibG9jay5taW5lY3JhZnQuYWNhY2lhX3dvb2QiOiJBa2FzeWFuZyBLYWhveSIsImJsb2NrLm1pbmVjcmFmdC5hY3RpdmF0b3JfcmFpbCI6IlRhZ2EtYnVrYXMgbmEgUmlsZXMiLCJibG9jay5taW5lY3JhZnQuYWlyIjoiSGltcGFwYXdpZCIsImJsb2NrLm1pbmVjcmFmdC5hbGxpdW0iOiJBbGxpdW0iLCJibG9jay5taW5lY3JhZnQuYW1ldGh5c3RfYmxvY2siOiJBbWV0aXN0YW5nIEJsb2tlIiwiYmxvY2subWluZWNyYWZ0LmFtZXRoeXN0X2NsdXN0ZXIiOiJLdW1wb2wgbmcgQW1ldGlzdGEiLCJibG9jay5taW5lY3JhZnQuYW5jaWVudF9kZWJyaXMiOiJTaW5hdW5hbmcgWWFnaXQiLCJibG9jay5taW5lY3JhZnQuYW5kZXNpdGUiOiJBbmRlc2F5dCIsImJsb2NrLm1pbmVjcmFmdC5hbmRlc2l0ZV9zbGFiIjoiQW5kZXNheXQgbmEgVGlsYWQiLCJibG9jay5taW5lY3JhZnQuYW5kZXNpdGVfc3RhaXJzIjoiQW5kZXNheXQgbmEgSGFnZGFuYW4iLCJibG9jay5taW5lY3JhZnQuYW5kZXNpdGVfd2FsbCI6IlBhZGVyIG5hIEFuZGVzYXl0IiwiYmxvY2subWluZWNyYWZ0LmFudmlsIjoiUGFsaWhhbiIsImJsb2NrLm1pbmVjcmFmdC5hdHRhY2hlZF9tZWxvbl9zdGVtIjoiTmFrYWthYml0IG5hIFRhbmdrYXkgbmcgTWVsb24iLCJibG9jay5taW5lY3JhZnQuYXR0YWNoZWRfcHVtcGtpbl9zdGVtIjoiTmFrYWthYml0IG5hIFRhbmdrYXkgbmcgS2FsYWJhc2EiLCJibG9jay5taW5lY3JhZnQuYXphbGVhIjoiQXphbGV5YSIsImJsb2NrLm1pbmVjcmFmdC5hemFsZWFfbGVhdmVzIjoiRGFob25nIEF6YWxleWEiLCJibG9jay5taW5lY3JhZnQuYXp1cmVfYmx1ZXQiOiJBc3VsIG5hIExpZ2F3IG5hIEJ1bGFrbGFrIn0K` + +const v1_19_4versionData = `base64:eyJhc3NldEluZGV4Ijp7ImlkIjoiMyIsInNoYTEiOiIwMWE3YjFjNzk0MGQ2MWY0NmExY2ZiYmNhMDY4NGE3ZTg2YWZmYTU4Iiwic2l6ZSI6NDEwMTkzLCJ0b3RhbFNpemUiOjU2MDcyMTgwMiwidXJsIjoiaHR0cHM6Ly9waXN0b24tbWV0YS5tb2phbmcuY29tL3YxL3BhY2thZ2VzLzAxYTdiMWM3OTQwZDYxZjQ2YTFjZmJiY2EwNjg0YTdlODZhZmZhNTgvMy5qc29uIn0sIm1haW5DbGFzcyI6Im5ldC5taW5lY3JhZnQuY2xpZW50Lm1haW4uTWFpbiIsIm1pbmltdW1MYXVuY2hlclZlcnNpb24iOjIxLCJyZWxlYXNlVGltZSI6IjIwMjMtMDMtMTRUMTI6NTY6MTgrMDA6MDAiLCJ0aW1lIjoiMjAyMy0wMy0xNFQxMjo1NjoxOCswMDowMCIsInR5cGUiOiJyZWxlYXNlIn0K` +const v1_19_4versionHashesData = `base64:eyJvYmplY3RzIjp7Im1pbmVjcmFmdC9sYW5nL2ZpbF9waC5qc29uIjp7Imhhc2giOiI3MTJhMjM2Nzk0MzEzZTUxMmU0N2UxYzAwMGE5ZmNkMmNjMjQ0ZWRjIiwic2l6ZSI6NDMxODg3fX19Cg==` + +func buildMockHTTPGet(visitedUrls *[]string) func(url string) (*http.Response, error) { + urlsToData := map[string]string{ + "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json": manifestData, + "https://piston-meta.mojang.com/v1/packages/715ccf3330885e75b205124f09f8712542cbe7e0/1.20.1.json": v1_20_1versionData, + "https://piston-meta.mojang.com/v1/packages/a4118bd311bc49c9ca298284c0055f25a007e4f8/1.19.4.json": v1_19_4versionData, + "https://piston-meta.mojang.com/v1/packages/9d58fdd2538c6877fb5c5c558ebc60ee0b6d0e84/5.json": v1_20_1versionHashesData, + "https://piston-meta.mojang.com/v1/packages/01a7b1c7940d61f46a1cfbbca0684a7e86affa58/3.json": v1_19_4versionHashesData, + "https://resources.download.minecraft.net/71/712a236794313e512e47e1c000a9fcd2cc244edc": languageData, + } + + return func(url string) (*http.Response, error) { + *visitedUrls = append(*visitedUrls, url) + resp := http.Response{} + v, ok := urlsToData[url] + if !ok { + resp.StatusCode = http.StatusNotFound + resp.Body = io.NopCloser(strings.NewReader("")) + return &resp, nil + } + content, err := decodeBase64(v) + if err != nil { + resp.StatusCode = http.StatusInternalServerError + resp.Body = io.NopCloser(strings.NewReader(err.Error())) + return &resp, nil + } + resp.Body = io.NopCloser(strings.NewReader(content)) + resp.StatusCode = http.StatusOK + return &resp, nil + } +} + +func decodeBase64(str string) (string, error) { + if strings.HasPrefix(str, "base64:") { + decoded, err := base64.StdEncoding.DecodeString(str[7:]) + if err != nil { + return "", err + } + return string(decoded), nil + } + return str, nil +} + +func TestRunWithNoArgsDownloadsFilesAndUsesLatestVersion(t *testing.T) { + // the comments next to the "is" asserts show up as explanations in the stderr on failure + is := is.New(t) + + mockFS := buildMockFS() + + visitedURLs := []string{} + is.NoErr(run(mockFS, buildMockHTTPGet(&visitedURLs), []string{"lang.test"})) + + is.Equal(len(visitedURLs), 4) // should have downloaded files + is.True(strings.HasSuffix(visitedURLs[1], "1.20.1.json")) // should have downloaded latest available version + + langDir, ok := mockFS.files["fil-ph"] + is.True(ok) // did not create language parent directory + is.True(langDir.Mode.IsDir()) + + _, ok = mockFS.files["fil-ph/fil_ph.go"] + is.True(ok) // did not generate Go src from language data +} + +func TestRunWithVersionArgDownloadsFilesAndUsesGivenVersion(t *testing.T) { + // the comments next to the "is" asserts show up as explanations in the stderr on failure + is := is.New(t) + + mockFS := buildMockFS() + + visitedURLs := []string{} + is.NoErr(run(mockFS, buildMockHTTPGet(&visitedURLs), []string{"lang.test", "-version=1.19.4"})) + + is.Equal(len(visitedURLs), 4) // should have downloaded files + is.True(strings.HasSuffix(visitedURLs[1], "1.19.4.json")) // should have downloaded provided version + + langDir, ok := mockFS.files["fil-ph"] + is.True(ok) // did not create language parent directory + is.True(langDir.Mode.IsDir()) + + _, ok = mockFS.files["fil-ph/fil_ph.go"] + is.True(ok) // did not generate Go src from language data +} + +func TestRunWithEnUSArgFileGeneratesENUsLangNoDownloads(t *testing.T) { + // the comments next to the "is" asserts show up as explanations in the stderr on failure + is := is.New(t) + + mockFS := buildMockFS() + + visitedURLs := []string{} + is.NoErr(run(mockFS, buildMockHTTPGet(&visitedURLs), []string{"lang.test", "-en_us=enus.json"})) + + is.Equal(len(visitedURLs), 0) // should have downloaded no files + + langDir, ok := mockFS.files["en-us"] + is.True(ok) // did not create language parent directory + is.True(langDir.Mode.IsDir()) + + _, ok = mockFS.files["en-us/en_us.go"] + is.True(ok) // did not generate Go src from language data +} diff --git a/go.mod b/go.mod index a9f43b02..bd11e17f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.19 require ( github.com/google/uuid v1.3.0 + github.com/hack-pad/hackpadfs v0.2.1 github.com/iancoleman/strcase v0.2.0 + github.com/matryer/is v1.4.1 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 ) diff --git a/go.sum b/go.sum index 63aa1a84..60230e09 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hack-pad/hackpadfs v0.2.1 h1:FelFhIhv26gyjujoA/yeFO+6YGlqzmc9la/6iKMIxMw= +github.com/hack-pad/hackpadfs v0.2.1/go.mod h1:khQBuCEwGXWakkmq8ZiFUvUZz84ZkJ2KNwKvChs4OrU= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= diff --git a/internal/filesystem/filesystem.go b/internal/filesystem/filesystem.go new file mode 100644 index 00000000..e9a50839 --- /dev/null +++ b/internal/filesystem/filesystem.go @@ -0,0 +1,12 @@ +package filesystem + +import "io/fs" + +type FS interface { + Open(name string) (fs.File, error) + OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) + fs.StatFS + fs.ReadDirFS + WriteFile(name string, data []byte, perm fs.FileMode) error + Mkdir(name string, perm fs.FileMode) error +}