Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

s2sx: Limit max executable size #368

Merged
merged 1 commit into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion s2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ Usage: s2sx [options] file1 file2

Compresses all files supplied as input separately.
If files have '.s2' extension they are assumed to be compressed already.
Output files are written as 'filename.s2sfx' and with '.exe' for windows targets.
Output files are written as 'filename.s2sx' and with '.exe' for windows targets.
If output is big, an additional file with ".more" is written. This must be included as well.
By default output files will be overwritten.

Wildcards are accepted: testdir/*.txt will compress all files in testdir ending with .txt
Expand All @@ -244,6 +245,8 @@ Options:
Compress using this amount of threads (default 32)
-help
Display help
-max string
Maximum executable size. Rest will be written to another file. (default "1G")
-os string
Destination operating system (default "windows")
-q Don't write any output to terminal, except errors
Expand All @@ -267,6 +270,17 @@ Available platforms are:
* windows-386
* windows-amd64

By default, there is a size limit of 1GB for the output executable.

When this is exceeded the remaining file content is written to a file called
output+`.more`. This file must be included for a successful extraction and
placed alongside the executable for a successful extraction.

This file *must* have the same name as the executable, so if the executable is renamed,
so must the `.more` file.

This functionality is disabled with stdin/stdout.

### Self-extracting TAR files

If you wrap a TAR file you can specify `-untar` to make it untar on the destination host.
Expand Down
11 changes: 9 additions & 2 deletions s2/cmd/_s2sx/_unpack/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ func main() {
exitErr(err)
rd, err := newReader(f, stat.Size())
exitErr(err)
f2, err := os.Open(me + ".more")
if err == nil {
rd = io.MultiReader(rd, f2)
}
if !os.IsNotExist(err) {
exitErr(err)
}
var tmp [1]byte
_, err = io.ReadFull(rd, tmp[:])
exitErr(err)
Expand All @@ -69,8 +76,8 @@ func main() {
switch tmp[0] {
case opUnpack:
outname := me + "-extracted"
if idx := strings.Index(me, ".s2sfx"); idx > 0 {
// Trim from '.s2sfx'
if idx := strings.Index(me, ".s2sx"); idx > 0 {
// Trim from '.s2sx'
outname = me[:idx]
}
var out io.Writer
Expand Down
88 changes: 85 additions & 3 deletions s2/cmd/_s2sx/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import (
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"unicode"

"github.com/klauspost/compress/s2"
"github.com/klauspost/compress/s2/cmd/internal/readahead"
Expand All @@ -31,6 +33,7 @@ var (
goos = flag.String("os", runtime.GOOS, "Destination operating system")
goarch = flag.String("arch", runtime.GOARCH, "Destination architecture")
cpu = flag.Int("cpu", runtime.GOMAXPROCS(0), "Compress using this amount of threads")
max = flag.String("max", "1G", "Maximum executable size. Rest will be written to another file.")
safe = flag.Bool("safe", false, "Do not overwrite output files")
stdout = flag.Bool("c", false, "Write all output to stdout. Multiple input files will be concatenated")
remove = flag.Bool("rm", false, "Delete source file(s) after successful compression")
Expand All @@ -48,6 +51,8 @@ var embeddedFiles embed.FS
func main() {
flag.Parse()
args := flag.Args()
sz, err := toSize(*max)
exitErr(err)
if len(args) == 0 || *help {
_, _ = fmt.Fprintf(os.Stderr, "s2sx v%v, built at %v.\n\n", version, date)
_, _ = fmt.Fprintf(os.Stderr, "Copyright (c) 2011 The Snappy-Go Authors. All rights reserved.\n"+
Expand All @@ -58,7 +63,8 @@ Compresses all files supplied as input separately.
Use file name - to read from stdin and write to stdout.
If input is already s2 compressed it will just have the executable wrapped.
Use s2c commandline tool for advanced option, eg 'cat file.txt||s2c -|s2sx - >out.s2sx'
Output files are written as 'filename.s2sfx' and with '.exe' for windows targets.
Output files are written as 'filename.s2sx' and with '.exe' for windows targets.
If output is big, an additional file with ".more" is written. This must be included as well.
By default output files will be overwritten.

Wildcards are accepted: testdir/*.txt will compress all files in testdir ending with .txt
Expand Down Expand Up @@ -94,6 +100,10 @@ Options:`)
exec, err = ioutil.ReadAll(s2.NewReader(bytes.NewBuffer(exec)))
exitErr(err)

written := int64(0)
if int64(len(exec))+1 >= sz {
exitErr(fmt.Errorf("max size less than unpacker. Max size must be at least %d bytes", len(exec)+1))
}
mode := byte(opUnpack)
if *untar {
mode = opUnTar
Expand All @@ -107,6 +117,7 @@ Options:`)
exitErr(err)
_, err = os.Stdout.Write([]byte{mode})
exitErr(err)
written += int64(len(exec) + 1)
}

if stdIn {
Expand Down Expand Up @@ -139,7 +150,7 @@ Options:`)
for _, filename := range files {
func() {
var closeOnce sync.Once
dstFilename := fmt.Sprintf("%s%s", strings.TrimPrefix(filename, ".s2"), ".s2sfx")
dstFilename := fmt.Sprintf("%s%s", strings.TrimPrefix(filename, ".s2"), ".s2sx")
if *goos == "windows" {
dstFilename += ".exe"
}
Expand Down Expand Up @@ -172,7 +183,23 @@ Options:`)
dstFile, err := os.OpenFile(dstFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0777)
exitErr(err)
defer dstFile.Close()
bw := bufio.NewWriterSize(dstFile, 4<<20*2)
sw := &switchWriter{w: dstFile, left: sz, close: nil}
sw.fn = func() {
dstFilename := dstFilename + ".more"
if *safe {
_, err := os.Stat(dstFilename)
if !os.IsNotExist(err) {
exitErr(fmt.Errorf("destination '%s' file exists", dstFilename))
}
}
dstFile, err := os.OpenFile(dstFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0777)
exitErr(err)
sw.close = dstFile.Close
sw.w = dstFile
sw.left = (1 << 63) - 1
}
defer sw.Close()
bw := bufio.NewWriterSize(sw, 4<<20*2)
defer bw.Flush()
out = bw
_, err = out.Write(exec)
Expand Down Expand Up @@ -252,3 +279,58 @@ func isS2Input(rd io.Reader) (bool, io.Reader) {
exitErr(err)
return false, nil
}

// toSize converts a size indication to bytes.
func toSize(size string) (int64, error) {
size = strings.ToUpper(strings.TrimSpace(size))
firstLetter := strings.IndexFunc(size, unicode.IsLetter)
if firstLetter == -1 {
firstLetter = len(size)
}

bytesString, multiple := size[:firstLetter], size[firstLetter:]
bytes, err := strconv.ParseInt(bytesString, 10, 64)
if err != nil {
return 0, fmt.Errorf("unable to parse size: %v", err)
}

switch multiple {
case "G", "GB", "GIB":
return bytes * 1 << 20, nil
case "M", "MB", "MIB":
return bytes * 1 << 20, nil
case "K", "KB", "KIB":
return bytes * 1 << 10, nil
case "B", "":
return bytes, nil
default:
return 0, fmt.Errorf("unknown size suffix: %v", multiple)
}
}

type switchWriter struct {
w io.Writer
left int64
fn func()
close func() error
}

func (w *switchWriter) Write(b []byte) (int, error) {
if int64(len(b)) <= w.left {
w.left -= int64(len(b))
return w.w.Write(b)
}
n, err := w.w.Write(b[:w.left])
if err != nil {
return n, err
}
w.fn()
n2, err := w.Write(b[n:])
return n + n2, err
}
func (w *switchWriter) Close() error {
if w.close == nil {
return nil
}
return w.close()
}