diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..3aeebad --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,34 @@ +name: Build + +on: + push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 + + - name: Build + run: bash ./buildAllPlatforms.sh + + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: bin + path: bin/* + + - name: Release + if: startsWith(github.ref, 'refs/tags') + uses: softprops/action-gh-release@v1 + with: + files: + bin/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dc47c4c --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, hzyitc +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/amlCRC.go b/amlCRC.go new file mode 100644 index 0000000..56e7a58 --- /dev/null +++ b/amlCRC.go @@ -0,0 +1,9 @@ +package AmlImg + +import "hash/crc32" + +// NOTE: Diffenent from the standard CRC32 +func AmlCRC(crc uint32, p []byte) uint32 { + table := crc32.MakeTable(0xedb88320) + return ^crc32.Update(^crc, table, p) +} diff --git a/buildAllPlatforms.sh b/buildAllPlatforms.sh new file mode 100644 index 0000000..ce0191c --- /dev/null +++ b/buildAllPlatforms.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +PROGRAM="AmlImg" +CLI="./cli" +OUTPUT="bin/" +LDFLAGS="-s -w" + +ver="$(git describe --tags --match "v*" --dirty="" 2>/dev/null || git log -1 --pretty=format:"v0.0.0-%h" 2>/dev/null || echo "v0.0.0")" +[ -n "$(git status --porcelain |& grep -Ev '^\?\?')" ] && ver="$ver-$(date +"%Y%M%d-%H%m%S")" +LDFLAGS="$LDFLAGS -X main.version=$ver" + +mkdir -p "${OUTPUT}" +rm -f "${OUTPUT}/${PROGRAM}_"* + +platforms=( + linux/386 + linux/amd64 + linux/arm + linux/arm64 + linux/mips/softfloat + linux/mips64 + linux/mips64le + linux/mipsle/softfloat + windows/386 + windows/amd64 + windows/arm +) +# platforms=($(go tool dist list)) + +for i in "${platforms[@]}"; do + os="$(echo "$i" | awk -F/ '{print $1}')" + arch="$(echo "$i" | awk -F/ '{print $2}')" + mips="$(echo "$i" | awk -F/ '{print $3}')" + + [ "$os" == "windows" ] && ext="exe" + + filename="${OUTPUT}/${PROGRAM}_${ver}_${os}_${arch}${ext:+.$ext}" + echo "build $filename for $i" + CGO_ENABLED=0 GOOS="${os}" GOARCH="${arch}" GOMIPS="${mips}" \ + go build -trimpath -ldflags "$LDFLAGS" -o "${filename}" ${CLI} +done diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000..131b11e --- /dev/null +++ b/cli/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/hzyitc/AmlImg" +) + +var version = "v0.0.0" + +func usage() { + print(os.Args[0] + " (" + version + ")\n") + print("Usage:\n") + print(" " + os.Args[0] + " unpack \n") + print(" " + os.Args[0] + " pack \n") +} + +func main() { + if len(os.Args) != 4 { + usage() + return + } + + switch os.Args[1] { + case "unpack": + os.MkdirAll(os.Args[3], 755) + + err := unpack(os.Args[2], os.Args[3]) + if err != nil { + println(err.Error()) + return + } + + case "pack": + err := pack(os.Args[2], os.Args[3]) + if err != nil { + println(err.Error()) + return + } + + } +} + +func unpack(filePath, extractPath string) error { + img, err := AmlImg.NewReader(filePath, true) + if err != nil { + return errors.New("NewReader error: " + err.Error()) + } + defer img.Close() + + cmdfile, err := os.Create(extractPath + "/commands.txt") + if err != nil { + return errors.New("Create error: " + err.Error()) + } + defer cmdfile.Close() + + for i := 0; i < int(img.Header.ItemCount); i++ { + item := img.Items[i] + + filename := fmt.Sprintf("%d.%s.%s", item.Id, item.Name, item.Type) + if item.ImgType == AmlImg.ImgType_Sparse { + filename += ".sparse" + } + + println("Extracting ", extractPath+"/"+filename) + + imtType := "unknown" + if item.ImgType == AmlImg.ImgType_Normal { + imtType = "normal" + } else if item.ImgType == AmlImg.ImgType_Sparse { + imtType = "sparse" + } + fmt.Fprintf(cmdfile, "%s:%s:%s:%s\n", item.Type, item.Name, imtType, filename) + + file, err := os.Create(extractPath + "/" + filename) + if err != nil { + return errors.New("Create error:" + err.Error()) + } + + err = img.Seek(uint32(i), 0) + if err != nil { + file.Close() + return errors.New("Seek error:" + err.Error()) + } + + _, err = io.Copy(file, img) + if err != nil { + file.Close() + return errors.New("Copy error:" + err.Error()) + } + + file.Close() + } + + return nil +} + +func pack(filePath, dirPath string) error { + img, err := AmlImg.NewWriter() + if err != nil { + return errors.New("NewWriter error: " + err.Error()) + } + + cmdfile, err := os.Open(dirPath + "/commands.txt") + if err != nil { + return errors.New("Open error: " + err.Error()) + } + defer cmdfile.Close() + + scanner := bufio.NewScanner(cmdfile) + scanner.Split(bufio.ScanWords) + + for scanner.Scan() { + txt := scanner.Text() + if txt == "" { + continue + } else if strings.HasPrefix(txt, "#") { + continue + } + + c := strings.SplitN(txt, ":", 4) + Type := c[0] + Name := c[1] + filename := c[3] + + imgType := AmlImg.ImgType_Normal + switch c[2] { + case "normal": + imgType = AmlImg.ImgType_Normal + case "sparse": + imgType = AmlImg.ImgType_Sparse + default: + return errors.New("unknown imgType: " + c[2]) + } + + img.Add(Type, Name, imgType, func(w io.Writer) error { + println("Packing ", filename) + + file, err := os.Open(dirPath + "/" + filename) + if err != nil { + return errors.New("Open error: " + err.Error()) + } + + _, err = io.Copy(w, file) + return err + }) + } + + err = scanner.Err() + if err != nil { + return errors.New("scanner error: " + err.Error()) + } + + err = img.Write(filePath, 2) + if err != nil { + return errors.New("Write error: " + err.Error()) + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7daa7c4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/hzyitc/AmlImg + +go 1.16 diff --git a/header.go b/header.go new file mode 100644 index 0000000..88764fe --- /dev/null +++ b/header.go @@ -0,0 +1,30 @@ +package AmlImg + +import ( + "encoding/binary" + "io" +) + +const ( + Magic = uint32(0x27B51956) +) + +type Header struct { + CRC uint32 + Version uint32 + Magic uint32 + Size uint64 + AlignSize uint32 + ItemCount uint32 + Reserved [36]byte +} + +func Header_Unpack(reader io.Reader) (*Header, error) { + header := Header{} + err := binary.Read(reader, binary.LittleEndian, &header) + return &header, err +} + +func (header *Header) Pack(writer io.Writer) error { + return binary.Write(writer, binary.LittleEndian, header) +} diff --git a/item.go b/item.go new file mode 100644 index 0000000..699789b --- /dev/null +++ b/item.go @@ -0,0 +1,123 @@ +package AmlImg + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" +) + +const ( + ImgType_Normal = uint32(0x00) + ImgType_Sparse = uint32(0xFE) +) + +type Item_v1 struct { + Id uint32 + ImgType uint32 + OffsetOfItem uint64 + OffsetOfImage uint64 + Size uint64 + Type [32]byte + Name [32]byte + Reserved [32]byte +} + +type Item_v2 struct { + Id uint32 + ImgType uint32 + OffsetOfItem uint64 + OffsetOfImage uint64 + Size uint64 + Type [256]byte + Name [256]byte + Reserved [32]byte +} + +type Item struct { + Id uint32 + ImgType uint32 + OffsetOfItem uint64 + OffsetOfImage uint64 + Size uint64 + Type string + Name string +} + +func Item_Unpack(reader io.Reader, version uint32) (*Item, error) { + if version == 1 { + d := Item_v1{} + err := binary.Read(reader, binary.LittleEndian, &d) + if err != nil { + return nil, err + } + + return &Item{ + d.Id, + d.ImgType, + d.OffsetOfItem, + d.OffsetOfImage, + d.Size, + string(bytes.TrimRight(d.Type[:], "\x00")), + string(bytes.TrimRight(d.Name[:], "\x00")), + }, nil + } else if version == 2 { + d := Item_v2{} + err := binary.Read(reader, binary.LittleEndian, &d) + if err != nil { + return nil, err + } + + return &Item{ + d.Id, + d.ImgType, + d.OffsetOfItem, + d.OffsetOfImage, + d.Size, + string(bytes.TrimRight(d.Type[:], "\x00")), + string(bytes.TrimRight(d.Name[:], "\x00")), + }, nil + } else { + return nil, fmt.Errorf("unsupport version: %d", version) + } +} + +func (item *Item) Pack(writer io.Writer, version uint32) error { + if version == 1 { + d := Item_v1{ + item.Id, + item.ImgType, + item.OffsetOfItem, + item.OffsetOfImage, + item.Size, + [32]byte{}, + [32]byte{}, + [32]byte{}, + } + copy(d.Type[:], []byte(item.Type)) + copy(d.Name[:], []byte(item.Name)) + if item.Type == "PARTITION" { + d.Reserved[1] = 1 // Unknown why + } + return binary.Write(writer, binary.LittleEndian, d) + } else if version == 2 { + d := Item_v2{ + item.Id, + item.ImgType, + item.OffsetOfItem, + item.OffsetOfImage, + item.Size, + [256]byte{}, + [256]byte{}, + [32]byte{}, + } + copy(d.Type[:], []byte(item.Type)) + copy(d.Name[:], []byte(item.Name)) + if item.Type == "PARTITION" { + d.Reserved[0] = 1 // Unknown why + } + return binary.Write(writer, binary.LittleEndian, d) + } else { + return fmt.Errorf("unsupport version: %d", version) + } +} diff --git a/reader.go b/reader.go new file mode 100644 index 0000000..17c124e --- /dev/null +++ b/reader.go @@ -0,0 +1,103 @@ +package AmlImg + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "os" +) + +type ImageReader struct { + file *os.File + + Header *Header + Items []*Item + + remain uint64 +} + +func NewReader(path string, check bool) (*ImageReader, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + + header, err := Header_Unpack(file) + if err != nil { + return nil, err + } + + if header.Magic != Magic { + return nil, fmt.Errorf("incorrect crc: should %08X but is %08X", Magic, header.Magic) + } + + if check { + _, err = file.Seek(4, io.SeekStart) + if err != nil { + return nil, err + } + + crc := uint32(0xffffffff) + var buf [4096]byte + for { + n, err := file.Read(buf[:]) + crc = AmlCRC(crc, buf[:n]) + if errors.Is(err, io.EOF) { + break + } else if err != nil { + return nil, err + } + } + + if header.CRC != crc { + return nil, fmt.Errorf("incorrect crc: should %08X but is %08X", header.CRC, crc) + } + } + + _, err = file.Seek(int64(binary.Size(Header{})), io.SeekStart) + if err != nil { + return nil, err + } + + items := make([]*Item, header.ItemCount) + for i := 0; i < int(header.ItemCount); i++ { + items[i], err = Item_Unpack(file, header.Version) + if err != nil { + return nil, err + } + } + + return &ImageReader{ + file, + header, + items, + 0, + }, nil +} + +func (r *ImageReader) Seek(id uint32, offset uint64) error { + item := r.Items[id] + _, err := r.file.Seek(int64(item.OffsetOfImage+offset), io.SeekStart) + r.remain = item.Size - offset + return err +} + +func (r *ImageReader) Read(b []byte) (int, error) { + if r.remain == 0 { + return 0, io.EOF + } + + size := cap(b) + if size > int(r.remain) { + size = int(r.remain) + } + + n, err := r.file.Read(b[:size]) + r.remain -= uint64(n) + return n, err +} + +func (r *ImageReader) Close() { + r.file.Close() +} diff --git a/writer.go b/writer.go new file mode 100644 index 0000000..76d6044 --- /dev/null +++ b/writer.go @@ -0,0 +1,147 @@ +package AmlImg + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "os" +) + +type ImageWriter struct { + items []*imageWriter_Item +} + +type imageWriter_Item struct { + Type string + Name string + imgType uint32 + callback func(w io.Writer) error +} + +func NewWriter() (*ImageWriter, error) { + return &ImageWriter{ + make([]*imageWriter_Item, 0), + }, nil +} + +func (w *ImageWriter) Add(Type string, Name string, imgType uint32, callback func(w io.Writer) error) { + w.items = append(w.items, &imageWriter_Item{ + Type, + Name, + imgType, + callback, + }) +} + +func (w *ImageWriter) Write(path string, version uint32) error { + file, err := os.Create(path) + if err != nil { + return err + } + + items_current := 0 + int64(binary.Size(Header{})) + item_size := 0 + if version == 1 { + item_size = binary.Size(Item_v1{}) + } else if version == 2 { + item_size = binary.Size(Item_v2{}) + } else { + return fmt.Errorf("unsupport version: %d", version) + } + data_current := items_current + int64(item_size*len(w.items)) + + for i, item := range w.items { + _, err := file.Seek(data_current, io.SeekStart) + if err != nil { + return err + } + + err = item.callback(file) + if err != nil { + return err + } + + current, err := file.Seek(0, io.SeekCurrent) + if err != nil { + return err + } + size := current - data_current + + if current%4 != 0 { + n, err := file.Write(make([]byte, 4-(current%4))) + if err != nil { + return err + } + current += int64(n) + } + + _, err = file.Seek(items_current, io.SeekStart) + if err != nil { + return err + } + + err = (&Item{ + Id: uint32(i), + ImgType: item.imgType, + OffsetOfItem: 0, + OffsetOfImage: uint64(data_current), + Size: uint64(size), + Type: item.Type, + Name: item.Name, + }).Pack(file, version) + if err != nil { + return err + } + + items_current += int64(item_size) + data_current = current + } + + _, err = file.Seek(0, io.SeekStart) + if err != nil { + return err + } + + header := &Header{ + CRC: 0, + Version: version, + Magic: Magic, + Size: uint64(data_current), + AlignSize: 4, + ItemCount: uint32(len(w.items)), + } + err = header.Pack(file) + if err != nil { + return err + } + + _, err = file.Seek(4, io.SeekStart) + if err != nil { + return err + } + + crc := uint32(0xffffffff) + var buf [4096]byte + for { + n, err := file.Read(buf[:]) + crc = AmlCRC(crc, buf[:n]) + if errors.Is(err, io.EOF) { + break + } else if err != nil { + return err + } + } + + header.CRC = crc + _, err = file.Seek(0, io.SeekStart) + if err != nil { + return err + } + err = header.Pack(file) + if err != nil { + return err + } + + return nil +}