├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── common └── common.go ├── cross-compile.sh ├── go.mod ├── go.sum ├── main.go ├── operations ├── apply.go └── diff.go ├── reader ├── reader.go └── reader_test.go └── writer ├── writer.go └── writer_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: [ "master" ] 5 | pull_request: 6 | branches: [ "master" ] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Go 13 | uses: actions/setup-go@v3 14 | with: 15 | go-version: 1.19 16 | - name: Build 17 | run: go build -v ./... 18 | - name: Test 19 | run: go test -v ./... 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bin/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Varun Ramesh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UPS Tools 2 | [![Go](https://github.com/rameshvarun/ups/actions/workflows/go.yml/badge.svg)](https://github.com/rameshvarun/ups/actions/workflows/go.yml) 3 | 4 | ## Installation 5 | 6 | Builds can be downloaded from the [releases page](https://github.com/rameshvarun/ups/releases/latest). Put the executable somewhere in your PATH. If you have the Go toolchain setup, you can also run `go get github.com/rameshvarun/ups` to install from source. 7 | 8 | ## Commands 9 | 10 | ### `ups diff -b base -m modified -o output.ups` 11 | Create a UPS patch file. 12 | 13 | ### `ups apply -b base -p patch.ups -o output` 14 | Apply a UPS patch to a file. 15 | 16 | ## Links 17 | - [UPS Format Specification](http://individual.utoronto.ca/dmeunier/ups-spec.pdf) 18 | - [mGBA UPS Reader](https://github.com/mgba-emu/mgba/blob/master/src/util/patch-ups.c) 19 | - [UPS Patcher in .NET](http://www.romhacking.net/utilities/606/) 20 | -------------------------------------------------------------------------------- /common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type PatchData struct { 4 | InputFileSize uint64 5 | OutputFileSize uint64 6 | 7 | PatchBlocks []PatchBlock 8 | 9 | InputChecksum uint32 10 | OutputChecksum uint32 11 | } 12 | 13 | // PatchBlock represents a single change from the original file to the modified 14 | // file. 15 | type PatchBlock struct { 16 | RelativeOffset uint64 17 | Data []byte 18 | } 19 | 20 | var Signature = []byte{'U', 'P', 'S', '1'} 21 | -------------------------------------------------------------------------------- /cross-compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | TARGETS="linux/386 linux/amd64 linux/arm linux/arm64 windows/386 windows/amd64 windows/arm windows/arm64 darwin/amd64 darwin/arm64" 3 | 4 | rm -rf ./bin 5 | for target in $TARGETS; do 6 | GOOS=${target%/*} 7 | GOARCH=${target#*/} 8 | echo "Building for $GOOS/$GOARCH" 9 | GOOS=$GOOS GOARCH=$GOARCH go build -o "bin/$GOOS-$GOARCH/" 10 | ( 11 | cd bin/$GOOS-$GOARCH/ 12 | zip -r "../$GOOS-$GOARCH.zip" . 13 | ) 14 | rm -rf "bin/$GOOS-$GOARCH/" 15 | done -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rameshvarun/ups 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/urfave/cli/v2 v2.4.10 7 | ) -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 5 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 6 | github.com/urfave/cli/v2 v2.4.10 h1:4qBCceIE7UP0T1qwloKzyyt1k/FcVNl2V6HBroizVRE= 7 | github.com/urfave/cli/v2 v2.4.10/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 10 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/urfave/cli/v2" 9 | "github.com/rameshvarun/ups/operations" 10 | "github.com/rameshvarun/ups/reader" 11 | "github.com/rameshvarun/ups/writer" 12 | ) 13 | 14 | func main() { 15 | app := cli.NewApp() 16 | app.Name = "ups" 17 | app.Usage = "Utilities for manipulating / creating UPS patch files." 18 | 19 | app.Commands = []*cli.Command{ 20 | { 21 | Name: "apply", 22 | Usage: "Apply a .ups patch to a file.", 23 | Aliases: []string{"patch"}, 24 | Flags: []cli.Flag{ 25 | &cli.StringFlag{ 26 | Name: "base, b", 27 | Usage: "The base file on top of which to apply the patch.", 28 | }, 29 | &cli.StringFlag{ 30 | Name: "patch, p", 31 | Usage: "The patch file to apply.", 32 | }, 33 | &cli.StringFlag{ 34 | Name: "output, o", 35 | Usage: "The file in which to write the patched data.", 36 | }, 37 | }, 38 | Action: func(c *cli.Context) error { 39 | if c.String("base") == "" || c.String("patch") == "" || c.String("output") == "" { 40 | if c.String("base") == "" { 41 | fmt.Printf("Missing required argument 'base'.\n") 42 | } 43 | if c.String("patch") == "" { 44 | fmt.Printf("Missing required argument 'patch'.\n") 45 | } 46 | if c.String("output") == "" { 47 | fmt.Printf("Missing required argument 'output'.\n") 48 | } 49 | fmt.Println() 50 | 51 | err := cli.ShowCommandHelp(c, "apply") 52 | if err != nil { 53 | panic(err) 54 | } 55 | return cli.Exit("", 1) 56 | } 57 | 58 | base, err := ioutil.ReadFile(c.String("base")) 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | patchBytes, err := ioutil.ReadFile(c.String("patch")) 64 | if err != nil { 65 | panic(err) 66 | } 67 | patch, err := reader.ReadUPS(patchBytes) 68 | if err != nil { 69 | panic(err) 70 | } 71 | 72 | result, err := operations.Apply(base, patch, false) 73 | if err != nil { 74 | panic(err) 75 | } 76 | 77 | err = ioutil.WriteFile(c.String("output"), result, 0644) 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | return nil 83 | }, 84 | }, 85 | { 86 | Name: "diff", 87 | Usage: "Diff two files, creating a UPS patch.", 88 | Aliases: []string{"create"}, 89 | Flags: []cli.Flag{ 90 | &cli.StringFlag{ 91 | Name: "base, b", 92 | Usage: "The base file.", 93 | }, 94 | &cli.StringFlag{ 95 | Name: "modified, m", 96 | Usage: "The modified file.", 97 | }, 98 | &cli.StringFlag{ 99 | Name: "output, o", 100 | Usage: "The file in which to write the patch data.", 101 | }, 102 | }, 103 | Action: func(c *cli.Context) error { 104 | if c.String("base") == "" || c.String("modified") == "" || c.String("output") == "" { 105 | if c.String("base") == "" { 106 | fmt.Printf("Missing required argument 'base'.\n") 107 | } 108 | if c.String("modified") == "" { 109 | fmt.Printf("Missing required argument 'patch'.\n") 110 | } 111 | if c.String("output") == "" { 112 | fmt.Printf("Missing required argument 'output'.\n") 113 | } 114 | fmt.Println() 115 | 116 | err := cli.ShowCommandHelp(c, "diff") 117 | if err != nil { 118 | panic(err) 119 | } 120 | return cli.Exit("", 1) 121 | } 122 | 123 | base, err := ioutil.ReadFile(c.String("base")) 124 | if err != nil { 125 | panic(err) 126 | } 127 | 128 | modified, err := ioutil.ReadFile(c.String("modified")) 129 | if err != nil { 130 | panic(err) 131 | } 132 | 133 | // Create patch data and write it to file. 134 | patch := operations.Diff(base, modified) 135 | err = ioutil.WriteFile(c.String("output"), writer.WriteUPS(patch), 0644) 136 | if err != nil { 137 | panic(err) 138 | } 139 | 140 | return nil 141 | }, 142 | }, 143 | } 144 | 145 | err := app.Run(os.Args) 146 | if err != nil { 147 | panic(err) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /operations/apply.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "errors" 5 | "hash/crc32" 6 | 7 | "github.com/rameshvarun/ups/common" 8 | ) 9 | 10 | // Apply applies the patch data to the given base. 11 | func Apply(base []byte, patch *common.PatchData, skipCRC bool) ([]byte, error) { 12 | if uint64(len(base)) != patch.InputFileSize { 13 | return nil, errors.New("Base file did not have expected size.") 14 | } 15 | if !skipCRC && crc32.ChecksumIEEE(base) != patch.InputChecksum { 16 | return nil, errors.New("Base file did not have expected checksum") 17 | } 18 | 19 | output := make([]byte, patch.OutputFileSize) 20 | copy(output, base) 21 | 22 | pointer := 0 23 | for _, block := range patch.PatchBlocks { 24 | pointer += int(block.RelativeOffset) 25 | 26 | for _, b := range block.Data { 27 | if pointer >= len(base) { 28 | output[pointer] = b 29 | } else { 30 | output[pointer] = base[pointer] ^ b 31 | } 32 | pointer++ 33 | } 34 | 35 | pointer++ 36 | } 37 | 38 | if !skipCRC && crc32.ChecksumIEEE(output) != patch.OutputChecksum { 39 | return nil, errors.New("Patch result did not have expected checksum") 40 | } 41 | 42 | return output, nil 43 | } 44 | -------------------------------------------------------------------------------- /operations/diff.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "hash/crc32" 5 | 6 | "github.com/rameshvarun/ups/common" 7 | ) 8 | 9 | // Diff takes in a base buffer, a modified buffer, and returns a PatchData object 10 | // that can be used to write to a UPS file. 11 | func Diff(base []byte, modified []byte) *common.PatchData { 12 | // Cumulative list of blocks that we construct as we scan through the buffers. 13 | var blocks []common.PatchBlock 14 | 15 | // The end position of the last patch block that we saw. 16 | lastBlock := uint64(0) 17 | // The current block that we are constructing. `nil` if there is no current block. 18 | var currentBlock *struct { 19 | Data []byte 20 | Start uint64 21 | } 22 | 23 | for pointer := 0; pointer < len(modified); pointer++ { 24 | // Determine if the byte has been 'modified' 25 | var different bool 26 | if pointer >= len(base) { 27 | // If the output file is larger than the input, and the byte in this extended 28 | // region is non-zero, then it is 'modified' 29 | different = modified[pointer] != 0 30 | } else { 31 | // Otherwise, simply check if the byte is different. 32 | different = modified[pointer] != base[pointer] 33 | } 34 | 35 | if different { 36 | // If the current byte is modified, but we are not constructing a block, 37 | // we need to create an in-progress block, then add in the data. 38 | if currentBlock == nil { 39 | currentBlock = &struct { 40 | Data []byte 41 | Start uint64 42 | }{ 43 | Data: []byte{}, 44 | Start: uint64(pointer), 45 | } 46 | } 47 | 48 | // If we are constructing an in-progress block, just add in the data. 49 | if currentBlock != nil { 50 | if pointer >= len(base) { 51 | currentBlock.Data = append(currentBlock.Data, modified[pointer]) 52 | } else { 53 | currentBlock.Data = append(currentBlock.Data, base[pointer]^modified[pointer]) 54 | } 55 | } 56 | } else { 57 | if currentBlock != nil { 58 | // This block has ended. 59 | blocks = append(blocks, common.PatchBlock{ 60 | Data: currentBlock.Data, 61 | RelativeOffset: currentBlock.Start - lastBlock, 62 | }) 63 | currentBlock = nil 64 | 65 | // lastBlock needs to point to the byte after the unmodified byte that ended 66 | // the block. 67 | lastBlock = uint64(pointer) + 1 68 | } 69 | } 70 | } 71 | 72 | // If we ended the loop on a block, then we need to end that block. 73 | if currentBlock != nil { 74 | blocks = append(blocks, common.PatchBlock{ 75 | Data: currentBlock.Data, 76 | RelativeOffset: currentBlock.Start - lastBlock, 77 | }) 78 | } 79 | 80 | // Return the full patch data structure. 81 | return &common.PatchData{ 82 | InputFileSize: uint64(len(base)), 83 | OutputFileSize: uint64(len(modified)), 84 | PatchBlocks: blocks, 85 | InputChecksum: crc32.ChecksumIEEE(base), 86 | OutputChecksum: crc32.ChecksumIEEE(modified), 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /reader/reader.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "hash/crc32" 8 | "io" 9 | 10 | "github.com/rameshvarun/ups/common" 11 | ) 12 | 13 | // ReadUPS takes a file object and reads it into a PatchData data structure. 14 | func ReadUPS(data []byte) (*common.PatchData, error) { 15 | reader := bytes.NewReader(data) 16 | 17 | // Verify Patch through Checksum 18 | patchSumExpected := crc32.ChecksumIEEE(data[:len(data)-4]) 19 | var patchSumActual uint32 20 | err := binary.Read(bytes.NewBuffer(data[len(data)-4:]), binary.LittleEndian, &patchSumActual) 21 | if err != nil { 22 | return nil, err 23 | } 24 | if patchSumExpected != patchSumActual { 25 | return nil, errors.New("Patch failed checksum.") 26 | } 27 | 28 | // Read and validate the signature. 29 | signature := make([]byte, 4) 30 | _, err = io.ReadAtLeast(reader, signature, 4) 31 | if err != nil { 32 | return nil, err 33 | } 34 | if !bytes.Equal(signature, common.Signature) { 35 | return nil, errors.New("File did not have valid UPS signature.") 36 | } 37 | 38 | // Read the input and output file sizes. 39 | inputFileSize, err := ReadVariableLengthInteger(reader) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | // Read the input and output file sizes. 45 | outputFileSize, err := ReadVariableLengthInteger(reader) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | // Read in the Patch Blocks 51 | var patchBlocks []common.PatchBlock 52 | for reader.Len() > 12 { 53 | // First, read in the offset from the previous block (or beginning of file) 54 | // as a variable-length integer. 55 | relativeOffset, err := ReadVariableLengthInteger(reader) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | // Read in the NULL-terminated data portion. 61 | var data []byte 62 | for { 63 | b, err := reader.ReadByte() 64 | if err != nil { 65 | return nil, err 66 | } 67 | if b == 0 { 68 | break 69 | } 70 | data = append(data, b) 71 | } 72 | 73 | // Add this to the Patch blocks splice. 74 | patchBlocks = append(patchBlocks, common.PatchBlock{ 75 | RelativeOffset: relativeOffset, 76 | Data: data, 77 | }) 78 | } 79 | 80 | // Read the checksum of the input file. 81 | var inputChecksum uint32 82 | err = binary.Read(reader, binary.LittleEndian, &inputChecksum) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | // Read in the checksum of the output file. 88 | var outputChecksum uint32 89 | err = binary.Read(reader, binary.LittleEndian, &outputChecksum) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | // What's left should just be the patch checksum. 95 | if reader.Len() != 4 { 96 | return nil, errors.New("File was longer than expected.") 97 | } 98 | 99 | return &common.PatchData{ 100 | InputFileSize: inputFileSize, 101 | OutputFileSize: outputFileSize, 102 | PatchBlocks: patchBlocks, 103 | InputChecksum: inputChecksum, 104 | OutputChecksum: outputChecksum, 105 | }, nil 106 | } 107 | 108 | // ReadVariableLengthInteger reads a variable-length encoded integer. 109 | // Based off of https://github.com/mgba-emu/mgba/blob/31993afd2a9bcadda690248f77d0f62363b82b51/src/util/patch-ups.c#L208 110 | func ReadVariableLengthInteger(reader io.ByteReader) (uint64, error) { 111 | value := uint64(0) 112 | shift := uint64(1) 113 | 114 | for true { 115 | b, err := reader.ReadByte() 116 | if err != nil { 117 | return 0, err 118 | } 119 | 120 | value += (uint64(b&0x7F) * shift) 121 | if b&0x80 != 0 { 122 | break 123 | } 124 | shift <<= 7 125 | value += shift 126 | } 127 | return value, nil 128 | } 129 | -------------------------------------------------------------------------------- /reader/reader_test.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestReadVariableLengthInteger(t *testing.T) { 9 | i, err := ReadVariableLengthInteger(bytes.NewReader([]byte{0x00, 0x7f, 0x7e, 0x82})) 10 | if err != nil { 11 | t.Error(err) 12 | } 13 | 14 | if i != 8388608 { 15 | t.Fail() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /writer/writer.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "hash/crc32" 7 | 8 | "github.com/rameshvarun/ups/common" 9 | ) 10 | 11 | // WriteUPS writes UPS data to a byte array. 12 | func WriteUPS(data *common.PatchData) []byte { 13 | var buffer bytes.Buffer 14 | 15 | // Write signature. 16 | buffer.Write(common.Signature) 17 | 18 | // Write input and output file sizes. 19 | buffer.Write(WriteVariableLengthInteger(data.InputFileSize)) 20 | buffer.Write(WriteVariableLengthInteger(data.OutputFileSize)) 21 | 22 | for _, block := range data.PatchBlocks { 23 | buffer.Write(WriteVariableLengthInteger(block.RelativeOffset)) 24 | buffer.Write(block.Data) 25 | buffer.WriteByte(0) 26 | } 27 | 28 | // Write the input and output checksums. 29 | binary.Write(&buffer, binary.LittleEndian, data.InputChecksum) 30 | binary.Write(&buffer, binary.LittleEndian, data.OutputChecksum) 31 | 32 | // Checksum the buffer so far. 33 | checksum := crc32.ChecksumIEEE(buffer.Bytes()) 34 | binary.Write(&buffer, binary.LittleEndian, checksum) 35 | 36 | return buffer.Bytes() 37 | } 38 | 39 | // WriteVariableLengthInteger writes a variable-length encoded integer. 40 | // based off of the source for http://www.romhacking.net/utilities/606/ 41 | func WriteVariableLengthInteger(value uint64) []byte { 42 | var data []byte 43 | x := value & 0x7f 44 | value >>= 7 45 | 46 | for value != 0 { 47 | data = append(data, byte(x)) 48 | value-- 49 | x = value & 0x7f 50 | value >>= 7 51 | } 52 | data = append(data, byte(0x80|x)) 53 | return data 54 | } 55 | -------------------------------------------------------------------------------- /writer/writer_test.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestWriteVariableLengthInteger(t *testing.T) { 10 | // Test cases. 11 | cases := []struct { 12 | Input uint64 13 | Expected []byte 14 | }{{ 15 | Input: 2, 16 | Expected: []byte{0x82}, 17 | }, { 18 | Input: 8388608, 19 | Expected: []byte{0x00, 0x7f, 0x7e, 0x82}, 20 | }} 21 | 22 | // Iterate through the test cases. 23 | for _, test := range cases { 24 | out := WriteVariableLengthInteger(test.Input) 25 | if !bytes.Equal(test.Expected, out) { 26 | fmt.Printf("Expected {%x} to be {%x}\n", out, test.Expected) 27 | t.Fail() 28 | } 29 | } 30 | 31 | } 32 | --------------------------------------------------------------------------------