├── 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 |
--------------------------------------------------------------------------------