├── LICENSE.md ├── Makefile ├── README.md ├── create.go ├── extract.go ├── go.mod ├── go.sum └── main.go /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Synthesio Inc. 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | CGO_ENABLED=0 go build -o selfextract 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Selfextract 2 | 3 | Selfextract creates [self-extracting 4 | archives](https://en.wikipedia.org/wiki/Self-extracting_archive). 5 | 6 | When running such an archive, the files contained within are extracted to a 7 | destination directory (either temporary or persistent). The extracted files can 8 | contain a special "startup" script, in that case it will be automatically 9 | executed right after extraction. 10 | 11 | It is a proof-of-concept and has only been tested on Linux (i.e. when 12 | Selfextract is compiled as an ELF executable). 13 | 14 | ## Usage 15 | 16 | ### Create an archive 17 | 18 | The syntax is somewhat inspired from tar. 19 | 20 | ./selfextract [OPTION...] FILE ... 21 | -C string 22 | change dir before archiving files, only affects input files (default ".") 23 | -f string 24 | name of the archive to create (default "selfextract.out") 25 | -v verbose output 26 | 27 | Example: 28 | 29 | selfextract -f myarchive -C mydir . 30 | 31 | This command will create the `myarchive` archive in the current directory. It 32 | will contain the contents (`.`) of the `mydir` directory. 33 | 34 | ### Startup script 35 | 36 | The startup script that you want to run after extraction must be put in the 37 | archive just like any other file. By default it should be at the root and be 38 | named `selfextract_startup`. 39 | 40 | Example: 41 | 42 | mydir/ 43 | ├── a 44 | ├── b 45 | ├── c 46 | └── selfextract_startup 47 | 48 | If using the command given above to create the archive 49 | (`selfextract -C mydir .`), then the startup script will be automatically 50 | launched after the files have been extracted. 51 | 52 | Note that this is why we want to write `-C mydir .` and not just `mydir`, 53 | because in that latter case the `mydir` directory itself will be in the archive 54 | at the root, and the startup script will not be at the root anymore. 55 | 56 | ### Execute the archive 57 | 58 | The archive can of course be executed simply by running it. In that case, it 59 | will be extracted in a temporary directory. Then, if there is a startup script 60 | in the extracted files, it will be run. Finally, after the script exits, the 61 | temporary directory will be deleted. 62 | 63 | ./myarchive 64 | 65 | Of course this only makes sense if there is a startup script (otherwise the 66 | files will just be extracted then deleted right after). 67 | 68 | The archive can be configured with environment variables: 69 | 70 | - `SELFEXTRACT_DIR=` specifies a custom, persistent extraction directory 71 | (default: a temporary directory) 72 | - `SELFEXTRACT_STARTUP=` specifies the name of the startup script 73 | (default: "selfextract_startup") 74 | - `SELFEXTRACT_VERBOSE=true` activates debug messages (default: false) 75 | 76 | All the arguments passed on the command line will be passed to the startup 77 | script. 78 | 79 | SELFEXTRACT_DIR=extractdir ./myarchive -a 1 -b 2 80 | 81 | This will extract files into the `extractdir` directory, run the startup script 82 | (with the arguments `-a 1 -b 2`), and when it's done, exit *without* deleting 83 | the directory. 84 | 85 | If this command is run again, since the extraction directory already exists, it 86 | is reused, meaning the files will not be extracted again, only the startup 87 | script will be launched. This enables a huge speedup. 88 | 89 | ## Internals 90 | 91 | An archive made with `selfextract` consists of: 92 | 93 | - a **stub**, which is the executable part of the archive, to which is 94 | appended: 95 | - a **boundary**, a special value that marks the end of the executable 96 | - a unique **key**, to identify the archive 97 | - a **payload**, which is a zstd-compressed, tar-archived collection of files. 98 | 99 | ``` 100 | self-executable archive 101 | 0 ┌──────────────────────────────────┐ 102 | │ │ 103 | │ stub (executable) │ 104 | │ │ 105 | ├──────────────────────────────────┤ 106 | │ boundary │ 107 | ├──────────────────────────────────┤ 108 | │ key │ 109 | ├──────────────────────────────────┤ 110 | │ │ 111 | │ │ 112 | │ │ 113 | │ │ 114 | │ │ 115 | │ payload │ 116 | │ (zstd-compressed tar) │ 117 | │ │ 118 | │ │ 119 | │ │ 120 | │ │ 121 | │ │ 122 | └──────────────────────────────────┘ 123 | ``` 124 | 125 | When you append data to an ELF binary, testing has shown that it still runs 126 | completely fine. So, when the archive is executed, the program contained in the 127 | stub: 128 | 129 | - reads its own file to locate the boundary 130 | - reads the key and the payload that come right after the boundary 131 | - extracts the files contained in the payload 132 | - creates a `.selfextract.key` that contains the unique key of the archive 133 | - runs the startup script 134 | 135 | To avoid having to compile and distribute two different binaries (the CLI tool 136 | to create binaries, and the archive stub), they're actually the same. When 137 | creating an archive, Selfextract uses itself as the stub. It knows whether it is 138 | used to create a binary or as a stub depending on whether it finds the boundary. 139 | 140 | ### Extracting to a temporary directory 141 | 142 | If the path of the extraction directory is not specified, it defaults to a 143 | temporary directory (a uniquely-named directory in `/tmp`). The directory is 144 | automatically deleted after running. 145 | 146 | ```mermaid 147 | graph TD 148 | Start((Start)) --> Createtmp[Create a temporary directory] 149 | Createtmp --> Extract[Extract the files] 150 | Extract --> QStartupExists{Is there
a startup script?} 151 | QStartupExists -->|Yes| Run[Run script,
wait for it
to terminate] 152 | QStartupExists -->|No| DeleteDir[Delete the directory] 153 | Run --> DeleteDir 154 | DeleteDir --> End((End)) 155 | ``` 156 | 157 | ### Extracting to a named directory 158 | 159 | If an extraction directory is specified, it's a bit more complex. 160 | 161 | Before extraction, the archive checks the existence of a key file in the 162 | extraction dir to know if a previous extraction completed successfully (because 163 | the key file is the last file to be written). It also checks its contents to 164 | know if it was the same archive that was previously extracted (by matching the 165 | value of the key). If everything matches, we can reuse the directory and skip 166 | extraction. If not, we cleanup the directory and extract the files as normal. 167 | 168 | ```mermaid 169 | graph TD 170 | Start((Start)) --> QDirExists 171 | 172 | QDirExists{Does
the directory
exist?} -->|Yes| QContainsKeyFile 173 | QDirExists -->|No| CreateDir 174 | 175 | QContainsKeyFile{Does it contain a
.selfextract.key file?} -->|Yes| QKeyMatches 176 | QContainsKeyFile -->|No| Cleanup 177 | 178 | QKeyMatches{Does the
key file match the
archive's key?} -->|Yes| QStartupExists 179 | QKeyMatches -->|No| Cleanup 180 | 181 | Cleanup[Erase the directory's contents] --> Extract 182 | 183 | CreateDir[Create the directory] --> Extract 184 | 185 | Extract[Extract the files] --> CreateKeyFile 186 | CreateKeyFile[Create the .selfextract.key file] --> QStartupExists 187 | 188 | QStartupExists{Is there
a startup script?} -->|Yes| Run 189 | QStartupExists -->|No| End 190 | 191 | Run[Run script,
wait for it
to terminate] --> End 192 | 193 | End((End)) 194 | ``` 195 | -------------------------------------------------------------------------------- /create.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "io" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/klauspost/compress/zstd" 11 | ) 12 | 13 | func create(stub, key []byte, out string, files []string, cd string) { 14 | if len(files) == 0 { 15 | die("no files to archive") 16 | } 17 | 18 | f, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) 19 | if err != nil { 20 | die("opening output file:", err) 21 | } 22 | 23 | _, err = f.Write(stub) 24 | if err != nil { 25 | die("writing stub to output file:", err) 26 | } 27 | 28 | _, err = f.Write(generateBoundary()) 29 | if err != nil { 30 | die("writing boundary to output file:", err) 31 | } 32 | 33 | _, err = f.Write(generateRandomKey()) 34 | if err != nil { 35 | die("writing key to output file:", err) 36 | } 37 | 38 | zWrt, err := zstd.NewWriter(f, zstd.WithEncoderLevel(zstd.SpeedFastest)) 39 | if err != nil { 40 | die("creating zstd compressor:", err) 41 | } 42 | 43 | tarWrt := tar.NewWriter(zWrt) 44 | 45 | for _, file := range files { 46 | rootDir := os.DirFS(cd) 47 | file = filepath.Clean(file) 48 | // file may be a simple file or a directory, walkdir works for both 49 | fs.WalkDir(rootDir, file, func(path string, d fs.DirEntry, err error) error { 50 | if err != nil { 51 | die("opening input file", path, err) 52 | } 53 | if path == "." { 54 | return nil 55 | } 56 | debug("archiving", path) 57 | 58 | var hdr tar.Header 59 | hdr.Name = path 60 | 61 | info, err := d.Info() 62 | if err != nil { 63 | die("getting info about file:", path) 64 | } 65 | mode := info.Mode() 66 | hdr.Mode = int64(mode) 67 | 68 | switch mode.Type() { 69 | case fs.ModeDir: 70 | hdr.Typeflag = tar.TypeDir 71 | case fs.ModeSymlink: 72 | hdr.Typeflag = tar.TypeSymlink 73 | target, err := os.Readlink(filepath.Join(cd, path)) 74 | if err != nil { 75 | die("getting target of symlink:", path) 76 | } 77 | hdr.Linkname = target 78 | case 0: // regular file 79 | hdr.Typeflag = tar.TypeReg 80 | hdr.Size = info.Size() 81 | default: 82 | die("unsupported file type:", path) 83 | } 84 | 85 | err = tarWrt.WriteHeader(&hdr) 86 | if err != nil { 87 | die("writing tar header of file:", path) 88 | } 89 | 90 | if mode.Type() == 0 { 91 | wf, err := os.Open(filepath.Join(cd, path)) 92 | if err != nil { 93 | die("opening file:", path) 94 | } 95 | _, err = io.Copy(tarWrt, wf) 96 | if err != nil { 97 | die("writing file to tar:", path) 98 | } 99 | wf.Close() 100 | } 101 | 102 | return nil 103 | }) 104 | } 105 | 106 | err = tarWrt.Close() 107 | if err != nil { 108 | die("closing tar:", err) 109 | } 110 | err = zWrt.Close() 111 | if err != nil { 112 | die("closing zstd:", err) 113 | } 114 | err = f.Chmod(0755) 115 | if err != nil { 116 | die("making output file executable:", err) 117 | } 118 | err = f.Close() 119 | if err != nil { 120 | die("closing output file:", err) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /extract.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "encoding/hex" 6 | "errors" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "os/signal" 11 | "path/filepath" 12 | "strconv" 13 | "strings" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/klauspost/compress/zstd" 18 | ) 19 | 20 | const keyFileName = ".selfextract.key" 21 | 22 | type selfExtractor struct { 23 | extractDir string 24 | skipExtract bool 25 | tempDir bool 26 | payload io.ReadCloser 27 | key []byte 28 | exitCode chan int 29 | } 30 | 31 | func extract(payload io.ReadCloser, key []byte) { 32 | se := selfExtractor{ 33 | payload: payload, 34 | key: key, 35 | exitCode: make(chan int), 36 | } 37 | se.setupSignals() 38 | se.prepareExtractDir() 39 | se.extract() 40 | go se.startup() 41 | exit := <-se.exitCode 42 | se.cleanup() 43 | os.Exit(exit) 44 | } 45 | 46 | func (se *selfExtractor) setupSignals() { 47 | grace := 10 * time.Second 48 | if graceStr := os.Getenv(EnvGraceTimeout); graceStr != "" { 49 | graceFl, err := strconv.ParseFloat(graceStr, 32) 50 | if err == nil && graceFl >= 0 { 51 | grace = time.Duration(graceFl) * time.Second 52 | } 53 | } 54 | 55 | c := make(chan os.Signal, 1) 56 | signal.Notify(c, os.Interrupt, syscall.SIGABRT, syscall.SIGQUIT) 57 | 58 | go func() { 59 | <-c 60 | debug("got signal, waiting for grace timeout before exiting") 61 | if grace != 0 { 62 | time.Sleep(grace) 63 | } 64 | se.exitCode <- 2 65 | }() 66 | } 67 | 68 | func (se *selfExtractor) getTarReader() *tar.Reader { 69 | zRdr, err := zstd.NewReader(se.payload) 70 | if err != nil { 71 | die("creating zstd reader:", err) 72 | } 73 | 74 | return tar.NewReader(zRdr) 75 | } 76 | 77 | func (se *selfExtractor) prepareExtractDir() { 78 | extractDir := os.Getenv(EnvDir) 79 | 80 | if extractDir == "" { 81 | var err error 82 | se.extractDir, err = os.MkdirTemp("", "selfextract") 83 | if err != nil { 84 | die("creating temporary extraction directory:", err) 85 | } 86 | se.tempDir = true 87 | return 88 | } 89 | 90 | se.extractDir = extractDir 91 | 92 | stat, err := os.Stat(extractDir) 93 | // if there's an error, we'll assume that it's because the directory 94 | // doesn't exist, so we create it 95 | if err != nil { 96 | err = os.MkdirAll(extractDir, 0755) 97 | if err != nil { 98 | die("creating extraction directory:", err) 99 | } 100 | return 101 | } 102 | 103 | if !stat.IsDir() { 104 | die("extraction directory not a directory") 105 | } 106 | 107 | // At this point, we know extractDir is a pre-existing directory. 108 | // To continue, we request that it's either: 109 | // - empty 110 | // - containing a key file (and possibly other files) 111 | // If it's either, we assume it's safe to use it, possibly erasing the files 112 | // it contains. If it's neither, the extract dir path may have been set to 113 | // an existing non-empty directory by error, so as a safeguard we abort. 114 | 115 | entries, err := os.ReadDir(extractDir) 116 | if err != nil { 117 | die("listing extraction dir:", err) 118 | } 119 | if len(entries) == 0 { 120 | return 121 | } 122 | 123 | keyFile, err := os.Open(filepath.Join(extractDir, keyFileName)) 124 | if err != nil { 125 | die("opening key file (extraction dir must be empty or contain a valid key file):", err) 126 | } 127 | defer keyFile.Close() 128 | 129 | keyData, err := io.ReadAll(keyFile) 130 | if err != nil { 131 | die("reading key file (extraction dir must be empty or contain a valid key file):", err) 132 | } 133 | 134 | if hex.EncodeToString(se.key) == strings.TrimSpace(string(keyData)) { 135 | debug("extraction dir has matching key") 136 | se.skipExtract = true 137 | return 138 | } 139 | 140 | debug("key doesn't match, cleaning extraction dir") 141 | err = cleanupDir(extractDir) 142 | if err != nil { 143 | die("cleaning extraction dir:", err) 144 | } 145 | } 146 | 147 | // cleanupDir removes the contents of a directory but not the directory itself 148 | func cleanupDir(dir string) error { 149 | entries, err := os.ReadDir(dir) 150 | if err != nil { 151 | return err 152 | } 153 | for _, entry := range entries { 154 | err := os.RemoveAll(filepath.Join(dir, entry.Name())) 155 | if err != nil { 156 | return err 157 | } 158 | } 159 | return nil 160 | } 161 | 162 | func createFile(path string) (*os.File, error) { 163 | dir := filepath.Dir(path) 164 | err := os.MkdirAll(dir, 0755) 165 | if err != nil { 166 | return nil, err 167 | } 168 | f, err := os.Create(path) 169 | if err != nil { 170 | return nil, err 171 | } 172 | return f, nil 173 | } 174 | 175 | func cleanupAndDie(dir string, v ...interface{}) { 176 | err := cleanupDir(dir) 177 | if err != nil { 178 | die(append([]interface{}{"got error:", err, "while cleaning up after:"}, v...)) 179 | } 180 | die(v...) 181 | } 182 | 183 | func (se *selfExtractor) extract() { 184 | debug("using extraction dir", se.extractDir) 185 | 186 | if se.skipExtract { 187 | debug("skipping extraction") 188 | return 189 | } 190 | 191 | tarRdr := se.getTarReader() 192 | 193 | for { 194 | hdr, err := tarRdr.Next() 195 | if err == io.EOF { 196 | break 197 | } 198 | if err != nil { 199 | die("reading embedded tar:", err) 200 | } 201 | 202 | name := filepath.Clean(hdr.Name) 203 | if name == "." { 204 | continue 205 | } 206 | pathName := filepath.Join(se.extractDir, name) 207 | switch hdr.Typeflag { 208 | case tar.TypeReg: 209 | debug("extracting file", name, "of size", hdr.Size) 210 | f, err := createFile(pathName) 211 | if err != nil { 212 | cleanupAndDie(se.extractDir, "creating file:", err) 213 | } 214 | 215 | _, err = io.Copy(f, tarRdr) 216 | if err != nil { 217 | cleanupAndDie(se.extractDir, "writing file:", err) 218 | } 219 | 220 | err = f.Chmod(os.FileMode(hdr.Mode)) 221 | if err != nil { 222 | cleanupAndDie(se.extractDir, "setting mode of file:", err) 223 | } 224 | 225 | f.Close() 226 | case tar.TypeDir: 227 | debug("creating directory", name) 228 | // We choose to disregard directory permissions and use a default 229 | // instead. Custom permissions (e.g. read-only directories) are 230 | // complex to handle, both when extracting and also when cleaning 231 | // up the directory. 232 | err := os.Mkdir(pathName, 0755) 233 | if err != nil { 234 | cleanupAndDie(se.extractDir, "creating directory", err) 235 | } 236 | case tar.TypeSymlink: 237 | debug("creating symlink", name) 238 | err := os.Symlink(hdr.Linkname, pathName) 239 | if err != nil { 240 | cleanupAndDie(se.extractDir, "creating symlink", err) 241 | } 242 | default: 243 | cleanupAndDie(se.extractDir, "unsupported file type in tar", hdr.Typeflag) 244 | } 245 | } 246 | 247 | se.payload.Close() 248 | 249 | se.createKeyFile() 250 | } 251 | 252 | func (se *selfExtractor) createKeyFile() { 253 | f, err := os.Create(filepath.Join(se.extractDir, keyFileName)) 254 | if err != nil { 255 | die("creating key file:", err) 256 | } 257 | _, err = f.WriteString(hex.EncodeToString(se.key)) 258 | if err != nil { 259 | die("writing key file:", err) 260 | } 261 | err = f.Close() 262 | if err != nil { 263 | die("closing key file:", err) 264 | } 265 | } 266 | 267 | func (se *selfExtractor) startup() { 268 | if isTruthy(os.Getenv(EnvExtractOnly)) { 269 | debug("extract only mode, skipping startup") 270 | se.exitCode <- 0 271 | return 272 | } 273 | 274 | startup := os.Getenv(EnvStartup) 275 | if startup == "" { 276 | startup = "selfextract_startup" 277 | } 278 | debug("using startup script", startup) 279 | 280 | os.Setenv(EnvDir, se.extractDir) 281 | startupPath := filepath.Join(se.extractDir, startup) 282 | cmd := exec.Command(startupPath, os.Args[1:]...) 283 | cmd.Stdin = os.Stdin 284 | cmd.Stderr = os.Stderr 285 | cmd.Stdout = os.Stdout 286 | err := cmd.Run() 287 | if err != nil { 288 | debug("startup script ended with error:", err) 289 | var ex *exec.ExitError 290 | if errors.As(err, &ex) { 291 | se.exitCode <- ex.ExitCode() 292 | } else { 293 | se.exitCode <- 1 294 | } 295 | return 296 | } 297 | se.exitCode <- 0 298 | } 299 | 300 | func (se *selfExtractor) cleanup() { 301 | if se.tempDir { 302 | debug("removing extraction dir") 303 | os.RemoveAll(se.extractDir) 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/synthesio/selfextract 2 | 3 | go 1.18 4 | 5 | require github.com/klauspost/compress v1.13.4 6 | 7 | require github.com/golang/snappy v0.0.3 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= 2 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 3 | github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s= 4 | github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/sha512" 7 | "encoding/hex" 8 | "flag" 9 | "fmt" 10 | "io" 11 | "log" 12 | "os" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | var verbose bool 18 | 19 | const ( 20 | EnvVerbose = "SELFEXTRACT_VERBOSE" 21 | EnvDir = "SELFEXTRACT_DIR" 22 | EnvStartup = "SELFEXTRACT_STARTUP" 23 | EnvExtractOnly = "SELFEXTRACT_EXTRACT_ONLY" 24 | EnvGraceTimeout = "SELFEXTRACT_GRACE_TIMEOUT" 25 | ) 26 | 27 | func init() { 28 | verbose = isTruthy(os.Getenv(EnvVerbose)) 29 | } 30 | 31 | func main() { 32 | stub, payload, key := readSelf() 33 | 34 | if payload != nil { 35 | extract(payload, key) 36 | return 37 | } 38 | 39 | flag.Usage = func() { 40 | fmt.Fprintf(flag.CommandLine.Output(), "%s [OPTION...] FILE ...\n", os.Args[0]) 41 | flag.PrintDefaults() 42 | } 43 | createName := flag.String("f", "selfextract.out", "name of the archive to create") 44 | changeDir := flag.String("C", ".", "change dir before archiving files, only affects input files") 45 | verboseFlg := flag.Bool("v", false, "verbose output") 46 | flag.Parse() 47 | verbose = verbose || *verboseFlg 48 | 49 | create(stub, key, *createName, flag.Args(), *changeDir) 50 | } 51 | 52 | func debug(v ...interface{}) { 53 | if verbose { 54 | v = append([]interface{}{"selfextract:"}, v...) 55 | log.Println(v...) 56 | } 57 | } 58 | 59 | func die(v ...interface{}) { 60 | v = append([]interface{}{"selfextract: FATAL:"}, v...) 61 | log.Fatalln(v...) 62 | } 63 | 64 | func isTruthy(s string) bool { 65 | switch strings.ToLower(s) { 66 | case "y", "yes", "true", "1": 67 | return true 68 | default: 69 | return false 70 | } 71 | } 72 | 73 | func generateBoundary() []byte { 74 | h := sha512.Sum512([]byte("boundary")) 75 | return h[:] 76 | } 77 | 78 | const keyLength = 16 79 | 80 | func generateRandomKey() []byte { 81 | buf := make([]byte, keyLength) 82 | _, err := rand.Read(buf) 83 | if err != nil { 84 | die("generating random key:", err) 85 | } 86 | return buf 87 | } 88 | 89 | // maxBoundaryOffset is the offset at which we stop looking for a boundary, 90 | // it's just a failsafe mechanism against big, corrupted archives. We set it to 91 | // a value much bigger than the expected size of the compiled stub. 92 | const maxBoundaryOffset = 100e6 // 100 MB 93 | 94 | func readSelf() ([]byte, io.ReadCloser, []byte) { 95 | t := time.Now() 96 | exePath, err := os.Executable() 97 | if err != nil { 98 | panic(err) 99 | } 100 | self, err := os.Open(exePath) 101 | if err != nil { 102 | die("opening itself:", exePath, err) 103 | } 104 | debug("opened itself in", time.Since(t)) 105 | 106 | t = time.Now() 107 | buf := make([]byte, maxBoundaryOffset+keyLength) 108 | n, err := self.Read(buf) 109 | var bufFull bool 110 | if err == io.EOF { 111 | bufFull = true 112 | } else if err != nil { 113 | die("reading itself:", err) 114 | } 115 | buf = buf[:n] 116 | debug("read itself in", time.Since(t)) 117 | 118 | boundary := generateBoundary() 119 | t = time.Now() 120 | bdyOff := bytes.Index(buf[:maxBoundaryOffset], boundary) 121 | debug("boundary search completed in", time.Since(t)) 122 | 123 | if bdyOff == -1 { 124 | if bufFull { 125 | die("boundary not found before byte", maxBoundaryOffset) 126 | } 127 | debug("no boundary") 128 | self.Close() 129 | return buf, nil, nil 130 | } 131 | debug("boundary found at", bdyOff) 132 | 133 | keyOff := bdyOff + len(boundary) 134 | payloadOff := keyOff + keyLength 135 | key := buf[keyOff:payloadOff] 136 | debug("key:", hex.EncodeToString(key)) 137 | 138 | _, err = self.Seek(int64(payloadOff), os.SEEK_SET) 139 | if err != nil { 140 | die("seeking to start of payload:", err) 141 | } 142 | buf = buf[:bdyOff] 143 | 144 | return buf, self, key 145 | } 146 | --------------------------------------------------------------------------------