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
+}