From b2adcd798140f7135c865544eabb9dffc1a4c56a Mon Sep 17 00:00:00 2001 From: AzenKain Date: Thu, 21 Aug 2025 21:44:16 +0700 Subject: [PATCH] UPDATE: update new language patch --- .../internal/languageservice.js | 4 +- frontend/src/pages/language/index.tsx | 74 +++-- internal/language-service.go | 269 ++++++++++-------- pkg/constant/constant.go | 2 +- pkg/language-patch/asset-meta/bytehash16.go | 30 ++ pkg/language-patch/asset-meta/dataEntry.go | 28 ++ pkg/language-patch/asset-meta/designIndex.go | 98 +++++++ pkg/language-patch/asset-meta/fileEntry.go | 63 ++++ pkg/language-patch/asset-meta/miniAsset.go | 32 +++ pkg/language-patch/excel-language/patch.go | 114 ++++++++ pkg/language-patch/excel-language/row.go | 81 ++++++ pkg/language-patch/excel-language/utils.go | 53 ++++ pkg/models/binary-version.go | 57 ++-- 13 files changed, 727 insertions(+), 178 deletions(-) create mode 100644 pkg/language-patch/asset-meta/bytehash16.go create mode 100644 pkg/language-patch/asset-meta/dataEntry.go create mode 100644 pkg/language-patch/asset-meta/designIndex.go create mode 100644 pkg/language-patch/asset-meta/fileEntry.go create mode 100644 pkg/language-patch/asset-meta/miniAsset.go create mode 100644 pkg/language-patch/excel-language/patch.go create mode 100644 pkg/language-patch/excel-language/row.go create mode 100644 pkg/language-patch/excel-language/utils.go diff --git a/frontend/bindings/firefly-launcher/internal/languageservice.js b/frontend/bindings/firefly-launcher/internal/languageservice.js index 657a236..1824044 100644 --- a/frontend/bindings/firefly-launcher/internal/languageservice.js +++ b/frontend/bindings/firefly-launcher/internal/languageservice.js @@ -8,7 +8,7 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime"; /** * @param {string} path - * @returns {Promise<[string, string]> & { cancel(): void }} + * @returns {Promise<[boolean, string, string, string]> & { cancel(): void }} */ export function GetLanguage(path) { let $resultPromise = /** @type {any} */($Call.ByID(3450750492, path)); @@ -19,7 +19,7 @@ export function GetLanguage(path) { * @param {string} path * @param {string} text * @param {string} voice - * @returns {Promise & { cancel(): void }} + * @returns {Promise<[boolean, string]> & { cancel(): void }} */ export function SetLanguage(path, text, voice) { let $resultPromise = /** @type {any} */($Call.ByID(2793672496, path, text, voice)); diff --git a/frontend/src/pages/language/index.tsx b/frontend/src/pages/language/index.tsx index bc4c259..9f5c1fd 100644 --- a/frontend/src/pages/language/index.tsx +++ b/frontend/src/pages/language/index.tsx @@ -27,28 +27,42 @@ export default function LanguagePage() { useEffect(() => { const getLanguage = async () => { - if (gameDir) { - const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows' - const fullPath = `${gameDir}/${subPath}` + if (!gameDir) return - const exists = await FSService.DirExists(fullPath) - if (exists) { - const [textLang, voiceLang] = await LanguageService.GetLanguage(fullPath) - setTextLang(textLang) - setVoiceLang(voiceLang) - setFolderCheckResult('success') - setSelectedTextLang(textLang) - setSelectedVoiceLang(voiceLang) - } else { - setTextLang('') - setVoiceLang('') - setSelectedTextLang('') - setSelectedVoiceLang('') - setFolderCheckResult('error') - setGameDir('') - } + const subPath = "StarRail_Data/StreamingAssets" + const fullPath = `${gameDir}/${subPath}` + + const exists = await FSService.DirExists(fullPath) + if (!exists) { + setTextLang("") + setVoiceLang("") + setSelectedTextLang("") + setSelectedVoiceLang("") + setFolderCheckResult("error") + setGameDir("") + return } + + const [ok, textLang, voiceLang, err] = await LanguageService.GetLanguage(fullPath) + if (!ok) { + setTextLang("") + setVoiceLang("") + setSelectedTextLang("") + setSelectedVoiceLang("") + setFolderCheckResult("error") + setGameDir("") + toast.error(err) + return + } + + // success + setTextLang(textLang) + setVoiceLang(voiceLang) + setFolderCheckResult("success") + setSelectedTextLang(textLang) + setSelectedVoiceLang(voiceLang) } + getLanguage() }, [gameDir]) @@ -86,19 +100,19 @@ export default function LanguagePage() { } try { setIsSettingLanguage(true) - const result = await LanguageService.SetLanguage( + const [ok, err] = await LanguageService.SetLanguage( `${gameDir}/StarRail_Data/StreamingAssets/DesignData/Windows`, selectedTextLang, selectedVoiceLang ) - if (result) { - toast.success('Language set successfully') - setTextLang(selectedTextLang) - setVoiceLang(selectedVoiceLang) - } - else { - toast.error('Language set failed') - } + if (ok) { + toast.success('Language set successfully') + setTextLang(selectedTextLang) + setVoiceLang(selectedVoiceLang) + } + else { + toast.error(err) + } } catch (err: any) { toast.error('SetLanguage error:', err) @@ -154,8 +168,8 @@ export default function LanguagePage() { {folderCheckResult && (
{folderCheckResult === 'success' ? ( <> diff --git a/internal/language-service.go b/internal/language-service.go index 8f95e63..4702c12 100644 --- a/internal/language-service.go +++ b/internal/language-service.go @@ -2,145 +2,174 @@ package internal import ( "bytes" - "fmt" + assetMeta "firefly-launcher/pkg/language-patch/asset-meta" + excelLanguage "firefly-launcher/pkg/language-patch/excel-language" + "firefly-launcher/pkg/models" "os" "path/filepath" + "slices" + "strings" ) type LanguageService struct{} func isValidLang(lang string) bool { valid := []string{"en", "jp", "cn", "kr"} - for _, v := range valid { - if lang == v { - return true - } - } - return false + return slices.Contains(valid, lang) } -func (l *LanguageService) GetLanguage(path string) (string, string, error) { - files, err := os.ReadDir(path) +func (l *LanguageService) GetLanguage(path string) (bool, string, string, string) { + currentVersionGame, err := models.ParseBinaryVersion(filepath.Join(path, "BinaryVersion.bytes")) if err != nil { - return "", "", err + return false, "", "", err.Error() } - for _, file := range files { - filePath := filepath.Join(path, file.Name()) - - content, err := os.ReadFile(filePath) - if err != nil { - continue - } - - patternToFind := []byte("SpriteOutput/UI/Fonts/RPG_CN.ttf") - idx := bytes.Index(content, patternToFind) - if idx == -1 { - continue - } - - pattern := []byte("Korean") - idx = bytes.Index(content, pattern) - if idx == -1 { - continue - } - - // Move to os text language - idx += 10 - idx += 4 - osText := string(content[idx : idx+2]) - idx += 3 * 4 - - // Move to cn voice language - idx += 1 - idx += 5 - cnVoice := string(content[idx : idx+2]) - idx += 3 * 2 // skip 2 entries - - // Move to os voice language - idx += 1 - idx += 5 - osVoice := string(content[idx : idx+2]) - idx += 3 * 5 // skip 5 entries - - // Move to cn text language - idx += 1 - idx += 4 - cnText := string(content[idx : idx+2]) - - textLang := osText - voiceLang := osVoice - if !isValidLang(textLang) { - textLang = cnText - } - if !isValidLang(voiceLang) { - voiceLang = cnVoice - } - - return textLang, voiceLang, nil + typeVersionGame := "os" + if strings.Contains(currentVersionGame.Name, "CN") { + typeVersionGame = "cn" } - return "", "", fmt.Errorf("couldn't find file to read language from") -} + assetPath := filepath.Join(path, "DesignData\\Windows") -func replaceBytes(content []byte, idx int, choice string, param int) int { - for i := 0; i < param; i++ { - copy(content[idx:idx+2], []byte(choice)) - idx += 3 - } - return idx -} - -func (l *LanguageService) SetLanguage(path string, text, voice string) (bool, error) { - files, err := os.ReadDir(path) + indexHash, err := assetMeta.GetIndexHash(assetPath) if err != nil { - return false, err + return false, "", "", err.Error() } - for _, file := range files { - filePath := filepath.Join(path, file.Name()) - - content, err := os.ReadFile(filePath) - if err != nil { - continue - } - - patternToFind := []byte("SpriteOutput/UI/Fonts/RPG_CN.ttf") - idx := bytes.Index(content, patternToFind) - if idx == -1 { - continue - } - - pattern := []byte("Korean") - idx = bytes.Index(content, pattern) - if idx == -1 { - continue - } - - idx += 10 - idx += 4 - idx = replaceBytes(content, idx, text, 4) - idx += 1 - - idx += 5 - idx = replaceBytes(content, idx, voice, 2) - idx += 1 - - idx += 5 - idx = replaceBytes(content, idx, voice, 5) - - idx += 1 - - idx += 4 - _ = replaceBytes(content, idx, text, 2) - - err = os.WriteFile(filePath, content, 0644) - if err != nil { - return false, err - } - - return true, nil + DesignIndex, err := assetMeta.DesignIndexFromBytes(assetPath, indexHash) + if err != nil { + return false, "", "", err.Error() + } + dataEntry, fileEntry, err := DesignIndex.FindDataAndFileByTarget(-515329346) + if err != nil { + return false, "", "", err.Error() + } + allowedLanguage := excelLanguage.NewExcelLanguage(assetPath, &dataEntry, &fileEntry) + languageRows, err := allowedLanguage.Parse() + if err != nil { + return false, "", "", err.Error() } - return false, fmt.Errorf("couldn't find file to patch. Make sure this file is placed in the correct folder") + currentTextLang := "" + currentVoiceLang := "" + + pairs := []struct { + area string + typ *uint8 + }{ + {"os", nil}, + {"cn", func() *uint8 { v := uint8(1); return &v }()}, + {"os", func() *uint8 { v := uint8(1); return &v }()}, + {"cn", nil}, + } + + for _, p := range pairs { + var found *excelLanguage.LanguageRow + for i := range languageRows { + if languageRows[i].Area != nil && *languageRows[i].Area == p.area { + if (languageRows[i].Type == nil && p.typ == nil) || + (languageRows[i].Type != nil && p.typ != nil && *languageRows[i].Type == *p.typ) { + found = &languageRows[i] + break + } + } + } + if found == nil { + continue + } + if found.DefaultLanguage != nil && found.Area != nil && *found.Area == typeVersionGame && found.Type == nil { + currentTextLang = *found.DefaultLanguage + } + if found.DefaultLanguage != nil && found.Area != nil && *found.Area == typeVersionGame && found.Type != nil { + currentVoiceLang = *found.DefaultLanguage + } + } + + if currentTextLang == "" || currentVoiceLang == "" || !isValidLang(currentTextLang) || !isValidLang(currentVoiceLang) { + return false, "", "", "not found language" + } + + return true, currentTextLang, currentVoiceLang, "" +} + + + +func (l *LanguageService) SetLanguage(path string, text, voice string) (bool, string) { + indexHash, err := assetMeta.GetIndexHash(path) + if err != nil { + return false, err.Error() + } + + DesignIndex, err := assetMeta.DesignIndexFromBytes(path, indexHash) + if err != nil { + return false, err.Error() + } + dataEntry, fileEntry, err := DesignIndex.FindDataAndFileByTarget(-515329346) + if err != nil { + return false, err.Error() + } + allowedLanguage := excelLanguage.NewExcelLanguage(path, &dataEntry, &fileEntry) + languageRows, err := allowedLanguage.Parse() + if err != nil { + return false, err.Error() + } + + pairs := []struct { + area string + typ *uint8 + lang string + }{ + {"os", nil, text}, + {"cn", func() *uint8 { v := uint8(1); return &v }(), voice}, + {"os", func() *uint8 { v := uint8(1); return &v }(), voice}, + {"cn", nil, text}, + } + + for _, p := range pairs { + var found *excelLanguage.LanguageRow + for i := range languageRows { + if languageRows[i].Area != nil && *languageRows[i].Area == p.area { + if (languageRows[i].Type == nil && p.typ == nil) || + (languageRows[i].Type != nil && p.typ != nil && *languageRows[i].Type == *p.typ) { + found = &languageRows[i] + break + } + } + } + if found == nil { + continue + } + + found.DefaultLanguage = &p.lang + found.LanguageList = []string{p.lang} + } + + data, err := allowedLanguage.Unmarshal(languageRows) + if err != nil { + return false, err.Error() + } + + filePath := filepath.Join(path, fileEntry.FileByteName+".bytes") + + f, err := os.OpenFile(filePath, os.O_RDWR, 0644) + if err != nil { + return false, err.Error() + } + defer f.Close() + + if _, err := f.Seek(int64(dataEntry.Offset), 0); err != nil { + return false, err.Error() + } + if _, err := f.Write(data); err != nil { + return false, err.Error() + } + + if len(data) < int(dataEntry.Size) { + remaining := int(dataEntry.Size) - len(data) + zeros := bytes.Repeat([]byte{0}, remaining) + if _, err := f.Write(zeros); err != nil { + return false, err.Error() + } + } + return true, "success" } diff --git a/pkg/constant/constant.go b/pkg/constant/constant.go index 6c99638..a950ae8 100644 --- a/pkg/constant/constant.go +++ b/pkg/constant/constant.go @@ -11,7 +11,7 @@ const LauncherFile = "firefly-launcher.exe" const TempUrl = "./temp" -const CurrentLauncherVersion = "1.3.0" +const CurrentLauncherVersion = "1.4.0" type ToolFile string diff --git a/pkg/language-patch/asset-meta/bytehash16.go b/pkg/language-patch/asset-meta/bytehash16.go new file mode 100644 index 0000000..eb0823c --- /dev/null +++ b/pkg/language-patch/asset-meta/bytehash16.go @@ -0,0 +1,30 @@ +package assetMeta + +import ( + "fmt" + "io" +) + +type ByteHash16 []byte + +func ByteHash16FromBytes(r io.ReadSeeker) (ByteHash16, error) { + fullHash := make([]byte, 16) + buf := make([]byte, 4) + for i := 0; i < 4; i++ { + if _, err := io.ReadFull(r, buf); err != nil { + return nil, err + } + for j := 0; j < 4; j++ { + fullHash[i*4+j] = buf[3-j] + } + } + return ByteHash16(fullHash), nil +} + +func (b ByteHash16) String() string { + s := "" + for _, v := range b { + s += fmt.Sprintf("%02x", v) + } + return s +} diff --git a/pkg/language-patch/asset-meta/dataEntry.go b/pkg/language-patch/asset-meta/dataEntry.go new file mode 100644 index 0000000..3943998 --- /dev/null +++ b/pkg/language-patch/asset-meta/dataEntry.go @@ -0,0 +1,28 @@ +package assetMeta + +import ( + "encoding/binary" + "io" +) + +type DataEntry struct { + NameHash int32 + Size uint32 + Offset uint32 +} + +func DataEntryFromBytes(r io.Reader) (*DataEntry, error) { + var d DataEntry + + if err := binary.Read(r, binary.BigEndian, &d.NameHash); err != nil { + return nil, err + } + if err := binary.Read(r, binary.BigEndian, &d.Size); err != nil { + return nil, err + } + if err := binary.Read(r, binary.BigEndian, &d.Offset); err != nil { + return nil, err + } + + return &d, nil +} diff --git a/pkg/language-patch/asset-meta/designIndex.go b/pkg/language-patch/asset-meta/designIndex.go new file mode 100644 index 0000000..c1576f3 --- /dev/null +++ b/pkg/language-patch/asset-meta/designIndex.go @@ -0,0 +1,98 @@ +package assetMeta + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "os" + "path/filepath" +) + + +type DesignIndex struct { + UnkI64 int64 + FileCount int32 + DesignDataCount int32 + FileList []FileEntry +} + + + +func (d *DesignIndex) FindDataAndFileByTarget(target int32) (DataEntry, FileEntry, error) { + for _, file := range d.FileList { + for _, entry := range file.DataEntries { + if entry.NameHash == target { + return entry, file, nil + } + } + } + return DataEntry{}, FileEntry{}, errors.New("not found") +} + + +func DesignIndexFromBytes(assetFolder string, indexHash string) (*DesignIndex, error) { + path := filepath.Join(assetFolder, fmt.Sprintf("DesignV_%s.bytes", indexHash)) + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + r := bytes.NewReader(data) + + var d DesignIndex + + if err := binary.Read(r, binary.BigEndian, &d.UnkI64); err != nil { + return nil, err + } + if err := binary.Read(r, binary.BigEndian, &d.FileCount); err != nil { + return nil, err + } + if err := binary.Read(r, binary.BigEndian, &d.DesignDataCount); err != nil { + return nil, err + } + + d.FileList = make([]FileEntry, 0, d.FileCount) + for i := int32(0); i < d.FileCount; i++ { + entry, err := FileEntryFromBytes(r) + if err != nil { + return nil, err + } + d.FileList = append(d.FileList, *entry) + } + + return &d, nil +} + + +func GetIndexHash(assetFolder string) (string, error) { + path := filepath.Join(assetFolder, "M_DesignV.bytes") + + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + + _, err = f.Seek(0x1C, 0) + if err != nil { + return "", err + } + + hash := make([]byte, 0x10) + index := 0 + for i := 0; i < 4; i++ { + chunk := make([]byte, 4) + _, err := f.Read(chunk) + if err != nil { + return "", err + } + + for bytePos := 3; bytePos >= 0; bytePos-- { + hash[index] = chunk[bytePos] + index++ + } + } + + return hex.EncodeToString(hash), nil +} \ No newline at end of file diff --git a/pkg/language-patch/asset-meta/fileEntry.go b/pkg/language-patch/asset-meta/fileEntry.go new file mode 100644 index 0000000..a7c1cdc --- /dev/null +++ b/pkg/language-patch/asset-meta/fileEntry.go @@ -0,0 +1,63 @@ +package assetMeta + +import ( + "encoding/binary" + "fmt" + "io" +) + +type FileEntry struct { + NameHash int32 + FileByteName string + Size int64 + DataCount int32 + DataEntries []DataEntry + Unk uint8 +} + +func FileEntryFromBytes(r io.Reader) (*FileEntry, error) { + var f FileEntry + + if err := binary.Read(r, binary.BigEndian, &f.NameHash); err != nil { + return nil, err + } + + buf := make([]byte, 16) + if _, err := io.ReadFull(r, buf); err != nil { + return nil, err + } + f.FileByteName = toHex(buf) + + if err := binary.Read(r, binary.BigEndian, &f.Size); err != nil { + return nil, err + } + if err := binary.Read(r, binary.BigEndian, &f.DataCount); err != nil { + return nil, err + } + + f.DataEntries = make([]DataEntry, 0, f.DataCount) + for i := int32(0); i < f.DataCount; i++ { + entry, err := DataEntryFromBytes(r) + if err != nil { + return nil, err + } + f.DataEntries = append(f.DataEntries, *entry) + } + + // read 1 byte + b := make([]byte, 1) + if _, err := r.Read(b); err != nil { + return nil, err + } + f.Unk = b[0] + + return &f, nil +} + +func toHex(buf []byte) string { + s := "" + for _, b := range buf { + s += fmt.Sprintf("%02x", b) + } + return s +} diff --git a/pkg/language-patch/asset-meta/miniAsset.go b/pkg/language-patch/asset-meta/miniAsset.go new file mode 100644 index 0000000..5b67910 --- /dev/null +++ b/pkg/language-patch/asset-meta/miniAsset.go @@ -0,0 +1,32 @@ +package assetMeta + +import ( + "encoding/binary" + "io" +) + +type MiniAsset struct { + RevisionID uint32 + DesignIndexHash ByteHash16 +} + +func MiniAssetFromBytes(r io.ReadSeeker) (*MiniAsset, error) { + if _, err := r.Seek(6*4, io.SeekCurrent); err != nil { + return nil, err + } + + var revID uint32 + if err := binary.Read(r, binary.LittleEndian, &revID); err != nil { + return nil, err + } + + hash, err := ByteHash16FromBytes(r) + if err != nil { + return nil, err + } + + return &MiniAsset{ + RevisionID: revID, + DesignIndexHash: hash, + }, nil +} diff --git a/pkg/language-patch/excel-language/patch.go b/pkg/language-patch/excel-language/patch.go new file mode 100644 index 0000000..8576c84 --- /dev/null +++ b/pkg/language-patch/excel-language/patch.go @@ -0,0 +1,114 @@ +package excelLanguage + +import ( + "bytes" + assetMeta "firefly-launcher/pkg/language-patch/asset-meta" + "io" + "os" + "path/filepath" +) + +type ExcelLanguage struct { + AssetFolder string + ExcelDataEntry *assetMeta.DataEntry + ExcelFileEntry *assetMeta.FileEntry +} + +func NewExcelLanguage(assetFolder string, dataEntry *assetMeta.DataEntry, fileEntry *assetMeta.FileEntry) *ExcelLanguage { + return &ExcelLanguage{ + AssetFolder: assetFolder, + ExcelDataEntry: dataEntry, + ExcelFileEntry: fileEntry, + } +} + +func (a *ExcelLanguage) Unmarshal(rows []LanguageRow) ([]byte, error) { + buf := new(bytes.Buffer) + + buf.WriteByte(0) + if err := writeI8Varint(buf, int8(len(rows))); err != nil { + return nil, err + } + + for _, row := range rows { + rowData, err := row.Unmarshal() + if err != nil { + return nil, err + } + if _, err := buf.Write(rowData); err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + +func (a *ExcelLanguage) Parse() ([]LanguageRow, error) { + excelPath := filepath.Join(a.AssetFolder, a.ExcelFileEntry.FileByteName+".bytes") + + f, err := os.Open(excelPath) + if err != nil { + return nil, err + } + defer f.Close() + + if _, err := f.Seek(int64(a.ExcelDataEntry.Offset), io.SeekStart); err != nil { + return nil, err + } + + buffer := make([]byte, a.ExcelDataEntry.Size) + if _, err := io.ReadFull(f, buffer); err != nil { + return nil, err + } + + reader := bytes.NewReader(buffer) + + _, _ = reader.ReadByte() // skip first byte + count, err := readI8Varint(reader) + if err != nil { + return nil, err + } + + rows := make([]LanguageRow, 0, count) + for i := 0; i < count; i++ { + bitmask, err := reader.ReadByte() + if err != nil { + return nil, err + } + + row := LanguageRow{} + + if bitmask&(1<<0) != 0 { + s, err := readString(reader) + if err != nil { + return nil, err + } + row.Area = &s + } + if bitmask&(1<<1) != 0 { + t, err := reader.ReadByte() + if err != nil { + return nil, err + } + row.Type = &t + } + if bitmask&(1<<2) != 0 { + arr, err := readStringArray(reader) + if err != nil { + return nil, err + } + row.LanguageList = arr + } + if bitmask&(1<<3) != 0 { + s, err := readString(reader) + if err != nil { + return nil, err + } + row.DefaultLanguage = &s + } + + rows = append(rows, row) + } + + return rows, nil +} diff --git a/pkg/language-patch/excel-language/row.go b/pkg/language-patch/excel-language/row.go new file mode 100644 index 0000000..51d648f --- /dev/null +++ b/pkg/language-patch/excel-language/row.go @@ -0,0 +1,81 @@ +package excelLanguage + +import ( + "bytes" + "errors" +) + +type LanguageRow struct { + Area *string + Type *uint8 + LanguageList []string + DefaultLanguage *string +} + +func (r *LanguageRow) Unmarshal() ([]byte, error) { + buf := new(bytes.Buffer) + + var bitmask uint8 + if r.Area != nil { + bitmask |= 1 << 0 + } + if r.Type != nil { + bitmask |= 1 << 1 + } + if len(r.LanguageList) > 0 { + bitmask |= 1 << 2 + } + if r.DefaultLanguage != nil { + bitmask |= 1 << 3 + } + + if err := buf.WriteByte(bitmask); err != nil { + return nil, err + } + + if r.Area != nil { + if err := writeString(buf, *r.Area); err != nil { + return nil, err + } + } + if r.Type != nil { + if err := buf.WriteByte(*r.Type); err != nil { + return nil, err + } + } + if len(r.LanguageList) > 0 { + if err := writeStringArray(buf, r.LanguageList); err != nil { + return nil, err + } + } + if r.DefaultLanguage != nil { + if err := writeString(buf, *r.DefaultLanguage); err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + +func writeString(buf *bytes.Buffer, s string) error { + if len(s) > 255 { + return errors.New("string too long") + } + if err := buf.WriteByte(uint8(len(s))); err != nil { + return err + } + _, err := buf.Write([]byte(s)) + return err +} + +func writeStringArray(buf *bytes.Buffer, arr []string) error { + if err := writeI8Varint(buf, int8(len(arr))); err != nil { + return err + } + for _, s := range arr { + if err := writeString(buf, s); err != nil { + return err + } + } + return nil +} diff --git a/pkg/language-patch/excel-language/utils.go b/pkg/language-patch/excel-language/utils.go new file mode 100644 index 0000000..e1fd4c6 --- /dev/null +++ b/pkg/language-patch/excel-language/utils.go @@ -0,0 +1,53 @@ +package excelLanguage + +import ( + "bytes" + "encoding/binary" + "io" +) + +func writeI8Varint(buf *bytes.Buffer, v int8) error { + uv := uint64((uint32(v) << 1) ^ uint32(v>>7)) // zigzag encode + b := make([]byte, binary.MaxVarintLen64) + n := binary.PutUvarint(b, uv) + _, err := buf.Write(b[:n]) + return err +} + +func readI8Varint(r *bytes.Reader) (int, error) { + uv, err := binary.ReadUvarint(r) + if err != nil { + return 0, err + } + // zigzag decode + v := int((uv >> 1) ^ uint64((int64(uv&1)<<63)>>63)) + return v, nil +} + +func readString(r *bytes.Reader) (string, error) { + l, err := r.ReadByte() + if err != nil { + return "", err + } + buf := make([]byte, l) + if _, err := io.ReadFull(r, buf); err != nil { + return "", err + } + return string(buf), nil +} + +func readStringArray(r *bytes.Reader) ([]string, error) { + length, err := readI8Varint(r) + if err != nil { + return nil, err + } + arr := make([]string, 0, length) + for i := 0; i < length; i++ { + s, err := readString(r) + if err != nil { + return nil, err + } + arr = append(arr, s) + } + return arr, nil +} \ No newline at end of file diff --git a/pkg/models/binary-version.go b/pkg/models/binary-version.go index 8fa0d66..a388e35 100644 --- a/pkg/models/binary-version.go +++ b/pkg/models/binary-version.go @@ -4,11 +4,13 @@ import ( "errors" "fmt" "os" + "regexp" "strconv" "strings" ) type BinaryVersion struct { + Name string Major int Minor int Patch int @@ -22,42 +24,47 @@ func ParseBinaryVersion(path string) (*BinaryVersion, error) { content := string(data) - dashPos := strings.LastIndex(content, "-") - if dashPos == -1 { + lastDash := strings.LastIndex(content, "-") + if lastDash == -1 { return nil, errors.New("no dash found in version string") } - - start := dashPos - 6 - if start < 0 { - start = 0 + + secondLastDash := strings.LastIndex(content[:lastDash], "-") + if secondLastDash == -1 { + return nil, errors.New("only one dash found in version string") } - versionSlice := content[start:] - end := strings.Index(versionSlice, "-") - if end == -1 { - end = len(versionSlice) - } - - versionStr := versionSlice[:end] - parts := strings.SplitN(versionStr, ".", 3) - if len(parts) != 3 { + versionSlice := content[secondLastDash+1 : lastDash] + re := regexp.MustCompile(`^([A-Za-z]+)([\d\.]+)$`) + matches := re.FindStringSubmatch(versionSlice) + if len(matches) < 3 { return nil, errors.New("invalid version format") } + binaryVersion := BinaryVersion{ + Name: matches[1], + } + numbers := strings.Split(matches[2], ".") - major, err := strconv.Atoi(parts[0]) - if err != nil { - return nil, err + if len(numbers) > 0 { + binaryVersion.Major, err = strconv.Atoi(numbers[0]) + if err != nil { + return nil, err + } } - minor, err := strconv.Atoi(parts[1]) - if err != nil { - return nil, err + if len(numbers) > 1 { + binaryVersion.Minor, err = strconv.Atoi(numbers[1]) + if err != nil { + return nil, err + } } - patch, err := strconv.Atoi(parts[2]) - if err != nil { - return nil, err + if len(numbers) > 2 { + binaryVersion.Patch, err = strconv.Atoi(numbers[2]) + if err != nil { + return nil, err + } } - return &BinaryVersion{major, minor, patch}, nil + return &binaryVersion, nil } func (v *BinaryVersion) String() string {