From 1d3db9278d6d7f1cf72419489a793c0545138f91 Mon Sep 17 00:00:00 2001 From: starainrt Date: Thu, 23 Oct 2025 21:50:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=A7=A3=E7=A0=81=E5=AE=BD?= =?UTF-8?q?=E6=9D=BE=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/asar/main.go | 4 ++- decoder.go | 48 ++++++++++++++++++++++++--- decoder_test.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++++ header.go | 60 +++++++++++++++++++++++++++++++--- 4 files changed, 186 insertions(+), 10 deletions(-) diff --git a/cmd/asar/main.go b/cmd/asar/main.go index 59cdc0a..d2047d8 100644 --- a/cmd/asar/main.go +++ b/cmd/asar/main.go @@ -35,7 +35,9 @@ func main() { } defer file.Close() - root, err := asar.Decode(file) + root, err := asar.DecodeWithOptions(file, asar.DecodeOptions{ + StrictValidation: false, + }) if err != nil { fmt.Fprintf(os.Stderr, "asar: %s\n", err) os.Exit(1) diff --git a/decoder.go b/decoder.go index e2bacc2..72d4fd0 100644 --- a/decoder.go +++ b/decoder.go @@ -3,6 +3,7 @@ package asar // import "b612.me/asar" import ( "encoding/binary" "errors" + "fmt" "io" ) @@ -10,11 +11,27 @@ var ( errMalformed = errors.New("asar: malformed archive") ) +// DecodeOptions 用于控制解码行为 +type DecodeOptions struct { + // StrictValidation 如果为 false,会忽略一些格式验证错误和尾部填充 + StrictValidation bool +} + +// DefaultDecodeOptions 默认选项(严格模式) +var DefaultDecodeOptions = DecodeOptions{ + StrictValidation: true, +} + // Decode decodes the ASAR archive in ra. // // Returns the root element and nil on success. nil and an error is returned on // failure. func Decode(ra io.ReaderAt) (*Entry, error) { + return DecodeWithOptions(ra, DefaultDecodeOptions) +} + +// DecodeWithOptions 带选项的解码函数 +func DecodeWithOptions(ra io.ReaderAt, opts DecodeOptions) (*Entry, error) { headerSize := uint32(0) headerStringSize := uint32(0) @@ -43,19 +60,42 @@ func Decode(ra io.ReaderAt) (*Entry, error) { } headerObjectSize := binary.LittleEndian.Uint32(buff[:4]) - if headerObjectSize != headerSize-4 { - return nil, errMalformed + + // 在宽松模式下放宽验证 + if opts.StrictValidation { + if headerObjectSize != headerSize-4 { + return nil, errMalformed + } + } else { + // 宽松模式:允许一定的偏差 + if headerObjectSize > headerSize || headerObjectSize < headerSize-20 { + return nil, fmt.Errorf("asar: header object size %d is out of reasonable range (header size: %d)", + headerObjectSize, headerSize) + } } headerStringSize = binary.LittleEndian.Uint32(buff[4:8]) } + // 在宽松模式下,尝试找到实际的 JSON 结束位置 + actualHeaderSize := int64(headerStringSize) + if !opts.StrictValidation && headerStringSize > 0 { + // 读取 header 数据来检测实际的 JSON 结束 + tempBuf := make([]byte, headerStringSize) + if n, _ := ra.ReadAt(tempBuf, 16); n > 0 { + // 找到最后一个有效的 JSON 结束位置 + if endPos := findJSONEnd(tempBuf); endPos > 0 { + actualHeaderSize = int64(endPos) + } + } + } + // read header string - headerSection := io.NewSectionReader(ra, 8+8, int64(headerStringSize)) + headerSection := io.NewSectionReader(ra, 8+8, actualHeaderSize) baseOffset := 8 + int64(headerSize) baseOffset += baseOffset % 4 // pickle objects are uint32 aligned - root, err := decodeHeader(ra, headerSection, baseOffset) + root, err := decodeHeaderWithOptions(ra, headerSection, baseOffset, opts) if err != nil { return nil, err } diff --git a/decoder_test.go b/decoder_test.go index 551684e..b3867b0 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -89,3 +89,87 @@ func TestExtractThis(t *testing.T) { } } } + +func TestExtractThisWithOpt(t *testing.T) { + f, err := os.Open("testdata/extractthis.asar") + if err != nil { + t.Fatal(err) + } + defer f.Close() + + root, err := DecodeWithOptions(f, DecodeOptions{ + StrictValidation: false, + }) + if err != nil { + t.Fatal(err) + } + + if root.Flags&FlagDir == 0 { + t.Fatal("expecting root directory to have FlagDir") + } + + { + f1 := root.Find("dir1", "file1.txt") + if f1 == nil { + t.Fatal("could not find dir1/file1.txt") + } + if f1.Path() != "dir1/file1.txt" { + t.Fatal("unexpected path") + } + body, err := ioutil.ReadAll(f1.Open()) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(body, []byte(`file one.`)) { + t.Fatalf("dir1/file1.txt body is incorrect (got %s)", body) + } + } + + { + f2 := root.Find("dir2").Find("file3.txt") + if f2 == nil { + t.Fatal("could not find dir2/file3.txt") + } + s := f2.String() + if s != `123` { + t.Fatalf("dir2/file3.txt body is incorrect (got %s)", s) + } + } + + { + empty := root.Find("emptyfile.txt") + if empty == nil { + t.Fatal("could not find emptyfile.txt") + } + if len(empty.Bytes()) != 0 { + t.Fatal("expecting emptyfile.txt to be empty") + } + } + + { + var i int + root.Walk(func(_ string, _ os.FileInfo, _ error) error { + i++ + return nil + }) + + if i != 7 { + t.Fatalf("expected to walk over 7 items, got %d", i) + } + } + + { + var i int + root.Walk(func(_ string, fi os.FileInfo, _ error) error { + i++ + if fi.IsDir() { + return filepath.SkipDir + } + return nil + }) + + if i != 4 { + t.Fatalf("expected to walk over 4 items, got %d", i) + } + } +} diff --git a/header.go b/header.go index 8ee9df9..3fcbedd 100644 --- a/header.go +++ b/header.go @@ -15,6 +15,7 @@ type jsonReader struct { BaseOffset int64 D *json.Decoder Token json.Token + Options DecodeOptions // 新增字段 } func (j *jsonReader) Peek() json.Token { @@ -128,9 +129,26 @@ func parseRoot(r *jsonReader) *Entry { r.ExpectStringVal("files") parseFiles(r, entry) r.ExpectDelim('}') - if r.Next() != nil { - panic(errHeader) + + // 关键修改:根据选项决定是否检查剩余内容 + if r.Options.StrictValidation { + // 严格模式:不允许有剩余数据 + if r.Next() != nil { + panic(errHeader) + } + } else { + // 宽松模式:消费并忽略剩余的 token + // 这些可能是 NUL 填充或其他无关数据 + for { + token := r.Next() + if token == nil { + break + } + // 可选:如果需要,可以在这里记录日志 + // log.Printf("Ignoring trailing token in ASAR header: %v", token) + } } + return entry } @@ -159,7 +177,8 @@ func parseEntry(r *jsonReader, parent *Entry) { } for !r.HasDelimRune('}') { - switch r.ExpectString() { + key := r.ExpectString() + switch key { case "files": child.Flags |= FlagDir parseFiles(r, child) @@ -176,7 +195,18 @@ func parseEntry(r *jsonReader, parent *Entry) { child.Flags |= FlagExecutable } default: - panic(errHeader) + if r.Options.StrictValidation { + // 严格模式:未知字段导致错误 + panic(errHeader) + } else { + // 宽松模式:跳过未知字段 + // 需要消费这个未知字段的值 + if token := r.Next(); token == nil { + panic(errHeader) + } + // 可选:记录日志 + // log.Printf("Ignoring unknown field '%s' in ASAR entry", key) + } } } @@ -189,13 +219,20 @@ func parseEntry(r *jsonReader, parent *Entry) { r.ExpectDelim('}') } +// 保留原有的 decodeHeader func decodeHeader(asar io.ReaderAt, header *io.SectionReader, offset int64) (entry *Entry, err error) { + return decodeHeaderWithOptions(asar, header, offset, DefaultDecodeOptions) +} + +// 新增带选项的版本 +func decodeHeaderWithOptions(asar io.ReaderAt, header *io.SectionReader, offset int64, opts DecodeOptions) (entry *Entry, err error) { decoder := json.NewDecoder(header) decoder.UseNumber() reader := jsonReader{ ASAR: asar, BaseOffset: offset, D: decoder, + Options: opts, // 传递选项 } defer func() { if r := recover(); r != nil { @@ -205,8 +242,21 @@ func decodeHeader(asar io.ReaderAt, header *io.SectionReader, offset int64) (ent panic(r) } } - }() entry = parseRoot(&reader) return } + +// findJSONEnd 找到 JSON 的实际结束位置(最后一个有意义的字符) +func findJSONEnd(data []byte) int { + // 从后往前找,跳过所有的 NUL 和空白字符 + for i := len(data) - 1; i >= 0; i-- { + b := data[i] + // 跳过 NUL 和空白字符 + if b != 0 && b != ' ' && b != '\n' && b != '\r' && b != '\t' { + // 找到了非空白字符,这应该是 JSON 的结束 + return i + 1 + } + } + return len(data) +}