├── hello.swift ├── hello-world ├── internal ├── strdist │ ├── export_test.go │ ├── suite_test.go │ ├── log.go │ ├── strdist.go │ └── strdist_test.go ├── deb │ ├── export_test.go │ ├── suite_test.go │ ├── helpers.go │ ├── chrorder.go │ ├── chrorder │ │ └── main.go │ ├── log.go │ ├── helpers_test.go │ ├── version_test.go │ ├── version.go │ ├── extract.go │ └── extract_test.go ├── cache │ ├── suite_test.go │ ├── cache_test.go │ └── cache.go ├── control │ ├── suite_test.go │ ├── helpers.go │ ├── helpers_test.go │ ├── control_test.go │ └── control.go ├── archive │ ├── export_test.go │ ├── suite_test.go │ ├── log.go │ ├── testarchive │ │ └── testarchive.go │ └── archive.go ├── setup │ ├── suite_test.go │ ├── fetch_test.go │ ├── log.go │ ├── tarjan_test.go │ ├── tarjan.go │ └── fetch.go ├── fsutil │ ├── suite_test.go │ ├── log.go │ ├── create_test.go │ └── create.go ├── slicer │ ├── suite_test.go │ └── log.go ├── scripts │ ├── suite_test.go │ ├── log.go │ ├── scripts.go │ └── scripts_test.go ├── jsonwall │ ├── suite_test.go │ ├── log.go │ └── jsonwall_test.go └── testutil │ ├── export_test.go │ ├── reindent.go │ ├── base.go │ ├── treedump.go │ ├── reindent_test.go │ ├── filepresencechecker.go │ ├── testutil_test.go │ ├── filepresencechecker_test.go │ ├── intcheckers_test.go │ ├── intcheckers.go │ ├── filecontentchecker.go │ ├── exec_test.go │ ├── filecontentchecker_test.go │ ├── containschecker.go │ ├── exec.go │ ├── pkgdata_test.go │ └── containschecker_test.go ├── cmd ├── chisel │ ├── cmd_debug.go │ ├── export_test.go │ ├── cmd_version_test.go │ ├── cmd_version.go │ ├── main_test.go │ ├── log.go │ ├── cmd_cut.go │ └── cmd_help.go ├── version.go └── mkversion.sh ├── go.mod ├── README.md ├── LICENSE ├── Dockerfile └── examples ├── chiselled-base.dockerfile ├── c-example.dockerfile └── swift-example.dockerfile /hello.swift: -------------------------------------------------------------------------------- 1 | print("hello, world") 2 | -------------------------------------------------------------------------------- /hello-world: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTim/swift-chiselled/HEAD/hello-world -------------------------------------------------------------------------------- /internal/strdist/export_test.go: -------------------------------------------------------------------------------- 1 | package strdist 2 | 3 | 4 | var GlobCost = globCost 5 | -------------------------------------------------------------------------------- /internal/deb/export_test.go: -------------------------------------------------------------------------------- 1 | package deb 2 | 3 | func FakePlatformGoArch(goArch string) (restore func()) { 4 | saved := platformGoArch 5 | platformGoArch = goArch 6 | return func() { platformGoArch = saved } 7 | } 8 | -------------------------------------------------------------------------------- /internal/cache/suite_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | func Test(t *testing.T) { TestingT(t) } 10 | 11 | type S struct{} 12 | 13 | var _ = Suite(&S{}) 14 | -------------------------------------------------------------------------------- /internal/control/suite_test.go: -------------------------------------------------------------------------------- 1 | package control_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | func Test(t *testing.T) { TestingT(t) } 10 | 11 | type S struct{} 12 | 13 | var _ = Suite(&S{}) 14 | 15 | -------------------------------------------------------------------------------- /cmd/chisel/cmd_debug.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type cmdDebug struct{} 4 | 5 | var shortDebugHelp = "Run debug commands" 6 | var longDebugHelp = ` 7 | The debug command contains a selection of additional sub-commands. 8 | 9 | Debug commands can be removed without notice and may not work on 10 | non-development systems. 11 | ` 12 | -------------------------------------------------------------------------------- /internal/archive/export_test.go: -------------------------------------------------------------------------------- 1 | package archive 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func FakeDo(do func(req *http.Request) (*http.Response, error)) (restore func()) { 8 | _httpDo := httpDo 9 | _bulkDo := bulkDo 10 | httpDo = do 11 | bulkDo = do 12 | return func() { 13 | httpDo = _httpDo 14 | bulkDo = _bulkDo 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cmd/chisel/export_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var RunMain = run 4 | 5 | func FakeIsStdoutTTY(t bool) (restore func()) { 6 | oldIsStdoutTTY := isStdoutTTY 7 | isStdoutTTY = t 8 | return func() { 9 | isStdoutTTY = oldIsStdoutTTY 10 | } 11 | } 12 | 13 | func FakeIsStdinTTY(t bool) (restore func()) { 14 | oldIsStdinTTY := isStdinTTY 15 | isStdinTTY = t 16 | return func() { 17 | isStdinTTY = oldIsStdinTTY 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/archive/suite_test.go: -------------------------------------------------------------------------------- 1 | package archive_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | 8 | "github.com/canonical/chisel/internal/archive" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type S struct{} 14 | 15 | var _ = Suite(&S{}) 16 | 17 | func (s *S) SetUpTest(c *C) { 18 | archive.SetLogger(c) 19 | } 20 | 21 | func (s *S) TearDownTest(c *C) { 22 | archive.SetLogger(nil) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/chisel/cmd_version_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | 6 | chisel "github.com/canonical/chisel/cmd/chisel" 7 | ) 8 | 9 | func (s *ChiselSuite) TestVersionCommand(c *C) { 10 | restore := fakeVersion("4.56") 11 | defer restore() 12 | 13 | _, err := chisel.Parser().ParseArgs([]string{"version"}) 14 | c.Assert(err, IsNil) 15 | c.Assert(s.Stdout(), Equals, "4.56\n") 16 | c.Assert(s.Stderr(), Equals, "") 17 | } 18 | -------------------------------------------------------------------------------- /internal/deb/suite_test.go: -------------------------------------------------------------------------------- 1 | package deb_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | 8 | "github.com/canonical/chisel/internal/deb" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type S struct{} 14 | 15 | var _ = Suite(&S{}) 16 | 17 | func (s *S) SetUpTest(c *C) { 18 | deb.SetDebug(true) 19 | deb.SetLogger(c) 20 | } 21 | 22 | func (s *S) TearDownTest(c *C) { 23 | deb.SetDebug(false) 24 | deb.SetLogger(nil) 25 | } 26 | -------------------------------------------------------------------------------- /internal/setup/suite_test.go: -------------------------------------------------------------------------------- 1 | package setup_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | 8 | "github.com/canonical/chisel/internal/setup" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type S struct{} 14 | 15 | var _ = Suite(&S{}) 16 | 17 | func (s *S) SetUpTest(c *C) { 18 | setup.SetDebug(true) 19 | setup.SetLogger(c) 20 | } 21 | 22 | func (s *S) TearDownTest(c *C) { 23 | setup.SetDebug(false) 24 | setup.SetLogger(nil) 25 | } 26 | -------------------------------------------------------------------------------- /internal/fsutil/suite_test.go: -------------------------------------------------------------------------------- 1 | package fsutil_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | 8 | "github.com/canonical/chisel/internal/fsutil" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type S struct{} 14 | 15 | var _ = Suite(&S{}) 16 | 17 | func (s *S) SetUpTest(c *C) { 18 | fsutil.SetDebug(true) 19 | fsutil.SetLogger(c) 20 | } 21 | 22 | func (s *S) TearDownTest(c *C) { 23 | fsutil.SetDebug(false) 24 | fsutil.SetLogger(nil) 25 | } 26 | -------------------------------------------------------------------------------- /internal/slicer/suite_test.go: -------------------------------------------------------------------------------- 1 | package slicer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | 8 | "github.com/canonical/chisel/internal/slicer" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type S struct{} 14 | 15 | var _ = Suite(&S{}) 16 | 17 | func (s *S) SetUpTest(c *C) { 18 | slicer.SetDebug(true) 19 | slicer.SetLogger(c) 20 | } 21 | 22 | func (s *S) TearDownTest(c *C) { 23 | slicer.SetDebug(false) 24 | slicer.SetLogger(nil) 25 | } 26 | -------------------------------------------------------------------------------- /internal/scripts/suite_test.go: -------------------------------------------------------------------------------- 1 | package scripts_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | 8 | "github.com/canonical/chisel/internal/scripts" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type S struct{} 14 | 15 | var _ = Suite(&S{}) 16 | 17 | func (s *S) SetUpTest(c *C) { 18 | scripts.SetDebug(true) 19 | scripts.SetLogger(c) 20 | } 21 | 22 | func (s *S) TearDownTest(c *C) { 23 | scripts.SetDebug(false) 24 | scripts.SetLogger(nil) 25 | } 26 | -------------------------------------------------------------------------------- /internal/strdist/suite_test.go: -------------------------------------------------------------------------------- 1 | package strdist_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | 8 | "github.com/canonical/chisel/internal/strdist" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type S struct{} 14 | 15 | var _ = Suite(&S{}) 16 | 17 | func (s *S) SetUpTest(c *C) { 18 | strdist.SetDebug(true) 19 | strdist.SetLogger(c) 20 | } 21 | 22 | func (s *S) TearDownTest(c *C) { 23 | strdist.SetDebug(false) 24 | strdist.SetLogger(nil) 25 | } 26 | -------------------------------------------------------------------------------- /internal/jsonwall/suite_test.go: -------------------------------------------------------------------------------- 1 | package jsonwall_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | 8 | "github.com/canonical/chisel/internal/jsonwall" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type S struct{} 14 | 15 | var _ = Suite(&S{}) 16 | 17 | func (s *S) SetUpTest(c *C) { 18 | jsonwall.SetDebug(true) 19 | jsonwall.SetLogger(c) 20 | } 21 | 22 | func (s *S) TearDownTest(c *C) { 23 | jsonwall.SetDebug(false) 24 | jsonwall.SetLogger(nil) 25 | } 26 | -------------------------------------------------------------------------------- /cmd/chisel/cmd_version.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/jessevdk/go-flags" 8 | 9 | "github.com/canonical/chisel/cmd" 10 | ) 11 | 12 | var shortVersionHelp = "Show version details" 13 | var longVersionHelp = ` 14 | The version command displays the versions of the running client and server. 15 | ` 16 | 17 | type cmdVersion struct {} 18 | 19 | func init() { 20 | addCommand("version", shortVersionHelp, longVersionHelp, func() flags.Commander { return &cmdVersion{} }, nil, nil) 21 | } 22 | 23 | func (cmd cmdVersion) Execute(args []string) error { 24 | if len(args) > 0 { 25 | return ErrExtraArgs 26 | } 27 | 28 | return printVersions() 29 | } 30 | 31 | func printVersions() error { 32 | fmt.Fprintf(Stdout, "%s\n", cmd.Version) 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/canonical/chisel 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb 7 | github.com/jessevdk/go-flags v1.5.0 8 | github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b 9 | github.com/klauspost/compress v1.15.4 10 | github.com/ulikunitz/xz v0.5.10 11 | go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd 12 | golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 13 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c 14 | gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 15 | ) 16 | 17 | require ( 18 | github.com/kr/pretty v0.2.1 // indirect 19 | github.com/kr/text v0.1.0 // indirect 20 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect 21 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /internal/control/helpers.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | var pathInfoExp = regexp.MustCompile(`([a-f0-9]{32,}) +([0-9]+) +\S+`) 10 | 11 | func ParsePathInfo(table, path string) (digest string, size int, ok bool) { 12 | pos := strings.Index(table, " "+path+"\n") 13 | if pos == -1 { 14 | if !strings.HasSuffix(table, " " + path) { 15 | return "", -1, false 16 | } 17 | pos = len(table) - len(path) 18 | } else { 19 | pos++ 20 | } 21 | eol := pos + len(path) 22 | for pos > 0 && table[pos] != '\n' { 23 | pos-- 24 | } 25 | match := pathInfoExp.FindStringSubmatch(table[pos:eol]) 26 | if match == nil { 27 | return "", -1, false 28 | } 29 | size, err := strconv.Atoi(match[2]) 30 | if err != nil { 31 | panic("internal error: FindPathInfo regexp is wrong") 32 | } 33 | return match[1], size, true 34 | } 35 | -------------------------------------------------------------------------------- /internal/deb/helpers.go: -------------------------------------------------------------------------------- 1 | package deb 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | type archPair struct { 9 | goArch string 10 | debArch string 11 | } 12 | 13 | var knownArchs = []archPair{ 14 | {"386", "i386"}, 15 | {"amd64", "amd64"}, 16 | {"arm", "armhf"}, 17 | {"arm64", "arm64"}, 18 | {"ppc64le", "ppc64el"}, 19 | {"riscv64", "riscv64"}, 20 | {"s390x", "s390x"}, 21 | } 22 | 23 | var platformGoArch = runtime.GOARCH 24 | 25 | func InferArch() (string, error) { 26 | for _, arch := range knownArchs { 27 | if arch.goArch == platformGoArch { 28 | return arch.debArch, nil 29 | } 30 | } 31 | return "", fmt.Errorf("cannot infer package architecture from current platform architecture: %s", platformGoArch) 32 | } 33 | 34 | func ValidateArch(debArch string) error { 35 | for _, arch := range knownArchs { 36 | if arch.debArch == debArch { 37 | return nil 38 | } 39 | } 40 | return fmt.Errorf("invalid package architecture: %s", debArch) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package cmd 16 | 17 | //go:generate ./mkversion.sh 18 | 19 | // Version will be overwritten at build-time via mkversion.sh 20 | var Version = "unknown" 21 | 22 | func MockVersion(version string) (restore func()) { 23 | old := Version 24 | Version = version 25 | return func() { Version = old } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chiselled Swift Demo 2 | 3 | This is a demo of using [Chiselled Ubuntu Containers](https://canonical.com/blog/chiselled-containers-perfect-gift-cloud-applications) to run a Swift application. 4 | 5 | Chiselled containers are inspired by the Distroless concept and produce a tiny container image that contains only the application and its runtime dependencies. This is in contrast to the traditional approach of using a base image such as Ubuntu or Alpine Linux, which can be many times larger than the application itself. The hello world example in this repository is 13.2MB. 6 | 7 | They also include no package manager or shell, greatly reducing the attack surface of the container. 8 | 9 | To build the Swift demo run: 10 | 11 | ```bash 12 | cd examples 13 | docker build .. --build-arg UBUNTU_RELEASE=22.04 -t chisel:22.04 14 | docker build . --build-arg UBUNTU_RELEASE=22.04 -f chiselled-base.dockerfile -t chiselled-base:22.04 15 | docker build . --build-arg UBUNTU_RELEASE=22.04 -f swift-example.dockerfile -t hello-world 16 | docker run --rm -it hello-world 17 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tim Condon 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 | -------------------------------------------------------------------------------- /internal/testutil/export_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil 16 | 17 | import ( 18 | "gopkg.in/check.v1" 19 | ) 20 | 21 | func UnexpectedIntChecker(relation string) *intChecker { 22 | return &intChecker{CheckerInfo: &check.CheckerInfo{Name: "unexpected", Params: []string{"a", "b"}}, rel: relation} 23 | } 24 | 25 | func FakeShellcheckPath(p string) (restore func()) { 26 | old := shellcheckPath 27 | shellcheckPath = p 28 | return func() { 29 | shellcheckPath = old 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/testutil/reindent.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | // Reindent deindents the provided string and replaces tabs with spaces 9 | // so yaml inlined into tests works properly when decoded. 10 | func Reindent(in string) []byte { 11 | var buf bytes.Buffer 12 | var trim string 13 | var trimSet bool 14 | for _, line := range strings.Split(in, "\n") { 15 | if !trimSet { 16 | trimmed := strings.TrimLeft(line, "\t") 17 | if trimmed == "" { 18 | continue 19 | } 20 | if trimmed[0] == ' ' { 21 | panic("Space used in indent early in string:\n" + in) 22 | } 23 | 24 | trim = line[:len(line)-len(trimmed)] 25 | trimSet = true 26 | 27 | if trim == "" { 28 | return []uint8(strings.ReplaceAll(in, "\t", " ") + "\n") 29 | } 30 | } 31 | trimmed := strings.TrimPrefix(line, trim) 32 | if len(trimmed) == len(line) && strings.TrimLeft(line, "\t ") != "" { 33 | panic("Line not indented consistently:\n" + line) 34 | } 35 | trimmed = strings.ReplaceAll(trimmed, "\t", " ") 36 | buf.WriteString(trimmed) 37 | buf.WriteByte('\n') 38 | } 39 | return buf.Bytes() 40 | } 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_RELEASE=22.04 2 | 3 | # STAGE 1: Build Chisel using the Golang SDK 4 | FROM public.ecr.aws/lts/ubuntu:${UBUNTU_RELEASE} as builder 5 | RUN apt-get update && apt-get install -y golang dpkg-dev ca-certificates git 6 | WORKDIR /build/ 7 | ADD . /build/ 8 | RUN cd cmd && ./mkversion.sh 9 | RUN go build -o $(pwd) $(pwd)/cmd/chisel 10 | 11 | # STAGE 2: Create a chiselled Ubuntu base to then ship the chisel binary 12 | FROM public.ecr.aws/lts/ubuntu:${UBUNTU_RELEASE} as installer 13 | RUN apt-get update && apt-get install -y ca-certificates 14 | COPY --from=builder /build/chisel /usr/bin/ 15 | WORKDIR /rootfs 16 | RUN chisel cut --root /rootfs libc6_libs ca-certificates_data base-files_release-info 17 | 18 | # STAGE 3: Assemble the chisel binary + its chiselled Ubuntu dependencies 19 | FROM scratch 20 | COPY --from=installer ["/rootfs", "/"] 21 | COPY --from=builder /build/chisel /usr/bin/ 22 | ENTRYPOINT [ "/usr/bin/chisel" ] 23 | CMD [ "--help" ] 24 | 25 | # *** BUILD (run from the host, not from the DevContainer) *** 26 | # docker build . -t chisel:latest 27 | # 28 | # *** USAGE *** 29 | # mkdir chiselled 30 | # docker run -v $(pwd)/chiselled:/opt/output --rm chisel cut --release ubuntu-22.04 --root /opt/output/ libc6_libs ca-certificates_data 31 | # ls -la ./chiselled -------------------------------------------------------------------------------- /internal/deb/chrorder.go: -------------------------------------------------------------------------------- 1 | // auto-generated, DO NOT EDIT! 2 | package deb 3 | 4 | var chOrder = [...]int{ 5 | -5, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 6 | 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 7 | 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 8 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 314, 315, 316, 317, 318, 319, 9 | 320, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 10 | 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 347, 348, 349, 350, 351, 11 | 352, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 12 | 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 379, 380, 381, -10, 383, 13 | 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 14 | 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 15 | 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 16 | 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 17 | 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 18 | 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 19 | 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 20 | 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 21 | } 22 | -------------------------------------------------------------------------------- /internal/setup/fetch_test.go: -------------------------------------------------------------------------------- 1 | package setup_test 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/canonical/chisel/internal/setup" 11 | ) 12 | 13 | // TODO Implement local test server instead of using live repository. 14 | 15 | func (s *S) TestFetch(c *C) { 16 | options := &setup.FetchOptions{ 17 | Label: "ubuntu", 18 | Version: "22.04", 19 | CacheDir: c.MkDir(), 20 | } 21 | 22 | for fetch := 0; fetch < 3; fetch++ { 23 | release, err := setup.FetchRelease(options) 24 | c.Assert(err, IsNil) 25 | 26 | c.Assert(release.Path, Equals, filepath.Join(options.CacheDir, "releases", "ubuntu-22.04")) 27 | 28 | archive := release.Archives["ubuntu"] 29 | c.Assert(archive.Name, Equals, "ubuntu") 30 | c.Assert(archive.Version, Equals, "22.04") 31 | 32 | // Fetch multiple times and use a marker file inside 33 | // the release directory to check if caching is both 34 | // preserving and cleaning it when appropriate. 35 | markerPath := filepath.Join(release.Path, "test.marker") 36 | switch fetch { 37 | case 0: 38 | err := ioutil.WriteFile(markerPath, nil, 0644) 39 | c.Assert(err, IsNil) 40 | case 1: 41 | _, err := ioutil.ReadFile(markerPath) 42 | c.Assert(err, IsNil) 43 | 44 | err = ioutil.WriteFile(filepath.Join(release.Path, ".etag"), []byte("wrong"), 0644) 45 | c.Assert(err, IsNil) 46 | case 2: 47 | _, err := ioutil.ReadFile(markerPath) 48 | c.Assert(os.IsNotExist(err), Equals, true) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/chiselled-base.dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM chisel:22.04 as installer 3 | # This Dockerfile is building from the "chisel:22.04" image, which has been built using the "../Dockerfile" file with the argument "UBUNTU_RELEASE=22.04" 4 | # ``` docker build .. --build-arg UBUNTU_RELEASE=22.04 -t chisel:22.04 ``` 5 | 6 | WORKDIR /staging 7 | # Sets the working directory to "/staging" 8 | 9 | RUN ["chisel", "cut", "--root", "/staging", \ 10 | "base-files_base", \ 11 | "base-files_release-info", \ 12 | "ca-certificates_data", \ 13 | "libstdc++6_libs" ] 14 | # Runs the "chisel" command with the "cut" option, setting the root directory to "/staging" and cutting the specified files/directories 15 | 16 | FROM scratch 17 | # Starts a new build stage from the "scratch" image, which is an empty image with no files or libraries 18 | 19 | COPY --from=installer [ "/staging/", "/" ] 20 | # Copies the files from the previous build stage ("installer") at the "/staging" directory to the root directory ("/") of the new image 21 | 22 | # *** USAGE (run from the host, not from the DevContainer) *** 23 | # This provides the usage instructions for building the "chiselled-base:22.04" image from this Dockerfile: 24 | # 1. Build the "chisel:22.04" image using the "../Dockerfile" file with the argument "UBUNTU_RELEASE=22.04" 25 | # ``` docker build .. --build-arg UBUNTU_RELEASE=22.04 -t chisel:22.04 ``` 26 | # 2. Build the "chiselled-base:22.04" image using this Dockerfile 27 | # ``` docker build . -t chiselled-base:22.04 -f chiselled-base.dockerfile ``` 28 | -------------------------------------------------------------------------------- /internal/testutil/base.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil 16 | 17 | import ( 18 | "gopkg.in/check.v1" 19 | ) 20 | 21 | // BaseTest is a structure used as a base test suite for many of the pebble 22 | // tests. 23 | type BaseTest struct { 24 | cleanupHandlers []func() 25 | } 26 | 27 | // SetUpTest prepares the cleanup 28 | func (s *BaseTest) SetUpTest(c *check.C) { 29 | s.cleanupHandlers = nil 30 | } 31 | 32 | // TearDownTest cleans up the channel.ini files in case they were changed by 33 | // the test. 34 | // It also runs the cleanup handlers 35 | func (s *BaseTest) TearDownTest(c *check.C) { 36 | // run cleanup handlers and clear the slice 37 | for _, f := range s.cleanupHandlers { 38 | f() 39 | } 40 | s.cleanupHandlers = nil 41 | } 42 | 43 | // AddCleanup adds a new cleanup function to the test 44 | func (s *BaseTest) AddCleanup(f func()) { 45 | s.cleanupHandlers = append(s.cleanupHandlers, f) 46 | } 47 | -------------------------------------------------------------------------------- /internal/testutil/treedump.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "io/fs" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | func TreeDump(dir string) map[string]string { 13 | result := make(map[string]string) 14 | dirfs := os.DirFS(dir) 15 | err := fs.WalkDir(dirfs, ".", func(path string, d fs.DirEntry, err error) error { 16 | if err != nil { 17 | return fmt.Errorf("walk error: %w", err) 18 | } 19 | if path == "." { 20 | return nil 21 | } 22 | finfo, err := d.Info() 23 | if err != nil { 24 | return fmt.Errorf("cannot get stat info for %q: %w", path, err) 25 | } 26 | fperm := finfo.Mode() & fs.ModePerm 27 | ftype := finfo.Mode() & fs.ModeType 28 | if finfo.Mode()&fs.ModeSticky != 0 { 29 | fperm |= 01000 30 | } 31 | fpath := filepath.Join(dir, path) 32 | switch ftype { 33 | case fs.ModeDir: 34 | result["/"+path+"/"] = fmt.Sprintf("dir %#o", fperm) 35 | case fs.ModeSymlink: 36 | lpath, err := os.Readlink(fpath) 37 | if err != nil { 38 | return err 39 | } 40 | result["/"+path] = fmt.Sprintf("symlink %s", lpath) 41 | case 0: // Regular 42 | data, err := ioutil.ReadFile(fpath) 43 | if err != nil { 44 | return fmt.Errorf("cannot read file: %w", err) 45 | } 46 | var entry string 47 | if len(data) == 0 { 48 | entry = fmt.Sprintf("file %#o empty", fperm) 49 | } else { 50 | sum := sha256.Sum256(data) 51 | entry = fmt.Sprintf("file %#o %.4x", fperm, sum) 52 | } 53 | result["/"+path] = entry 54 | default: 55 | return fmt.Errorf("unknown file type %d: %s", ftype, fpath) 56 | } 57 | return nil 58 | }) 59 | if err != nil { 60 | panic(err) 61 | } 62 | return result 63 | } 64 | -------------------------------------------------------------------------------- /internal/deb/chrorder/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | ) 9 | 10 | var ( 11 | matchDigit = regexp.MustCompile("[0-9]").Match 12 | matchAlpha = regexp.MustCompile("[a-zA-Z]").Match 13 | ) 14 | 15 | func chOrder(ch uint8) int { 16 | // "~" is lower than everything else 17 | if ch == '~' { 18 | return -10 19 | } 20 | // empty is higher than "~" but lower than everything else 21 | if ch == 0 { 22 | return -5 23 | } 24 | if matchAlpha([]byte{ch}) { 25 | return int(ch) 26 | } 27 | 28 | // can only happen if cmpString sets '0' because there is no fragment 29 | if matchDigit([]byte{ch}) { 30 | return 0 31 | } 32 | 33 | return int(ch) + 256 34 | } 35 | 36 | func main() { 37 | var outFile string 38 | var pkgName string 39 | flag.StringVar(&outFile, "output", "-", "output file") 40 | flag.StringVar(&pkgName, "package", "foo", "package name") 41 | flag.Parse() 42 | 43 | out := os.Stdout 44 | if outFile != "" && outFile != "-" { 45 | var err error 46 | out, err = os.Create(outFile) 47 | if err != nil { 48 | fmt.Fprintf(os.Stderr, "error: %v", err) 49 | os.Exit(1) 50 | } 51 | defer out.Close() 52 | } 53 | 54 | if pkgName == "" { 55 | pkgName = "foo" 56 | } 57 | 58 | fmt.Fprintln(out, "// auto-generated, DO NOT EDIT!") 59 | fmt.Fprintf(out, "package %v\n", pkgName) 60 | fmt.Fprintf(out, "\n") 61 | fmt.Fprintln(out, "var chOrder = [...]int{") 62 | for i := 0; i < 16; i++ { 63 | fmt.Fprintf(out, "\t") 64 | for j := 0; j < 16; j++ { 65 | if j != 0 { 66 | fmt.Fprintf(out, " ") 67 | } 68 | fmt.Fprintf(out, "%d,", chOrder(uint8(i*16+j))) 69 | 70 | } 71 | fmt.Fprintf(out, "\n") 72 | } 73 | fmt.Fprintln(out, "}") 74 | } 75 | -------------------------------------------------------------------------------- /internal/testutil/reindent_test.go: -------------------------------------------------------------------------------- 1 | package testutil_test 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/canonical/chisel/internal/testutil" 7 | . "gopkg.in/check.v1" 8 | ) 9 | 10 | type reindentTest struct { 11 | raw, result, error string 12 | } 13 | 14 | var reindentTests = []reindentTest{{ 15 | raw: "a\nb", 16 | result: "a\nb", 17 | }, { 18 | raw: "\ta\n\tb", 19 | result: "a\nb", 20 | }, { 21 | raw: "a\n\tb\nc", 22 | result: "a\n b\nc", 23 | }, { 24 | raw: "a\n b\nc", 25 | result: "a\n b\nc", 26 | }, { 27 | raw: "\ta\n\t\tb\n\tc", 28 | result: "a\n b\nc", 29 | }, { 30 | raw: "\t a", 31 | error: "Space used in indent early in string:\n\t a", 32 | }, { 33 | raw: "\t a\n\t b\n\t c", 34 | error: "Space used in indent early in string:\n\t a\n\t b\n\t c", 35 | }, { 36 | raw: " a\nb", 37 | error: "Space used in indent early in string:\n a\nb", 38 | }, { 39 | raw: "\ta\nb", 40 | error: "Line not indented consistently:\nb", 41 | }} 42 | 43 | func (s *S) TestReindent(c *C) { 44 | for _, test := range reindentTests { 45 | s.testReindent(c, test) 46 | } 47 | } 48 | 49 | func (*S) testReindent(c *C, test reindentTest) { 50 | defer func() { 51 | if err := recover(); err != nil { 52 | errMsg, ok := err.(string) 53 | if !ok { 54 | panic(err) 55 | } 56 | c.Assert(errMsg, Equals, test.error) 57 | } 58 | }() 59 | 60 | c.Logf("Test: %#v", test) 61 | 62 | if !strings.HasSuffix(test.result, "\n") { 63 | test.result += "\n" 64 | } 65 | 66 | reindented := testutil.Reindent(test.raw) 67 | if test.error != "" { 68 | c.Errorf("Expected panic with message '%#v'", test.error) 69 | return 70 | } 71 | c.Assert(string(reindented), Equals, test.result) 72 | } 73 | -------------------------------------------------------------------------------- /internal/deb/log.go: -------------------------------------------------------------------------------- 1 | package deb 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // Avoid importing the log type information unnecessarily. There's a small cost 9 | // associated with using an interface rather than the type. Depending on how 10 | // often the logger is plugged in, it would be worth using the type instead. 11 | type log_Logger interface { 12 | Output(calldepth int, s string) error 13 | } 14 | 15 | var globalLoggerLock sync.Mutex 16 | var globalLogger log_Logger 17 | var globalDebug bool 18 | 19 | // Specify the *log.Logger object where log messages should be sent to. 20 | func SetLogger(logger log_Logger) { 21 | globalLoggerLock.Lock() 22 | globalLogger = logger 23 | globalLoggerLock.Unlock() 24 | } 25 | 26 | // Enable the delivery of debug messages to the logger. Only meaningful 27 | // if a logger is also set. 28 | func SetDebug(debug bool) { 29 | globalLoggerLock.Lock() 30 | globalDebug = debug 31 | globalLoggerLock.Unlock() 32 | } 33 | 34 | // logf sends to the logger registered via SetLogger the string resulting 35 | // from running format and args through Sprintf. 36 | func logf(format string, args ...interface{}) { 37 | globalLoggerLock.Lock() 38 | defer globalLoggerLock.Unlock() 39 | if globalLogger != nil { 40 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 41 | } 42 | } 43 | 44 | // debugf sends to the logger registered via SetLogger the string resulting 45 | // from running format and args through Sprintf, but only if debugging was 46 | // enabled via SetDebug. 47 | func debugf(format string, args ...interface{}) { 48 | globalLoggerLock.Lock() 49 | defer globalLoggerLock.Unlock() 50 | if globalDebug && globalLogger != nil { 51 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/archive/log.go: -------------------------------------------------------------------------------- 1 | package archive 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // Avoid importing the log type information unnecessarily. There's a small cost 9 | // associated with using an interface rather than the type. Depending on how 10 | // often the logger is plugged in, it would be worth using the type instead. 11 | type log_Logger interface { 12 | Output(calldepth int, s string) error 13 | } 14 | 15 | var globalLoggerLock sync.Mutex 16 | var globalLogger log_Logger 17 | var globalDebug bool 18 | 19 | // Specify the *log.Logger object where log messages should be sent to. 20 | func SetLogger(logger log_Logger) { 21 | globalLoggerLock.Lock() 22 | globalLogger = logger 23 | globalLoggerLock.Unlock() 24 | } 25 | 26 | // Enable the delivery of debug messages to the logger. Only meaningful 27 | // if a logger is also set. 28 | func SetDebug(debug bool) { 29 | globalLoggerLock.Lock() 30 | globalDebug = debug 31 | globalLoggerLock.Unlock() 32 | } 33 | 34 | // logf sends to the logger registered via SetLogger the string resulting 35 | // from running format and args through Sprintf. 36 | func logf(format string, args ...interface{}) { 37 | globalLoggerLock.Lock() 38 | defer globalLoggerLock.Unlock() 39 | if globalLogger != nil { 40 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 41 | } 42 | } 43 | 44 | // debugf sends to the logger registered via SetLogger the string resulting 45 | // from running format and args through Sprintf, but only if debugging was 46 | // enabled via SetDebug. 47 | func debugf(format string, args ...interface{}) { 48 | globalLoggerLock.Lock() 49 | defer globalLoggerLock.Unlock() 50 | if globalDebug && globalLogger != nil { 51 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/fsutil/log.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // Avoid importing the log type information unnecessarily. There's a small cost 9 | // associated with using an interface rather than the type. Depending on how 10 | // often the logger is plugged in, it would be worth using the type instead. 11 | type log_Logger interface { 12 | Output(calldepth int, s string) error 13 | } 14 | 15 | var globalLoggerLock sync.Mutex 16 | var globalLogger log_Logger 17 | var globalDebug bool 18 | 19 | // Specify the *log.Logger object where log messages should be sent to. 20 | func SetLogger(logger log_Logger) { 21 | globalLoggerLock.Lock() 22 | globalLogger = logger 23 | globalLoggerLock.Unlock() 24 | } 25 | 26 | // Enable the delivery of debug messages to the logger. Only meaningful 27 | // if a logger is also set. 28 | func SetDebug(debug bool) { 29 | globalLoggerLock.Lock() 30 | globalDebug = debug 31 | globalLoggerLock.Unlock() 32 | } 33 | 34 | // logf sends to the logger registered via SetLogger the string resulting 35 | // from running format and args through Sprintf. 36 | func logf(format string, args ...interface{}) { 37 | globalLoggerLock.Lock() 38 | defer globalLoggerLock.Unlock() 39 | if globalLogger != nil { 40 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 41 | } 42 | } 43 | 44 | // debugf sends to the logger registered via SetLogger the string resulting 45 | // from running format and args through Sprintf, but only if debugging was 46 | // enabled via SetDebug. 47 | func debugf(format string, args ...interface{}) { 48 | globalLoggerLock.Lock() 49 | defer globalLoggerLock.Unlock() 50 | if globalDebug && globalLogger != nil { 51 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/scripts/log.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // Avoid importing the log type information unnecessarily. There's a small cost 9 | // associated with using an interface rather than the type. Depending on how 10 | // often the logger is plugged in, it would be worth using the type instead. 11 | type log_Logger interface { 12 | Output(calldepth int, s string) error 13 | } 14 | 15 | var globalLoggerLock sync.Mutex 16 | var globalLogger log_Logger 17 | var globalDebug bool 18 | 19 | // Specify the *log.Logger object where log messages should be sent to. 20 | func SetLogger(logger log_Logger) { 21 | globalLoggerLock.Lock() 22 | globalLogger = logger 23 | globalLoggerLock.Unlock() 24 | } 25 | 26 | // Enable the delivery of debug messages to the logger. Only meaningful 27 | // if a logger is also set. 28 | func SetDebug(debug bool) { 29 | globalLoggerLock.Lock() 30 | globalDebug = debug 31 | globalLoggerLock.Unlock() 32 | } 33 | 34 | // logf sends to the logger registered via SetLogger the string resulting 35 | // from running format and args through Sprintf. 36 | func logf(format string, args ...interface{}) { 37 | globalLoggerLock.Lock() 38 | defer globalLoggerLock.Unlock() 39 | if globalLogger != nil { 40 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 41 | } 42 | } 43 | 44 | // debugf sends to the logger registered via SetLogger the string resulting 45 | // from running format and args through Sprintf, but only if debugging was 46 | // enabled via SetDebug. 47 | func debugf(format string, args ...interface{}) { 48 | globalLoggerLock.Lock() 49 | defer globalLoggerLock.Unlock() 50 | if globalDebug && globalLogger != nil { 51 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/setup/log.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // Avoid importing the log type information unnecessarily. There's a small cost 9 | // associated with using an interface rather than the type. Depending on how 10 | // often the logger is plugged in, it would be worth using the type instead. 11 | type log_Logger interface { 12 | Output(calldepth int, s string) error 13 | } 14 | 15 | var globalLoggerLock sync.Mutex 16 | var globalLogger log_Logger 17 | var globalDebug bool 18 | 19 | // Specify the *log.Logger object where log messages should be sent to. 20 | func SetLogger(logger log_Logger) { 21 | globalLoggerLock.Lock() 22 | globalLogger = logger 23 | globalLoggerLock.Unlock() 24 | } 25 | 26 | // Enable the delivery of debug messages to the logger. Only meaningful 27 | // if a logger is also set. 28 | func SetDebug(debug bool) { 29 | globalLoggerLock.Lock() 30 | globalDebug = debug 31 | globalLoggerLock.Unlock() 32 | } 33 | 34 | // logf sends to the logger registered via SetLogger the string resulting 35 | // from running format and args through Sprintf. 36 | func logf(format string, args ...interface{}) { 37 | globalLoggerLock.Lock() 38 | defer globalLoggerLock.Unlock() 39 | if globalLogger != nil { 40 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 41 | } 42 | } 43 | 44 | // debugf sends to the logger registered via SetLogger the string resulting 45 | // from running format and args through Sprintf, but only if debugging was 46 | // enabled via SetDebug. 47 | func debugf(format string, args ...interface{}) { 48 | globalLoggerLock.Lock() 49 | defer globalLoggerLock.Unlock() 50 | if globalDebug && globalLogger != nil { 51 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/slicer/log.go: -------------------------------------------------------------------------------- 1 | package slicer 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // Avoid importing the log type information unnecessarily. There's a small cost 9 | // associated with using an interface rather than the type. Depending on how 10 | // often the logger is plugged in, it would be worth using the type instead. 11 | type log_Logger interface { 12 | Output(calldepth int, s string) error 13 | } 14 | 15 | var globalLoggerLock sync.Mutex 16 | var globalLogger log_Logger 17 | var globalDebug bool 18 | 19 | // Specify the *log.Logger object where log messages should be sent to. 20 | func SetLogger(logger log_Logger) { 21 | globalLoggerLock.Lock() 22 | globalLogger = logger 23 | globalLoggerLock.Unlock() 24 | } 25 | 26 | // Enable the delivery of debug messages to the logger. Only meaningful 27 | // if a logger is also set. 28 | func SetDebug(debug bool) { 29 | globalLoggerLock.Lock() 30 | globalDebug = debug 31 | globalLoggerLock.Unlock() 32 | } 33 | 34 | // logf sends to the logger registered via SetLogger the string resulting 35 | // from running format and args through Sprintf. 36 | func logf(format string, args ...interface{}) { 37 | globalLoggerLock.Lock() 38 | defer globalLoggerLock.Unlock() 39 | if globalLogger != nil { 40 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 41 | } 42 | } 43 | 44 | // debugf sends to the logger registered via SetLogger the string resulting 45 | // from running format and args through Sprintf, but only if debugging was 46 | // enabled via SetDebug. 47 | func debugf(format string, args ...interface{}) { 48 | globalLoggerLock.Lock() 49 | defer globalLoggerLock.Unlock() 50 | if globalDebug && globalLogger != nil { 51 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/jsonwall/log.go: -------------------------------------------------------------------------------- 1 | package jsonwall 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // Avoid importing the log type information unnecessarily. There's a small cost 9 | // associated with using an interface rather than the type. Depending on how 10 | // often the logger is plugged in, it would be worth using the type instead. 11 | type log_Logger interface { 12 | Output(calldepth int, s string) error 13 | } 14 | 15 | var globalLoggerLock sync.Mutex 16 | var globalLogger log_Logger 17 | var globalDebug bool 18 | 19 | // Specify the *log.Logger object where log messages should be sent to. 20 | func SetLogger(logger log_Logger) { 21 | globalLoggerLock.Lock() 22 | globalLogger = logger 23 | globalLoggerLock.Unlock() 24 | } 25 | 26 | // Enable the delivery of debug messages to the logger. Only meaningful 27 | // if a logger is also set. 28 | func SetDebug(debug bool) { 29 | globalLoggerLock.Lock() 30 | globalDebug = debug 31 | globalLoggerLock.Unlock() 32 | } 33 | 34 | // logf sends to the logger registered via SetLogger the string resulting 35 | // from running format and args through Sprintf. 36 | func logf(format string, args ...interface{}) { 37 | globalLoggerLock.Lock() 38 | defer globalLoggerLock.Unlock() 39 | if globalLogger != nil { 40 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 41 | } 42 | } 43 | 44 | // debugf sends to the logger registered via SetLogger the string resulting 45 | // from running format and args through Sprintf, but only if debugging was 46 | // enabled via SetDebug. 47 | func debugf(format string, args ...interface{}) { 48 | globalLoggerLock.Lock() 49 | defer globalLoggerLock.Unlock() 50 | if globalDebug && globalLogger != nil { 51 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/deb/helpers_test.go: -------------------------------------------------------------------------------- 1 | package deb_test 2 | 3 | import ( 4 | "github.com/canonical/chisel/internal/deb" 5 | . "gopkg.in/check.v1" 6 | ) 7 | 8 | func inferArchFromPlatform(platformArch string) string { 9 | restore := deb.FakePlatformGoArch(platformArch) 10 | defer restore() 11 | goArch, _ := deb.InferArch() 12 | return goArch 13 | } 14 | 15 | func (s *S) TestInferArch(c *C) { 16 | c.Assert(inferArchFromPlatform("386"), Equals, "i386") 17 | c.Assert(inferArchFromPlatform("amd64"), Equals, "amd64") 18 | c.Assert(inferArchFromPlatform("arm"), Equals, "armhf") 19 | c.Assert(inferArchFromPlatform("arm64"), Equals, "arm64") 20 | c.Assert(inferArchFromPlatform("ppc64le"), Equals, "ppc64el") 21 | c.Assert(inferArchFromPlatform("riscv64"), Equals, "riscv64") 22 | c.Assert(inferArchFromPlatform("s390x"), Equals, "s390x") 23 | c.Assert(inferArchFromPlatform("i386"), Equals, "") 24 | c.Assert(inferArchFromPlatform("armhf"), Equals, "") 25 | c.Assert(inferArchFromPlatform("ppc64el"), Equals, "") 26 | c.Assert(inferArchFromPlatform("foo"), Equals, "") 27 | c.Assert(inferArchFromPlatform(""), Equals, "") 28 | } 29 | 30 | func (s *S) TestValidateArch(c *C) { 31 | c.Assert(deb.ValidateArch("i386"), IsNil) 32 | c.Assert(deb.ValidateArch("amd64"), IsNil) 33 | c.Assert(deb.ValidateArch("armhf"), IsNil) 34 | c.Assert(deb.ValidateArch("arm64"), IsNil) 35 | c.Assert(deb.ValidateArch("ppc64el"), IsNil) 36 | c.Assert(deb.ValidateArch("riscv64"), IsNil) 37 | c.Assert(deb.ValidateArch("s390x"), IsNil) 38 | c.Assert(deb.ValidateArch("386"), Not(IsNil)) 39 | c.Assert(deb.ValidateArch("arm"), Not(IsNil)) 40 | c.Assert(deb.ValidateArch("ppc64le"), Not(IsNil)) 41 | c.Assert(deb.ValidateArch("foo"), Not(IsNil)) 42 | c.Assert(deb.ValidateArch("i3866"), Not(IsNil)) 43 | c.Assert(deb.ValidateArch(""), Not(IsNil)) 44 | } 45 | -------------------------------------------------------------------------------- /internal/strdist/log.go: -------------------------------------------------------------------------------- 1 | package strdist 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // Avoid importing the log type information unnecessarily. There's a small cost 9 | // associated with using an interface rather than the type. Depending on how 10 | // often the logger is plugged in, it would be worth using the type instead. 11 | type log_Logger interface { 12 | Output(calldepth int, s string) error 13 | } 14 | 15 | var globalLoggerLock sync.Mutex 16 | var globalLogger log_Logger 17 | var globalDebug bool 18 | 19 | // Specify the *log.Logger object where log messages should be sent to. 20 | func SetLogger(logger log_Logger) { 21 | globalLoggerLock.Lock() 22 | globalLogger = logger 23 | globalLoggerLock.Unlock() 24 | } 25 | 26 | // Enable the delivery of debug messages to the logger. Only meaningful 27 | // if a logger is also set. 28 | func SetDebug(debug bool) { 29 | globalLoggerLock.Lock() 30 | globalDebug = debug 31 | globalLoggerLock.Unlock() 32 | } 33 | 34 | func IsDebugOn() bool { 35 | globalLoggerLock.Lock() 36 | on := globalDebug 37 | globalLoggerLock.Unlock() 38 | return on 39 | } 40 | 41 | // logf sends to the logger registered via SetLogger the string resulting 42 | // from running format and args through Sprintf. 43 | func logf(format string, args ...interface{}) { 44 | globalLoggerLock.Lock() 45 | defer globalLoggerLock.Unlock() 46 | if globalLogger != nil { 47 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 48 | } 49 | } 50 | 51 | // debugf sends to the logger registered via SetLogger the string resulting 52 | // from running format and args through Sprintf, but only if debugging was 53 | // enabled via SetDebug. 54 | func debugf(format string, args ...interface{}) { 55 | globalLoggerLock.Lock() 56 | defer globalLoggerLock.Unlock() 57 | if globalDebug && globalLogger != nil { 58 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/testutil/filepresencechecker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "gopkg.in/check.v1" 22 | ) 23 | 24 | type filePresenceChecker struct { 25 | *check.CheckerInfo 26 | present bool 27 | } 28 | 29 | // FilePresent verifies that the given file exists. 30 | var FilePresent check.Checker = &filePresenceChecker{ 31 | CheckerInfo: &check.CheckerInfo{Name: "FilePresent", Params: []string{"filename"}}, 32 | present: true, 33 | } 34 | 35 | // FileAbsent verifies that the given file does not exist. 36 | var FileAbsent check.Checker = &filePresenceChecker{ 37 | CheckerInfo: &check.CheckerInfo{Name: "FileAbsent", Params: []string{"filename"}}, 38 | present: false, 39 | } 40 | 41 | func (c *filePresenceChecker) Check(params []interface{}, names []string) (result bool, error string) { 42 | filename, ok := params[0].(string) 43 | if !ok { 44 | return false, "filename must be a string" 45 | } 46 | _, err := os.Stat(filename) 47 | if os.IsNotExist(err) && c.present { 48 | return false, fmt.Sprintf("file %q is absent but should exist", filename) 49 | } 50 | if err == nil && !c.present { 51 | return false, fmt.Sprintf("file %q is present but should not exist", filename) 52 | } 53 | return true, "" 54 | } 55 | -------------------------------------------------------------------------------- /examples/c-example.dockerfile: -------------------------------------------------------------------------------- 1 | # This Dockerfile is building an image for a simple "Hello, world" C program that uses a GCC compiler and a chiselled Ubuntu base image 2 | 3 | ARG UBUNTU_RELEASE=22.04 4 | 5 | FROM public.ecr.aws/lts/ubuntu:${UBUNTU_RELEASE} AS builder 6 | 7 | WORKDIR /app 8 | 9 | RUN apt-get update && apt-get install -y gcc 10 | # Updates the package lists and installs the GCC compiler on the current build stage 11 | 12 | RUN echo 'main(){printf("hello, world\\n");}' > hello.c 13 | # Creates a "hello.c" file for a simple "Hello, world" C program in the current working directory 14 | 15 | RUN gcc -w hello.c -o ./hello-world 16 | # Compiles the "hello.c" file using the GCC compiler and creates a "hello-world" binary in the current working directory 17 | 18 | FROM chiselled-base:${UBUNTU_RELEASE} 19 | # Starts a new build stage from the previously built chiselled base image for the specified Ubuntu release (22.04 by default) 20 | 21 | COPY --from=builder /app/hello-world / 22 | # Copies the "hello-world" binary from the "builder" build stage to the root directory ("/") of the new image 23 | 24 | CMD [ "/hello-world" ] 25 | # Sets the default command to run when the container is started from this image to "/hello-world" 26 | 27 | # USAGE: 28 | # 29 | # 1. Build the "chisel:22.04" image using the "../Dockerfile" file 30 | # ``` docker build .. --build-arg UBUNTU_RELEASE=22.04 -t chisel:22.04 ``` 31 | # 2. Build the chiselled Ubuntu base image using the provided "chiselled-base.dockerfile" file 32 | # ``` docker build . --build-arg UBUNTU_RELEASE=22.04 -f chiselled-base.dockerfile -t chiselled-base:22.04 ``` 33 | # 3. Build the "hello-world" image using this Dockerfile 34 | # ``` docker build . --build-arg UBUNTU_RELEASE=22.04 -f c-example.dockerfile -t hello-world ``` 35 | # 4. Run the "hello-world" container from the built image 36 | # ``` docker run --rm -it hello-world ``` 37 | # 5. Make sure the output shows the "hello, world" text! -------------------------------------------------------------------------------- /internal/fsutil/create_test.go: -------------------------------------------------------------------------------- 1 | package fsutil_test 2 | 3 | import ( 4 | "bytes" 5 | "io/fs" 6 | "path/filepath" 7 | "syscall" 8 | 9 | . "gopkg.in/check.v1" 10 | 11 | "github.com/canonical/chisel/internal/fsutil" 12 | "github.com/canonical/chisel/internal/testutil" 13 | ) 14 | 15 | type createTest struct { 16 | options fsutil.CreateOptions 17 | result map[string]string 18 | error string 19 | } 20 | 21 | var createTests = []createTest{{ 22 | options: fsutil.CreateOptions{ 23 | Path: "foo/bar", 24 | Data: bytes.NewBufferString("data1"), 25 | Mode: 0444, 26 | }, 27 | result: map[string]string{ 28 | "/foo/": "dir 0755", 29 | "/foo/bar": "file 0444 5b41362b", 30 | }, 31 | }, { 32 | options: fsutil.CreateOptions{ 33 | Path: "foo/bar", 34 | Link: "../baz", 35 | Mode: fs.ModeSymlink, 36 | }, 37 | result: map[string]string{ 38 | "/foo/": "dir 0755", 39 | "/foo/bar": "symlink ../baz", 40 | }, 41 | }, { 42 | options: fsutil.CreateOptions{ 43 | Path: "foo/bar", 44 | Mode: fs.ModeDir | 0444, 45 | }, 46 | result: map[string]string{ 47 | "/foo/": "dir 0755", 48 | "/foo/bar/": "dir 0444", 49 | }, 50 | }, { 51 | options: fsutil.CreateOptions{ 52 | Path: "tmp", 53 | Mode: fs.ModeDir | fs.ModeSticky | 0775, 54 | }, 55 | result: map[string]string{ 56 | "/tmp/": "dir 01775", 57 | }, 58 | }} 59 | 60 | func (s *S) TestCreate(c *C) { 61 | oldUmask := syscall.Umask(0) 62 | defer func() { 63 | syscall.Umask(oldUmask) 64 | }() 65 | 66 | for _, test := range createTests { 67 | c.Logf("Options: %v", test.options) 68 | dir := c.MkDir() 69 | options := test.options 70 | options.Path = filepath.Join(dir, options.Path) 71 | err := fsutil.Create(&options) 72 | if test.error != "" { 73 | c.Assert(err, ErrorMatches, test.error) 74 | continue 75 | } else { 76 | c.Assert(err, IsNil) 77 | } 78 | result := testutil.TreeDump(dir) 79 | c.Assert(result, DeepEquals, test.result) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/testutil/testutil_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil_test 16 | 17 | import ( 18 | "reflect" 19 | "testing" 20 | 21 | "gopkg.in/check.v1" 22 | ) 23 | 24 | func Test(t *testing.T) { 25 | check.TestingT(t) 26 | } 27 | 28 | type S struct{} 29 | 30 | var _ = check.Suite(&S{}) 31 | 32 | func testInfo(c *check.C, checker check.Checker, name string, paramNames []string) { 33 | info := checker.Info() 34 | if info.Name != name { 35 | c.Fatalf("Got name %s, expected %s", info.Name, name) 36 | } 37 | if !reflect.DeepEqual(info.Params, paramNames) { 38 | c.Fatalf("Got param names %#v, expected %#v", info.Params, paramNames) 39 | } 40 | } 41 | 42 | func testCheck(c *check.C, checker check.Checker, result bool, error string, params ...interface{}) ([]interface{}, []string) { 43 | info := checker.Info() 44 | if len(params) != len(info.Params) { 45 | c.Fatalf("unexpected param count in test; expected %d got %d", len(info.Params), len(params)) 46 | } 47 | names := append([]string{}, info.Params...) 48 | resultActual, errorActual := checker.Check(params, names) 49 | if resultActual != result || errorActual != error { 50 | c.Fatalf("%s.Check(%#v) returned (%#v, %#v) rather than (%#v, %#v)", 51 | info.Name, params, resultActual, errorActual, result, error) 52 | } 53 | return params, names 54 | } 55 | -------------------------------------------------------------------------------- /examples/swift-example.dockerfile: -------------------------------------------------------------------------------- 1 | # This Dockerfile is building an image for a simple "Hello, world" Swift program that uses the Swift compiler and a chiselled Ubuntu base image 2 | 3 | ARG UBUNTU_RELEASE=22.04 4 | ARG SWIFT_VERSION=5.8 5 | ARG UBUNTU_NAME_RELEASE=jammy 6 | 7 | FROM swift:${SWIFT_VERSION}-${UBUNTU_NAME_RELEASE} AS builder 8 | 9 | WORKDIR /app 10 | 11 | RUN apt-get update && apt-get upgrade -y 12 | # Updates the package lists 13 | 14 | RUN echo 'print("hello, world")' > hello.swift 15 | # Creates a "hello.swift" file for a simple "Hello, world" Swift program in the current working directory 16 | 17 | RUN swiftc hello.swift -o ./hello-world -static-stdlib 18 | # Compiles the "hello.swift" file using the Swift compiler and creates a "hello-world" binary in the current working directory 19 | 20 | FROM chiselled-base:${UBUNTU_RELEASE} 21 | # Starts a new build stage from the previously built chiselled base image for the specified Ubuntu release (22.04 by default) 22 | 23 | COPY --from=builder /app/hello-world / 24 | # Copies the "hello-world" binary from the "builder" build stage to the root directory ("/") of the new image 25 | 26 | CMD [ "/hello-world" ] 27 | # Sets the default command to run when the container is started from this image to "/hello-world" 28 | 29 | # USAGE: 30 | # 31 | # 1. Build the "chisel:22.04" image using the "../Dockerfile" file 32 | # ``` docker build .. --build-arg UBUNTU_RELEASE=22.04 -t chisel:22.04 ``` 33 | # 2. Build the chiselled Ubuntu base image using the provided "chiselled-base.dockerfile" file 34 | # ``` docker build . --build-arg UBUNTU_RELEASE=22.04 -f chiselled-base.dockerfile -t chiselled-base:22.04 ``` 35 | # 3. Build the "hello-world" image using this Dockerfile 36 | # ``` docker build . --build-arg UBUNTU_RELEASE=22.04 -f swift-example.dockerfile -t hello-world ``` 37 | # 4. Run the "hello-world" container from the built image 38 | # ``` docker run --rm -it hello-world ``` 39 | # 5. Make sure the output shows the "hello, world" text! -------------------------------------------------------------------------------- /internal/testutil/filepresencechecker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil_test 16 | 17 | import ( 18 | "fmt" 19 | "io/ioutil" 20 | "path/filepath" 21 | 22 | "gopkg.in/check.v1" 23 | 24 | . "github.com/canonical/chisel/internal/testutil" 25 | ) 26 | 27 | type filePresenceCheckerSuite struct{} 28 | 29 | var _ = check.Suite(&filePresenceCheckerSuite{}) 30 | 31 | func (*filePresenceCheckerSuite) TestFilePresent(c *check.C) { 32 | d := c.MkDir() 33 | filename := filepath.Join(d, "foo") 34 | testInfo(c, FilePresent, "FilePresent", []string{"filename"}) 35 | testCheck(c, FilePresent, false, `filename must be a string`, 42) 36 | testCheck(c, FilePresent, false, fmt.Sprintf(`file %q is absent but should exist`, filename), filename) 37 | c.Assert(ioutil.WriteFile(filename, nil, 0644), check.IsNil) 38 | testCheck(c, FilePresent, true, "", filename) 39 | } 40 | 41 | func (*filePresenceCheckerSuite) TestFileAbsent(c *check.C) { 42 | d := c.MkDir() 43 | filename := filepath.Join(d, "foo") 44 | testInfo(c, FileAbsent, "FileAbsent", []string{"filename"}) 45 | testCheck(c, FileAbsent, false, `filename must be a string`, 42) 46 | testCheck(c, FileAbsent, true, "", filename) 47 | c.Assert(ioutil.WriteFile(filename, nil, 0644), check.IsNil) 48 | testCheck(c, FileAbsent, false, fmt.Sprintf(`file %q is present but should not exist`, filename), filename) 49 | } 50 | -------------------------------------------------------------------------------- /internal/testutil/intcheckers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil_test 16 | 17 | import ( 18 | "gopkg.in/check.v1" 19 | 20 | . "github.com/canonical/chisel/internal/testutil" 21 | ) 22 | 23 | type intCheckersSuite struct{} 24 | 25 | var _ = check.Suite(&intCheckersSuite{}) 26 | 27 | func (*intCheckersSuite) TestIntChecker(c *check.C) { 28 | c.Assert(1, IntLessThan, 2) 29 | c.Assert(1, IntLessEqual, 1) 30 | c.Assert(1, IntEqual, 1) 31 | c.Assert(2, IntNotEqual, 1) 32 | c.Assert(2, IntGreaterThan, 1) 33 | c.Assert(2, IntGreaterEqual, 2) 34 | 35 | // Wrong argument types. 36 | testCheck(c, IntLessThan, false, "left-hand-side argument must be an int", false, 1) 37 | testCheck(c, IntLessThan, false, "right-hand-side argument must be an int", 1, false) 38 | 39 | // Relationship error. 40 | testCheck(c, IntLessThan, false, "relation 2 < 1 is not true", 2, 1) 41 | testCheck(c, IntLessEqual, false, "relation 2 <= 1 is not true", 2, 1) 42 | testCheck(c, IntEqual, false, "relation 2 == 1 is not true", 2, 1) 43 | testCheck(c, IntNotEqual, false, "relation 2 != 2 is not true", 2, 2) 44 | testCheck(c, IntGreaterThan, false, "relation 1 > 2 is not true", 1, 2) 45 | testCheck(c, IntGreaterEqual, false, "relation 1 >= 2 is not true", 1, 2) 46 | 47 | // Unexpected relation. 48 | unexpected := UnexpectedIntChecker("===") 49 | testCheck(c, unexpected, false, `unexpected relation "==="`, 1, 2) 50 | } 51 | -------------------------------------------------------------------------------- /internal/control/helpers_test.go: -------------------------------------------------------------------------------- 1 | package control_test 2 | 3 | import ( 4 | "github.com/canonical/chisel/internal/control" 5 | 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | type parsePathInfoTest struct { 10 | table string 11 | path string 12 | size int 13 | digest string 14 | } 15 | 16 | var parsePathInfoTests = []parsePathInfoTest{{ 17 | table: ` 18 | before 1 /one/path 19 | 0123456789abcdef0123456789abcdef 2 /the/path 20 | after 2 /two/path 21 | `, 22 | path: "/the/path", 23 | size: 2, 24 | digest: "0123456789abcdef0123456789abcdef", 25 | }, { 26 | table: ` 27 | 0123456789abcdef0123456789abcdef 1 /the/path 28 | after 2 /two/path 29 | `, 30 | path: "/the/path", 31 | size: 1, 32 | digest: "0123456789abcdef0123456789abcdef", 33 | }, { 34 | table: ` 35 | before 1 /two/path 36 | 0123456789abcdef0123456789abcdef 2 /the/path 37 | `, 38 | path: "/the/path", 39 | size: 2, 40 | digest: "0123456789abcdef0123456789abcdef", 41 | }, { 42 | table: `0123456789abcdef0123456789abcdef 0 /the/path`, 43 | path: "/the/path", 44 | size: 0, 45 | digest: "0123456789abcdef0123456789abcdef", 46 | }, { 47 | table: `0123456789abcdef0123456789abcdef 555 /the/path`, 48 | path: "/the/path", 49 | size: 555, 50 | digest: "0123456789abcdef0123456789abcdef", 51 | }, { 52 | table: `deadbeef 0 /the/path`, 53 | path: "/the/path", 54 | digest: "", 55 | }, { 56 | table: `bad-data 0 /the/path`, 57 | path: "/the/path", 58 | digest: "", 59 | }} 60 | 61 | func (s *S) TestParsePathInfo(c *C) { 62 | for _, test := range parsePathInfoTests { 63 | c.Logf("Path is %q, expecting digest %q and size %d.", test.path, test.digest, test.size) 64 | digest, size, ok := control.ParsePathInfo(test.table, test.path) 65 | c.Logf("Got digest %q, size %d, ok %v.", digest, size, ok) 66 | if test.digest == "" { 67 | c.Assert(digest, Equals, "") 68 | c.Assert(size, Equals, -1) 69 | c.Assert(ok, Equals, false) 70 | } else { 71 | c.Assert(digest, Equals, test.digest) 72 | c.Assert(size, Equals, test.size) 73 | c.Assert(ok, Equals, true) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /internal/fsutil/create.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | type CreateOptions struct { 12 | Path string 13 | Mode fs.FileMode 14 | Data io.Reader 15 | Link string 16 | } 17 | 18 | func Create(o *CreateOptions) error { 19 | var err error 20 | switch o.Mode & fs.ModeType { 21 | case 0: 22 | err = createFile(o) 23 | case fs.ModeDir: 24 | err = createDir(o) 25 | case fs.ModeSymlink: 26 | err = createSymlink(o) 27 | default: 28 | err = fmt.Errorf("unsupported file type: %s", o.Path) 29 | } 30 | return err 31 | } 32 | 33 | func createDir(o *CreateOptions) error { 34 | debugf("Creating directory: %s (mode %#o)", o.Path, o.Mode) 35 | err := os.MkdirAll(filepath.Dir(o.Path), 0755) 36 | if err != nil { 37 | return err 38 | } 39 | err = os.Mkdir(o.Path, o.Mode) 40 | if os.IsExist(err) { 41 | err = os.Chmod(o.Path, o.Mode) 42 | } 43 | return err 44 | } 45 | 46 | func createFile(o *CreateOptions) error { 47 | debugf("Writing file: %s (mode %#o)", o.Path, o.Mode) 48 | err := os.MkdirAll(filepath.Dir(o.Path), 0755) 49 | if err != nil && !os.IsExist(err) { 50 | return err 51 | } 52 | file, err := os.OpenFile(o.Path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, o.Mode) 53 | if err != nil { 54 | return err 55 | } 56 | _, copyErr := io.Copy(file, o.Data) 57 | err = file.Close() 58 | if copyErr != nil { 59 | return copyErr 60 | } 61 | return err 62 | } 63 | 64 | func createSymlink(o *CreateOptions) error { 65 | debugf("Creating symlink: %s => %s", o.Path, o.Link) 66 | err := os.MkdirAll(filepath.Dir(o.Path), 0755) 67 | if err != nil && !os.IsExist(err) { 68 | return err 69 | } 70 | fileinfo, err := os.Lstat(o.Path) 71 | if err == nil { 72 | if (fileinfo.Mode() & os.ModeSymlink) != 0 { 73 | link, err := os.Readlink(o.Path) 74 | if err != nil { 75 | return err 76 | } 77 | if link == o.Link { 78 | return nil 79 | } 80 | } 81 | err = os.Remove(o.Path) 82 | if err != nil { 83 | return err 84 | } 85 | } else if !os.IsNotExist(err) { 86 | return err 87 | } 88 | return os.Symlink(o.Link, o.Path) 89 | } 90 | -------------------------------------------------------------------------------- /cmd/chisel/main_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "golang.org/x/crypto/ssh/terminal" 9 | 10 | . "gopkg.in/check.v1" 11 | 12 | "github.com/canonical/chisel/cmd" 13 | "github.com/canonical/chisel/internal/testutil" 14 | 15 | chisel "github.com/canonical/chisel/cmd/chisel" 16 | ) 17 | 18 | // Hook up check.v1 into the "go test" runner 19 | func Test(t *testing.T) { TestingT(t) } 20 | 21 | type BaseChiselSuite struct { 22 | testutil.BaseTest 23 | stdin *bytes.Buffer 24 | stdout *bytes.Buffer 25 | stderr *bytes.Buffer 26 | password string 27 | } 28 | 29 | func (s *BaseChiselSuite) readPassword(fd int) ([]byte, error) { 30 | return []byte(s.password), nil 31 | } 32 | 33 | func (s *BaseChiselSuite) SetUpTest(c *C) { 34 | s.BaseTest.SetUpTest(c) 35 | 36 | s.stdin = bytes.NewBuffer(nil) 37 | s.stdout = bytes.NewBuffer(nil) 38 | s.stderr = bytes.NewBuffer(nil) 39 | s.password = "" 40 | 41 | chisel.Stdin = s.stdin 42 | chisel.Stdout = s.stdout 43 | chisel.Stderr = s.stderr 44 | chisel.ReadPassword = s.readPassword 45 | 46 | s.AddCleanup(chisel.FakeIsStdoutTTY(false)) 47 | s.AddCleanup(chisel.FakeIsStdinTTY(false)) 48 | } 49 | 50 | func (s *BaseChiselSuite) TearDownTest(c *C) { 51 | chisel.Stdin = os.Stdin 52 | chisel.Stdout = os.Stdout 53 | chisel.Stderr = os.Stderr 54 | chisel.ReadPassword = terminal.ReadPassword 55 | 56 | s.BaseTest.TearDownTest(c) 57 | } 58 | 59 | func (s *BaseChiselSuite) Stdout() string { 60 | return s.stdout.String() 61 | } 62 | 63 | func (s *BaseChiselSuite) Stderr() string { 64 | return s.stderr.String() 65 | } 66 | 67 | func (s *BaseChiselSuite) ResetStdStreams() { 68 | s.stdin.Reset() 69 | s.stdout.Reset() 70 | s.stderr.Reset() 71 | } 72 | 73 | func fakeArgs(args ...string) (restore func()) { 74 | old := os.Args 75 | os.Args = args 76 | return func() { os.Args = old } 77 | } 78 | 79 | func fakeVersion(v string) (restore func()) { 80 | old := cmd.Version 81 | cmd.Version = v 82 | return func() { cmd.Version = old } 83 | } 84 | 85 | type ChiselSuite struct { 86 | BaseChiselSuite 87 | } 88 | 89 | var _ = Suite(&ChiselSuite{}) 90 | -------------------------------------------------------------------------------- /internal/setup/tarjan_test.go: -------------------------------------------------------------------------------- 1 | // This file was copied from mgo, MongoDB driver for Go. 2 | // 3 | // Copyright (c) 2010-2013 - Gustavo Niemeyer 4 | // 5 | // All rights reserved. 6 | // 7 | // Redistribution and use in source and binary forms, with or without 8 | // modification, are permitted provided that the following conditions are met: 9 | // 10 | // 1. Redistributions of source code must retain the above copyright notice, this 11 | // list of conditions and the following disclaimer. 12 | // 2. Redistributions in binary form must reproduce the above copyright notice, 13 | // this list of conditions and the following disclaimer in the documentation 14 | // and/or other materials provided with the distribution. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | package setup 28 | 29 | import ( 30 | . "gopkg.in/check.v1" 31 | ) 32 | 33 | type TarjanSuite struct{} 34 | 35 | var _ = Suite(TarjanSuite{}) 36 | 37 | func (TarjanSuite) TestExample(c *C) { 38 | successors := map[string][]string{ 39 | "1": {"2", "3"}, 40 | "2": {"1", "5"}, 41 | "3": {"4"}, 42 | "4": {"3", "5"}, 43 | "5": {"6"}, 44 | "6": {"7"}, 45 | "7": {"8"}, 46 | "8": {"6", "9"}, 47 | "9": {}, 48 | } 49 | 50 | c.Assert(tarjanSort(successors), DeepEquals, [][]string{ 51 | {"9"}, 52 | {"6", "7", "8"}, 53 | {"5"}, 54 | {"3", "4"}, 55 | {"1", "2"}, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/chisel/log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // Avoid importing the log type information unnecessarily. There's a small cost 9 | // associated with using an interface rather than the type. Depending on how 10 | // often the logger is plugged in, it would be worth using the type instead. 11 | type log_Logger interface { 12 | Output(calldepth int, s string) error 13 | } 14 | 15 | var globalLoggerLock sync.Mutex 16 | var globalLogger log_Logger 17 | var globalDebug bool 18 | 19 | // Specify the *log.Logger object where log messages should be sent to. 20 | func SetLogger(logger log_Logger) { 21 | globalLoggerLock.Lock() 22 | globalLogger = logger 23 | globalLoggerLock.Unlock() 24 | } 25 | 26 | // Enable the delivery of debug messages to the logger. Only meaningful 27 | // if a logger is also set. 28 | func SetDebug(debug bool) { 29 | globalLoggerLock.Lock() 30 | globalDebug = debug 31 | globalLoggerLock.Unlock() 32 | } 33 | 34 | // logf sends to the logger registered via SetLogger the string resulting 35 | // from running format and args through Sprintf. 36 | func logf(format string, args ...interface{}) { 37 | globalLoggerLock.Lock() 38 | defer globalLoggerLock.Unlock() 39 | if globalLogger != nil { 40 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 41 | } 42 | } 43 | 44 | // debugf sends to the logger registered via SetLogger the string resulting 45 | // from running format and args through Sprintf, but only if debugging was 46 | // enabled via SetDebug. 47 | func debugf(format string, args ...interface{}) { 48 | globalLoggerLock.Lock() 49 | defer globalLoggerLock.Unlock() 50 | if globalDebug && globalLogger != nil { 51 | globalLogger.Output(2, fmt.Sprintf(format, args...)) 52 | } 53 | } 54 | 55 | // panicf sends to the logger registered via SetLogger the string resulting 56 | // from running format and args through Sprintf, and then panics with the 57 | // same message. 58 | func panicf(format string, args ...interface{}) { 59 | globalLoggerLock.Lock() 60 | defer globalLoggerLock.Unlock() 61 | if globalDebug && globalLogger != nil { 62 | msg := fmt.Sprintf(format, args...) 63 | globalLogger.Output(2, msg) 64 | panic(msg) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cmd/mkversion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # debugging if anything fails is tricky as dh-golang eats up all output 5 | # uncomment the lines below to get a useful trace if you have to touch 6 | # this again (my advice is: DON'T) 7 | #set -x 8 | #logfile=/tmp/mkversions.log 9 | #exec >> $logfile 2>&1 10 | #echo "env: $(set)" 11 | #echo "mkversion.sh run from: $0" 12 | #echo "pwd: $(pwd)" 13 | 14 | # we have two directories we need to care about: 15 | # - our toplevel pkg builddir which is where "mkversion.sh" is located 16 | # and where "snap-confine" expects its cmd/VERSION file 17 | # - the GO_GENERATE_BUILDDIR which may be the toplevel pkg dir. but 18 | # during "dpkg-buildpackage" it will become a different _build/ dir 19 | # that dh-golang creates and that only contains a subset of the 20 | # files of the toplevel buildir. 21 | PKG_BUILDDIR=$(dirname "$0")/.. 22 | GO_GENERATE_BUILDDIR="$(pwd)" 23 | 24 | # run from "go generate" adjust path 25 | if [ "$GOPACKAGE" = "cmd" ]; then 26 | GO_GENERATE_BUILDDIR="$(pwd)/.." 27 | fi 28 | 29 | OUTPUT_ONLY=false 30 | if [ "$1" = "--output-only" ]; then 31 | OUTPUT_ONLY=true 32 | shift 33 | fi 34 | 35 | # If the version is passed in as an argument to mkversion.sh, let's use that. 36 | if [ -n "$1" ]; then 37 | v="$1" 38 | o=shell 39 | fi 40 | 41 | if [ -z "$v" ]; then 42 | # Let's try to derive the version from git.. 43 | if command -v git >/dev/null; then 44 | # not using "--dirty" here until the following bug is fixed: 45 | # https://bugs.launchpad.net/snapcraft/+bug/1662388 46 | v="$(git describe --always | sed -e 's/-/+git/;y/-/./' )" 47 | o=git 48 | fi 49 | fi 50 | 51 | if [ -z "$v" ]; then 52 | # at this point we maybe in _build/src/github etc where we have no 53 | # debian/changelog (dh-golang only exports the sources here) 54 | # switch to the real source dir for the changelog parsing 55 | v="$(cd "$PKG_BUILDDIR"; dpkg-parsechangelog --show-field Version)"; 56 | o=debian/changelog 57 | fi 58 | 59 | if [ -z "$v" ]; then 60 | exit 1 61 | fi 62 | 63 | if [ "$OUTPUT_ONLY" = true ]; then 64 | echo "$v" 65 | exit 0 66 | fi 67 | 68 | echo "*** Setting version to '$v' from $o." >&2 69 | 70 | cat < "$GO_GENERATE_BUILDDIR/version_generated.go" 71 | package cmd 72 | 73 | // generated by mkversion.sh; do not edit 74 | 75 | func init() { 76 | Version = "$v" 77 | } 78 | EOF 79 | 80 | cat < "$PKG_BUILDDIR/cmd/VERSION" 81 | $v 82 | EOF 83 | 84 | #cat < "$PKG_BUILDDIR/data/info" 85 | #VERSION=$v 86 | #EOF 87 | -------------------------------------------------------------------------------- /internal/control/control_test.go: -------------------------------------------------------------------------------- 1 | package control_test 2 | 3 | import ( 4 | "github.com/canonical/chisel/internal/control" 5 | 6 | "bytes" 7 | "io/ioutil" 8 | "testing" 9 | 10 | . "gopkg.in/check.v1" 11 | ) 12 | 13 | var testFile = `` + 14 | `Section: one 15 | Line: line for one 16 | Multi: 17 | multi line 18 | for one 19 | 20 | Line: line for two 21 | Multi: multi line 22 | for two 23 | Section: two 24 | 25 | Multi: 26 | multi line 27 | for three 28 | Line: line for three 29 | Section: three 30 | 31 | Section: four 32 | Multi: 33 | Space at EOL above 34 | Extra space 35 | One tab 36 | ` 37 | 38 | var testFileResults = map[string]map[string]string{ 39 | "one": map[string]string{ 40 | "Section": "one", 41 | "Line": "line for one", 42 | "Multi": "multi line\nfor one", 43 | }, 44 | "two": map[string]string{ 45 | "Section": "two", 46 | "Line": "line for two", 47 | "Multi": "multi line\nfor two", 48 | }, 49 | "three": map[string]string{ 50 | "Section": "three", 51 | "Line": "line for three", 52 | "Multi": "multi line\nfor three", 53 | }, 54 | "four": map[string]string{ 55 | "Multi": "Space at EOL above\n Extra space\nOne tab", 56 | }, 57 | } 58 | 59 | func (s *S) TestParseString(c *C) { 60 | file, err := control.ParseString("Section", testFile) 61 | c.Assert(err, IsNil) 62 | 63 | for skey, svalues := range testFileResults { 64 | section := file.Section(skey) 65 | for key, value := range svalues { 66 | c.Assert(section.Get(key), Equals, value, Commentf("Section %q / Key %q", skey, key)) 67 | } 68 | } 69 | } 70 | 71 | func (s *S) TestParseReader(c *C) { 72 | file, err := control.ParseReader("Section", bytes.NewReader([]byte(testFile))) 73 | c.Assert(err, IsNil) 74 | 75 | for skey, svalues := range testFileResults { 76 | section := file.Section(skey) 77 | for key, value := range svalues { 78 | c.Assert(section.Get(key), Equals, value, Commentf("Section %q / Key %q", skey, key)) 79 | } 80 | } 81 | } 82 | 83 | func BenchmarkParse(b *testing.B) { 84 | data, err := ioutil.ReadFile("Packages") 85 | if err != nil { 86 | b.Fatalf("cannot open Packages file: %v", err) 87 | } 88 | content := string(data) 89 | b.ResetTimer() 90 | for i := 0; i < b.N; i++ { 91 | control.ParseString("Package", content) 92 | } 93 | } 94 | 95 | func BenchmarkSectionGet(b *testing.B) { 96 | data, err := ioutil.ReadFile("Packages") 97 | if err != nil { 98 | b.Fatalf("cannot open Packages file: %v", err) 99 | } 100 | content := string(data) 101 | file, err := control.ParseString("Package", content) 102 | if err != nil { 103 | panic(err) 104 | } 105 | b.ResetTimer() 106 | for i := 0; i < b.N; i++ { 107 | section := file.Section("util-linux") 108 | value := section.Get("Description") 109 | if value != "miscellaneous system utilities" { 110 | b.Fatalf("Unexpected package description: %q", value) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /internal/testutil/intcheckers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil 16 | 17 | import ( 18 | "fmt" 19 | 20 | "gopkg.in/check.v1" 21 | ) 22 | 23 | type intChecker struct { 24 | *check.CheckerInfo 25 | rel string 26 | } 27 | 28 | func (checker *intChecker) Check(params []interface{}, names []string) (result bool, error string) { 29 | a, ok := params[0].(int) 30 | if !ok { 31 | return false, "left-hand-side argument must be an int" 32 | } 33 | b, ok := params[1].(int) 34 | if !ok { 35 | return false, "right-hand-side argument must be an int" 36 | } 37 | switch checker.rel { 38 | case "<": 39 | result = a < b 40 | case "<=": 41 | result = a <= b 42 | case "==": 43 | result = a == b 44 | case "!=": 45 | result = a != b 46 | case ">": 47 | result = a > b 48 | case ">=": 49 | result = a >= b 50 | default: 51 | return false, fmt.Sprintf("unexpected relation %q", checker.rel) 52 | } 53 | if !result { 54 | error = fmt.Sprintf("relation %d %s %d is not true", a, checker.rel, b) 55 | } 56 | return result, error 57 | } 58 | 59 | // IntLessThan checker verifies that one integer is less than other integer. 60 | // 61 | // For example: 62 | // c.Assert(1, IntLessThan, 2) 63 | var IntLessThan = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntLessThan", Params: []string{"a", "b"}}, rel: "<"} 64 | 65 | // IntLessEqual checker verifies that one integer is less than or equal to other integer. 66 | // 67 | // For example: 68 | // c.Assert(1, IntLessEqual, 1) 69 | var IntLessEqual = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntLessEqual", Params: []string{"a", "b"}}, rel: "<="} 70 | 71 | // IntEqual checker verifies that one integer is equal to other integer. 72 | // 73 | // For example: 74 | // c.Assert(1, IntEqual, 1) 75 | var IntEqual = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntEqual", Params: []string{"a", "b"}}, rel: "=="} 76 | 77 | // IntNotEqual checker verifies that one integer is not equal to other integer. 78 | // 79 | // For example: 80 | // c.Assert(1, IntNotEqual, 2) 81 | var IntNotEqual = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntNotEqual", Params: []string{"a", "b"}}, rel: "!="} 82 | 83 | // IntGreaterThan checker verifies that one integer is greater than other integer. 84 | // 85 | // For example: 86 | // c.Assert(2, IntGreaterThan, 1) 87 | var IntGreaterThan = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntGreaterThan", Params: []string{"a", "b"}}, rel: ">"} 88 | 89 | // IntGreaterEqual checker verifies that one integer is greater than or equal to other integer. 90 | // 91 | // For example: 92 | // c.Assert(1, IntGreaterEqual, 2) 93 | var IntGreaterEqual = &intChecker{CheckerInfo: &check.CheckerInfo{Name: "IntGreaterEqual", Params: []string{"a", "b"}}, rel: ">="} 94 | -------------------------------------------------------------------------------- /internal/strdist/strdist.go: -------------------------------------------------------------------------------- 1 | package strdist 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type CostInt int64 9 | 10 | func (cv CostInt) String() string { 11 | if cv == Inhibit { 12 | return "-" 13 | } 14 | return fmt.Sprint(int64(cv)) 15 | } 16 | 17 | const Inhibit = 1<<63 - 1 18 | 19 | type Cost struct { 20 | SwapAB CostInt 21 | DeleteA CostInt 22 | InsertB CostInt 23 | 24 | private struct{} 25 | } 26 | 27 | type CostFunc func(ar, br rune) Cost 28 | 29 | func StandardCost(ar, br rune) Cost { 30 | return Cost{SwapAB: 1, DeleteA: 1, InsertB: 1} 31 | } 32 | 33 | func Distance(a, b string, f CostFunc, cut int64) int64 { 34 | if a == b { 35 | return 0 36 | } 37 | lst := make([]CostInt, len(b)+1) 38 | bl := 0 39 | for bi, br := range b { 40 | bl++ 41 | cost := f(-1, br) 42 | if cost.InsertB == Inhibit || lst[bi] == Inhibit { 43 | lst[bi+1] = Inhibit 44 | } else { 45 | lst[bi+1] = lst[bi] + cost.InsertB 46 | } 47 | } 48 | lst = lst[:bl+1] 49 | // Not required, but caching means preventing the fast path 50 | // below from calling the function and locking every time. 51 | debug := IsDebugOn() 52 | if debug { 53 | debugf(">>> %v", lst) 54 | } 55 | for _, ar := range a { 56 | last := lst[0] 57 | cost := f(ar, -1) 58 | if cost.DeleteA == Inhibit || last == Inhibit { 59 | lst[0] = Inhibit 60 | } else { 61 | lst[0] = last + cost.DeleteA 62 | } 63 | stop := true 64 | i := 0 65 | for _, br := range b { 66 | i++ 67 | cost := f(ar, br) 68 | min := CostInt(Inhibit) 69 | if ar == br { 70 | min = last 71 | } else if cost.SwapAB != Inhibit && last != Inhibit { 72 | min = last + cost.SwapAB 73 | } 74 | if cost.InsertB != Inhibit && lst[i-1] != Inhibit { 75 | if n := lst[i-1] + cost.InsertB; n < min { 76 | min = n 77 | } 78 | } 79 | if cost.DeleteA != Inhibit && lst[i] != Inhibit { 80 | if n := lst[i] + cost.DeleteA; n < min { 81 | min = n 82 | } 83 | } 84 | last, lst[i] = lst[i], min 85 | if min < CostInt(cut) { 86 | stop = false 87 | } 88 | } 89 | if debug { 90 | debugf("... %v", lst) 91 | } 92 | _ = stop 93 | if cut != 0 && stop { 94 | break 95 | } 96 | } 97 | return int64(lst[len(lst)-1]) 98 | } 99 | 100 | // GlobPath returns true if a and b match using supported wildcards. 101 | // Note that both a and b main contain wildcards, and it's up to the 102 | // call site to constrain the string content if that's not desirable. 103 | // 104 | // Supported wildcards: 105 | // 106 | // ? - Any one character, except for / 107 | // * - Any zero or more characters, execept for / 108 | // ** - Any zero or more characrers, including / 109 | // 110 | func GlobPath(a, b string) bool { 111 | a = strings.ReplaceAll(a, "**", "⁑") 112 | b = strings.ReplaceAll(b, "**", "⁑") 113 | return Distance(a, b, globCost, 1) == 0 114 | } 115 | 116 | func globCost(ar, br rune) Cost { 117 | if ar == '⁑' || br == '⁑' { 118 | return Cost{SwapAB: 0, DeleteA: 0, InsertB: 0} 119 | } 120 | if ar == '/' || br == '/' { 121 | return Cost{SwapAB: Inhibit, DeleteA: Inhibit, InsertB: Inhibit} 122 | } 123 | if ar == '*' || br == '*' { 124 | return Cost{SwapAB: 0, DeleteA: 0, InsertB: 0} 125 | } 126 | if ar == '?' || br == '?' { 127 | return Cost{SwapAB: 0, DeleteA: 1, InsertB: 1} 128 | } 129 | return Cost{SwapAB: 1, DeleteA: 1, InsertB: 1} 130 | } 131 | 132 | -------------------------------------------------------------------------------- /internal/strdist/strdist_test.go: -------------------------------------------------------------------------------- 1 | package strdist_test 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | 6 | "strings" 7 | "testing" 8 | 9 | "github.com/canonical/chisel/internal/strdist" 10 | ) 11 | 12 | type distanceTest struct { 13 | a, b string 14 | f strdist.CostFunc 15 | r int64 16 | cut int64 17 | } 18 | 19 | func uniqueCost(ar, br rune) strdist.Cost { 20 | return strdist.Cost{SwapAB: 1, DeleteA: 3, InsertB: 5} 21 | } 22 | 23 | var distanceTests = []distanceTest{ 24 | {f: uniqueCost, r: 0, a: "abc", b: "abc"}, 25 | {f: uniqueCost, r: 1, a: "abc", b: "abd"}, 26 | {f: uniqueCost, r: 1, a: "abc", b: "adc"}, 27 | {f: uniqueCost, r: 1, a: "abc", b: "dbc"}, 28 | {f: uniqueCost, r: 2, a: "abc", b: "add"}, 29 | {f: uniqueCost, r: 2, a: "abc", b: "ddc"}, 30 | {f: uniqueCost, r: 2, a: "abc", b: "dbd"}, 31 | {f: uniqueCost, r: 3, a: "abc", b: "ddd"}, 32 | {f: uniqueCost, r: 3, a: "abc", b: "ab"}, 33 | {f: uniqueCost, r: 3, a: "abc", b: "bc"}, 34 | {f: uniqueCost, r: 3, a: "abc", b: "ac"}, 35 | {f: uniqueCost, r: 6, a: "abc", b: "a"}, 36 | {f: uniqueCost, r: 6, a: "abc", b: "b"}, 37 | {f: uniqueCost, r: 6, a: "abc", b: "c"}, 38 | {f: uniqueCost, r: 9, a: "abc", b: ""}, 39 | {f: uniqueCost, r: 5, a: "abc", b: "abcd"}, 40 | {f: uniqueCost, r: 5, a: "abc", b: "dabc"}, 41 | {f: uniqueCost, r: 10, a: "abc", b: "adbdc"}, 42 | {f: uniqueCost, r: 10, a: "abc", b: "dabcd"}, 43 | {f: uniqueCost, r: 40, a: "abc", b: "ddaddbddcdd"}, 44 | {f: strdist.StandardCost, r: 3, a: "abcdefg", b: "axcdfgh"}, 45 | {f: strdist.StandardCost, r: 2, cut: 2, a: "abcdef", b: "abc"}, 46 | {f: strdist.StandardCost, r: 2, cut: 3, a: "abcdef", b: "abcd"}, 47 | {f: strdist.GlobCost, r: 0, a: "abc*", b: "abcdef"}, 48 | {f: strdist.GlobCost, r: 0, a: "ab*ef", b: "abcdef"}, 49 | {f: strdist.GlobCost, r: 0, a: "*def", b: "abcdef"}, 50 | {f: strdist.GlobCost, r: 0, a: "a*/def", b: "abc/def"}, 51 | {f: strdist.GlobCost, r: 1, a: "a*/def", b: "abc/gef"}, 52 | {f: strdist.GlobCost, r: 0, a: "a*/*f", b: "abc/def"}, 53 | {f: strdist.GlobCost, r: 1, a: "a*/*f", b: "abc/defh"}, 54 | {f: strdist.GlobCost, r: 1, a: "a*/*f", b: "abc/defhi"}, 55 | {f: strdist.GlobCost, r: strdist.Inhibit, a: "a*", b: "abc/def"}, 56 | {f: strdist.GlobCost, r: strdist.Inhibit, a: "a*/*f", b: "abc/def/hij"}, 57 | {f: strdist.GlobCost, r: 0, a: "a**f/hij", b: "abc/def/hij"}, 58 | {f: strdist.GlobCost, r: 1, a: "a**f/hij", b: "abc/def/hik"}, 59 | {f: strdist.GlobCost, r: 2, a: "a**fg", b: "abc/def/hik"}, 60 | {f: strdist.GlobCost, r: 0, a: "a**f/hij/klm", b: "abc/d**m"}, 61 | } 62 | 63 | func (s *S) TestDistance(c *C) { 64 | for _, test := range distanceTests { 65 | c.Logf("Test: %v", test) 66 | if strings.Contains(test.a, "*") || strings.Contains(test.b, "*") { 67 | c.Assert(strdist.GlobPath(test.a, test.b), Equals, test.r == 0) 68 | } 69 | f := test.f 70 | if f == nil { 71 | f = strdist.StandardCost 72 | } 73 | test.a = strings.ReplaceAll(test.a, "**", "⁑") 74 | test.b = strings.ReplaceAll(test.b, "**", "⁑") 75 | r := strdist.Distance(test.a, test.b, f, test.cut) 76 | c.Assert(r, Equals, test.r) 77 | } 78 | } 79 | 80 | func BenchmarkDistance(b *testing.B) { 81 | const one = "abdefghijklmnopqrstuvwxyz" 82 | const two = "a.d.f.h.j.l.n.p.r.t.v.x.z" 83 | for i := 0; i < b.N; i++ { 84 | strdist.Distance(one, two, strdist.StandardCost, 0) 85 | } 86 | } 87 | 88 | func BenchmarkDistanceCut(b *testing.B) { 89 | const one = "abdefghijklmnopqrstuvwxyz" 90 | const two = "a.d.f.h.j.l.n.p.r.t.v.x.z" 91 | for i := 0; i < b.N; i++ { 92 | strdist.Distance(one, two, strdist.StandardCost, 1) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /internal/testutil/filecontentchecker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io/ioutil" 21 | "regexp" 22 | "strings" 23 | 24 | "gopkg.in/check.v1" 25 | ) 26 | 27 | type fileContentChecker struct { 28 | *check.CheckerInfo 29 | exact bool 30 | } 31 | 32 | // FileEquals verifies that the given file's content is equal 33 | // to the string (or fmt.Stringer) or []byte provided. 34 | var FileEquals check.Checker = &fileContentChecker{ 35 | CheckerInfo: &check.CheckerInfo{Name: "FileEquals", Params: []string{"filename", "contents"}}, 36 | exact: true, 37 | } 38 | 39 | // FileContains verifies that the given file's content contains 40 | // the string (or fmt.Stringer) or []byte provided. 41 | var FileContains check.Checker = &fileContentChecker{ 42 | CheckerInfo: &check.CheckerInfo{Name: "FileContains", Params: []string{"filename", "contents"}}, 43 | } 44 | 45 | // FileMatches verifies that the given file's content matches 46 | // the string provided. 47 | var FileMatches check.Checker = &fileContentChecker{ 48 | CheckerInfo: &check.CheckerInfo{Name: "FileMatches", Params: []string{"filename", "regex"}}, 49 | } 50 | 51 | func (c *fileContentChecker) Check(params []interface{}, names []string) (result bool, error string) { 52 | filename, ok := params[0].(string) 53 | if !ok { 54 | return false, "Filename must be a string" 55 | } 56 | if names[1] == "regex" { 57 | regexpr, ok := params[1].(string) 58 | if !ok { 59 | return false, "Regex must be a string" 60 | } 61 | rx, err := regexp.Compile(regexpr) 62 | if err != nil { 63 | return false, fmt.Sprintf("Cannot compile regexp %q: %v", regexpr, err) 64 | } 65 | params[1] = rx 66 | } 67 | return fileContentCheck(filename, params[1], c.exact) 68 | } 69 | 70 | func fileContentCheck(filename string, content interface{}, exact bool) (result bool, error string) { 71 | buf, err := ioutil.ReadFile(filename) 72 | if err != nil { 73 | return false, fmt.Sprintf("Cannot read file %q: %v", filename, err) 74 | } 75 | presentableBuf := string(buf) 76 | if exact { 77 | switch content := content.(type) { 78 | case string: 79 | result = presentableBuf == content 80 | case []byte: 81 | result = bytes.Equal(buf, content) 82 | presentableBuf = "" 83 | case fmt.Stringer: 84 | result = presentableBuf == content.String() 85 | default: 86 | error = fmt.Sprintf("Cannot compare file contents with something of type %T", content) 87 | } 88 | } else { 89 | switch content := content.(type) { 90 | case string: 91 | result = strings.Contains(presentableBuf, content) 92 | case []byte: 93 | result = bytes.Contains(buf, content) 94 | presentableBuf = "" 95 | case *regexp.Regexp: 96 | result = content.Match(buf) 97 | case fmt.Stringer: 98 | result = strings.Contains(presentableBuf, content.String()) 99 | default: 100 | error = fmt.Sprintf("Cannot compare file contents with something of type %T", content) 101 | } 102 | } 103 | if !result { 104 | if error == "" { 105 | error = fmt.Sprintf("Cannot match with file contents:\n%v", presentableBuf) 106 | } 107 | return result, error 108 | } 109 | return result, "" 110 | } 111 | -------------------------------------------------------------------------------- /internal/testutil/exec_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil 16 | 17 | import ( 18 | "fmt" 19 | "io/ioutil" 20 | "os/exec" 21 | "path/filepath" 22 | 23 | "gopkg.in/check.v1" 24 | ) 25 | 26 | type fakeCommandSuite struct{} 27 | 28 | var _ = check.Suite(&fakeCommandSuite{}) 29 | 30 | func (s *fakeCommandSuite) TestFakeCommand(c *check.C) { 31 | fake := FakeCommand(c, "cmd", "true") 32 | defer fake.Restore() 33 | err := exec.Command("cmd", "first-run", "--arg1", "arg2", "a space").Run() 34 | c.Assert(err, check.IsNil) 35 | err = exec.Command("cmd", "second-run", "--arg1", "arg2", "a %s").Run() 36 | c.Assert(err, check.IsNil) 37 | c.Assert(fake.Calls(), check.DeepEquals, [][]string{ 38 | {"cmd", "first-run", "--arg1", "arg2", "a space"}, 39 | {"cmd", "second-run", "--arg1", "arg2", "a %s"}, 40 | }) 41 | } 42 | 43 | func (s *fakeCommandSuite) TestFakeCommandAlso(c *check.C) { 44 | fake := FakeCommand(c, "fst", "") 45 | also := fake.Also("snd", "") 46 | defer fake.Restore() 47 | 48 | c.Assert(exec.Command("fst").Run(), check.IsNil) 49 | c.Assert(exec.Command("snd").Run(), check.IsNil) 50 | c.Check(fake.Calls(), check.DeepEquals, [][]string{{"fst"}, {"snd"}}) 51 | c.Check(fake.Calls(), check.DeepEquals, also.Calls()) 52 | } 53 | 54 | func (s *fakeCommandSuite) TestFakeCommandConflictEcho(c *check.C) { 55 | fake := FakeCommand(c, "do-not-swallow-echo-args", "") 56 | defer fake.Restore() 57 | 58 | c.Assert(exec.Command("do-not-swallow-echo-args", "-E", "-n", "-e").Run(), check.IsNil) 59 | c.Assert(fake.Calls(), check.DeepEquals, [][]string{ 60 | {"do-not-swallow-echo-args", "-E", "-n", "-e"}, 61 | }) 62 | } 63 | 64 | func (s *fakeCommandSuite) TestFakeShellchecksWhenAvailable(c *check.C) { 65 | tmpDir := c.MkDir() 66 | fakeShellcheck := FakeCommand(c, "shellcheck", fmt.Sprintf(`cat > %s/input`, tmpDir)) 67 | defer fakeShellcheck.Restore() 68 | 69 | restore := FakeShellcheckPath(fakeShellcheck.Exe()) 70 | defer restore() 71 | 72 | fake := FakeCommand(c, "some-command", "echo some-command") 73 | 74 | c.Assert(exec.Command("some-command").Run(), check.IsNil) 75 | 76 | c.Assert(fake.Calls(), check.DeepEquals, [][]string{ 77 | {"some-command"}, 78 | }) 79 | c.Assert(fakeShellcheck.Calls(), check.DeepEquals, [][]string{ 80 | {"shellcheck", "-s", "bash", "-"}, 81 | }) 82 | 83 | scriptData, err := ioutil.ReadFile(fake.Exe()) 84 | c.Assert(err, check.IsNil) 85 | c.Assert(string(scriptData), Contains, "\necho some-command\n") 86 | 87 | data, err := ioutil.ReadFile(filepath.Join(tmpDir, "input")) 88 | c.Assert(err, check.IsNil) 89 | c.Assert(data, check.DeepEquals, scriptData) 90 | } 91 | 92 | func (s *fakeCommandSuite) TestFakeNoShellchecksWhenNotAvailable(c *check.C) { 93 | fakeShellcheck := FakeCommand(c, "shellcheck", `echo "i am not called"; exit 1`) 94 | defer fakeShellcheck.Restore() 95 | 96 | restore := FakeShellcheckPath("") 97 | defer restore() 98 | 99 | // This would fail with proper shellcheck due to SC2086: Double quote to 100 | // prevent globbing and word splitting. 101 | fake := FakeCommand(c, "some-command", "echo $1") 102 | 103 | c.Assert(exec.Command("some-command").Run(), check.IsNil) 104 | 105 | c.Assert(fake.Calls(), check.DeepEquals, [][]string{ 106 | {"some-command"}, 107 | }) 108 | c.Assert(fakeShellcheck.Calls(), check.HasLen, 0) 109 | } 110 | -------------------------------------------------------------------------------- /internal/control/control.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "strings" 8 | ) 9 | 10 | 11 | // The logic in this file is supposed to be fast so that parsing large data 12 | // files feels instantaneous. It does that by performing a fast scan once to 13 | // index the sections, and then rather than parsing the individual sections it 14 | // scans fields directly on retrieval. That means the whole content is loaded 15 | // in memory at once and without impact to the GC. Should be a good enough 16 | // strategy for the sort of files handled, with long documents of sections 17 | // that are relatively few fields long. 18 | 19 | type File interface { 20 | Section(key string) Section 21 | } 22 | 23 | type Section interface { 24 | Get(key string) string 25 | } 26 | 27 | type ctrlFile struct { 28 | // For the moment content is cached as a string internally as it's faster 29 | // to convert it all at once and remaining operations will not involve 30 | // the GC for the individual string data. 31 | content string 32 | sections map[string]ctrlPos 33 | sectionKey string 34 | } 35 | 36 | func (f *ctrlFile) Section(key string) Section { 37 | if pos, ok := f.sections[key]; ok { 38 | return &ctrlSection{f.content[pos.start:pos.end]} 39 | } 40 | return nil 41 | } 42 | 43 | type ctrlSection struct { 44 | content string 45 | } 46 | 47 | func (s *ctrlSection) Get(key string) string { 48 | content := s.content 49 | pos := 0 50 | if len(content) > len(key)+1 && content[:len(key)] == key && content[len(key)] == ':' { 51 | // Key is on the first line. 52 | pos = len(key) + 1 53 | } else { 54 | prefix := "\n" + key + ":" 55 | pos = strings.Index(content, prefix) 56 | if pos < 0 { 57 | return "" 58 | } 59 | pos += len(prefix) 60 | if pos+1 > len(content) { 61 | return "" 62 | } 63 | } 64 | if content[pos] == ' ' { 65 | pos++ 66 | } 67 | eol := strings.Index(content[pos:], "\n") 68 | if eol < 0 { 69 | eol = len(content) 70 | } else { 71 | eol += pos 72 | } 73 | value := content[pos:eol] 74 | if eol+1 >= len(content) || content[eol+1] != ' ' && content[eol+1] != '\t' { 75 | // Single line value. 76 | return value 77 | } 78 | // Multi line value so we'll need to allocate. 79 | var multi bytes.Buffer 80 | if len(value) > 0 { 81 | multi.WriteString(value) 82 | multi.WriteByte('\n') 83 | } 84 | for { 85 | pos = eol + 2 86 | eol = strings.Index(content[pos:], "\n") 87 | if eol < 0 { 88 | eol = len(content) 89 | } else { 90 | eol += pos 91 | } 92 | multi.WriteString(content[pos:eol]) 93 | if eol+1 >= len(content) || content[eol+1] != ' ' && content[eol+1] != '\t' { 94 | break 95 | } 96 | multi.WriteByte('\n') 97 | } 98 | return multi.String() 99 | } 100 | 101 | type ctrlPos struct { 102 | start, end int 103 | } 104 | 105 | func ParseReader(sectionKey string, content io.Reader) (File, error) { 106 | data, err := ioutil.ReadAll(content) 107 | if err != nil { 108 | return nil, err 109 | } 110 | return ParseString(sectionKey, string(data)) 111 | } 112 | 113 | func ParseString(sectionKey, content string) (File, error) { 114 | skey := sectionKey + ": " 115 | skeylen := len(skey) 116 | sections := make(map[string]ctrlPos) 117 | start := 0 118 | pos := start 119 | for pos < len(content) { 120 | eol := strings.Index(content[pos:], "\n") 121 | if eol < 0 { 122 | eol = len(content) 123 | } else { 124 | eol += pos 125 | } 126 | if pos+skeylen < len(content) && content[pos:pos+skeylen] == skey { 127 | pos += skeylen 128 | end := strings.Index(content[pos:], "\n\n") 129 | if end < 0 { 130 | end = len(content) 131 | } else { 132 | end += pos 133 | } 134 | sections[content[pos:eol]] = ctrlPos{start, end} 135 | pos = end + 2 136 | start = pos 137 | } else { 138 | pos = eol + 1 139 | } 140 | } 141 | return &ctrlFile{ 142 | content: content, 143 | sections: sections, 144 | sectionKey: sectionKey, 145 | }, nil 146 | } 147 | -------------------------------------------------------------------------------- /internal/deb/version_test.go: -------------------------------------------------------------------------------- 1 | // -*- Mode: Go; indent-tabs-mode: t -*- 2 | 3 | /* 4 | * Copyright (C) 2014-2015 Canonical Ltd 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License version 3 as 8 | * published by the Free Software Foundation. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | */ 19 | 20 | package deb_test 21 | 22 | import ( 23 | . "gopkg.in/check.v1" 24 | 25 | "github.com/canonical/chisel/internal/deb" 26 | ) 27 | 28 | type VersionTestSuite struct{} 29 | 30 | var _ = Suite(&VersionTestSuite{}) 31 | 32 | func (s *VersionTestSuite) TestVersionCompare(c *C) { 33 | for _, t := range []struct { 34 | A, B string 35 | res int 36 | }{ 37 | {"20000000000000000000", "020000000000000000000", 0}, 38 | {"1.0", "2.0", -1}, 39 | {"1.3", "1.2.2.2", 1}, 40 | {"1.3", "1.3.1", -1}, 41 | {"1.0", "1.0~", 1}, 42 | {"7.2p2", "7.2", 1}, 43 | {"0.4a6", "0.4", 1}, 44 | {"0pre", "0pre", 0}, 45 | {"0pree", "0pre", 1}, 46 | {"1.18.36:5.4", "1.18.36:5.5", -1}, 47 | {"1.18.36:5.4", "1.18.37:1.1", -1}, 48 | {"2.0.7pre1", "2.0.7r", -1}, 49 | {"0.10.0", "0.8.7", 1}, 50 | // subrev 51 | {"1.0-1", "1.0-2", -1}, 52 | {"1.0-1.1", "1.0-1", 1}, 53 | {"1.0-1.1", "1.0-1.1", 0}, 54 | // do we like strange versions? Yes we like strange versions… 55 | {"0", "0", 0}, 56 | {"0", "00", 0}, 57 | {"", "", 0}, 58 | {"", "0", -1}, 59 | {"0", "", 1}, 60 | {"", "~", 1}, 61 | {"~", "", -1}, 62 | // from the apt suite 63 | {"0-pre", "0-pre", 0}, 64 | {"0-pre", "0-pree", -1}, 65 | {"1.1.6r2-2", "1.1.6r-1", 1}, 66 | {"2.6b2-1", "2.6b-2", 1}, 67 | {"0.4a6-2", "0.4-1", 1}, 68 | {"3.0~rc1-1", "3.0-1", -1}, 69 | {"1.0", "1.0-0", 0}, 70 | {"0.2", "1.0-0", -1}, 71 | {"1.0", "1.0-0+b1", -1}, 72 | {"1.0", "1.0-0~", 1}, 73 | // from the old perl cupt 74 | {"1.2.3", "1.2.3", 0}, // identical 75 | {"4.4.3-2", "4.4.3-2", 0}, // identical 76 | {"1.2.3", "1.2.3-0", 0}, // zero revision 77 | {"009", "9", 0}, // zeroes… 78 | {"009ab5", "9ab5", 0}, // there as well 79 | {"1.2.3", "1.2.3-1", -1}, // added non-zero revision 80 | {"1.2.3", "1.2.4", -1}, // just bigger 81 | {"1.2.4", "1.2.3", 1}, // order doesn't matter 82 | {"1.2.24", "1.2.3", 1}, // bigger, eh? 83 | {"0.10.0", "0.8.7", 1}, // bigger, eh? 84 | {"3.2", "2.3", 1}, // major number rocks 85 | {"1.3.2a", "1.3.2", 1}, // letters rock 86 | {"0.5.0~git", "0.5.0~git2", -1}, // numbers rock 87 | {"2a", "21", -1}, // but not in all places 88 | {"1.2a+~bCd3", "1.2a++", -1}, // tilde doesn't rock 89 | {"1.2a+~bCd3", "1.2a+~", 1}, // but first is longer! 90 | {"5.10.0", "5.005", 1}, // preceding zeroes don't matters 91 | {"3a9.8", "3.10.2", -1}, // letters are before all letter symbols 92 | {"3a9.8", "3~10", 1}, // but after the tilde 93 | {"1.4+OOo3.0.0~", "1.4+OOo3.0.0-4", -1}, // another tilde check 94 | {"2.4.7-1", "2.4.7-z", -1}, // revision comparing 95 | {"1.002-1+b2", "1.00", 1}, // whatever... 96 | {"12-20220319-1ubuntu1", "12-20220319-1ubuntu2", -1}, // libgcc-s1 97 | {"1:13.0.1-2ubuntu2", "1:13.0.1-2ubuntu3", -1}, 98 | } { 99 | res := deb.CompareVersions(t.A, t.B) 100 | c.Assert(res, Equals, t.res, Commentf("%#v %#v: %v but got %v", t.A, t.B, res, t.res)) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /internal/deb/version.go: -------------------------------------------------------------------------------- 1 | // -*- Mode: Go; indent-tabs-mode: t -*- 2 | 3 | /* 4 | * Copyright (C) 2014-2017 Canonical Ltd 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License version 3 as 8 | * published by the Free Software Foundation. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | */ 19 | 20 | package deb 21 | 22 | import ( 23 | "strings" 24 | ) 25 | 26 | func max(a, b int) int { 27 | if a < b { 28 | return b 29 | } 30 | return a 31 | } 32 | 33 | //go:generate go run ./chrorder/main.go -package=deb -output=chrorder.go 34 | 35 | func cmpString(as, bs string) int { 36 | for i := 0; i < max(len(as), len(bs)); i++ { 37 | var a uint8 38 | var b uint8 39 | if i < len(as) { 40 | a = as[i] 41 | } 42 | if i < len(bs) { 43 | b = bs[i] 44 | } 45 | if chOrder[a] < chOrder[b] { 46 | return -1 47 | } 48 | if chOrder[a] > chOrder[b] { 49 | return +1 50 | } 51 | } 52 | return 0 53 | } 54 | 55 | func trimLeadingZeroes(a string) string { 56 | for i := 0; i < len(a); i++ { 57 | if a[i] != '0' { 58 | return a[i:] 59 | } 60 | } 61 | return "" 62 | } 63 | 64 | // a and b both match /[0-9]+/ 65 | func cmpNumeric(a, b string) int { 66 | a = trimLeadingZeroes(a) 67 | b = trimLeadingZeroes(b) 68 | 69 | switch d := len(a) - len(b); { 70 | case d > 0: 71 | return 1 72 | case d < 0: 73 | return -1 74 | } 75 | for i := 0; i < len(a); i++ { 76 | switch { 77 | case a[i] > b[i]: 78 | return 1 79 | case a[i] < b[i]: 80 | return -1 81 | } 82 | } 83 | return 0 84 | } 85 | 86 | func matchEpoch(a string) bool { 87 | if len(a) == 0 { 88 | return false 89 | } 90 | if a[0] < '0' || a[0] > '9' { 91 | return false 92 | } 93 | var i int 94 | for i = 1; i < len(a) && a[i] >= '0' && a[i] <= '9'; i++ { 95 | } 96 | return i < len(a) && a[i] == ':' 97 | } 98 | 99 | func atMostOneDash(a string) bool { 100 | seen := false 101 | for i := 0; i < len(a); i++ { 102 | if a[i] == '-' { 103 | if seen { 104 | return false 105 | } 106 | seen = true 107 | } 108 | } 109 | return true 110 | } 111 | 112 | func nextFrag(s string) (frag, rest string, numeric bool) { 113 | if len(s) == 0 { 114 | return "", "", false 115 | } 116 | 117 | var i int 118 | if s[0] >= '0' && s[0] <= '9' { 119 | // is digit 120 | for i = 1; i < len(s) && s[i] >= '0' && s[i] <= '9'; i++ { 121 | } 122 | numeric = true 123 | } else { 124 | // not digit 125 | for i = 1; i < len(s) && (s[i] < '0' || s[i] > '9'); i++ { 126 | } 127 | } 128 | return s[:i], s[i:], numeric 129 | } 130 | 131 | func compareSubversion(va, vb string) int { 132 | var a, b string 133 | var anum, bnum bool 134 | var res int 135 | for res == 0 { 136 | a, va, anum = nextFrag(va) 137 | b, vb, bnum = nextFrag(vb) 138 | if a == "" && b == "" { 139 | break 140 | } 141 | if anum && bnum { 142 | res = cmpNumeric(a, b) 143 | } else { 144 | res = cmpString(a, b) 145 | } 146 | } 147 | return res 148 | } 149 | 150 | // CompareVersions compare two version strings that follow the debian 151 | // version policy and 152 | // Returns: 153 | // -1 if a is smaller than b 154 | // 0 if a equals b 155 | // +1 if a is bigger than b 156 | func CompareVersions(va, vb string) int { 157 | var sa, sb string 158 | if ia := strings.IndexByte(va, '-'); ia < 0 { 159 | sa = "0" 160 | } else { 161 | va, sa = va[:ia], va[ia+1:] 162 | } 163 | if ib := strings.IndexByte(vb, '-'); ib < 0 { 164 | sb = "0" 165 | } else { 166 | vb, sb = vb[:ib], vb[ib+1:] 167 | } 168 | 169 | // the main version number (before the "-") 170 | res := compareSubversion(va, vb) 171 | if res != 0 { 172 | return res 173 | } 174 | 175 | // the subversion revision behind the "-" 176 | return compareSubversion(sa, sb) 177 | } 178 | -------------------------------------------------------------------------------- /cmd/chisel/cmd_cut.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/jessevdk/go-flags" 5 | 6 | "fmt" 7 | "io/ioutil" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/canonical/chisel/internal/archive" 12 | "github.com/canonical/chisel/internal/cache" 13 | "github.com/canonical/chisel/internal/setup" 14 | "github.com/canonical/chisel/internal/slicer" 15 | ) 16 | 17 | var shortCutHelp = "Cut a tree with selected slices" 18 | var longCutHelp = ` 19 | The cut command uses the provided selection of package slices 20 | to create a new filesystem tree in the root location. 21 | ` 22 | 23 | var cutDescs = map[string]string{ 24 | "release": "Chisel release directory", 25 | "root": "Root for generated content", 26 | "arch": "Package architecture", 27 | } 28 | 29 | type cmdCut struct { 30 | Release string `long:"release" value-name:""` 31 | RootDir string `long:"root" value-name:"" required:"yes"` 32 | Arch string `long:"arch" value-name:""` 33 | 34 | Positional struct { 35 | SliceRefs []string `positional-arg-name:"" required:"yes"` 36 | } `positional-args:"yes"` 37 | } 38 | 39 | func init() { 40 | addCommand("cut", shortCutHelp, longCutHelp, func() flags.Commander { return &cmdCut{} }, cutDescs, nil) 41 | } 42 | 43 | func (cmd *cmdCut) Execute(args []string) error { 44 | if len(args) > 0 { 45 | return ErrExtraArgs 46 | } 47 | 48 | sliceKeys := make([]setup.SliceKey, len(cmd.Positional.SliceRefs)) 49 | for i, sliceRef := range cmd.Positional.SliceRefs { 50 | sliceKey, err := setup.ParseSliceKey(sliceRef) 51 | if err != nil { 52 | return err 53 | } 54 | sliceKeys[i] = sliceKey 55 | } 56 | 57 | var release *setup.Release 58 | var err error 59 | if strings.Contains(cmd.Release, "/") { 60 | release, err = setup.ReadRelease(cmd.Release) 61 | } else { 62 | var label, version string 63 | if cmd.Release == "" { 64 | label, version, err = readReleaseInfo() 65 | } else { 66 | label, version, err = parseReleaseInfo(cmd.Release) 67 | } 68 | if err != nil { 69 | return err 70 | } 71 | release, err = setup.FetchRelease(&setup.FetchOptions{ 72 | Label: label, 73 | Version: version, 74 | }) 75 | } 76 | if err != nil { 77 | return err 78 | } 79 | 80 | selection, err := setup.Select(release, sliceKeys) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | archives := make(map[string]archive.Archive) 86 | for archiveName, archiveInfo := range release.Archives { 87 | openArchive, err := archive.Open(&archive.Options{ 88 | Label: archiveName, 89 | Version: archiveInfo.Version, 90 | Arch: cmd.Arch, 91 | Suites: archiveInfo.Suites, 92 | Components: archiveInfo.Components, 93 | CacheDir: cache.DefaultDir("chisel"), 94 | }) 95 | if err != nil { 96 | return err 97 | } 98 | archives[archiveName] = openArchive 99 | } 100 | 101 | return slicer.Run(&slicer.RunOptions{ 102 | Selection: selection, 103 | Archives: archives, 104 | TargetDir: cmd.RootDir, 105 | }) 106 | 107 | return printVersions() 108 | } 109 | 110 | // TODO These need testing, and maybe moving into a common file. 111 | 112 | var releaseExp = regexp.MustCompile(`^([a-z](?:-?[a-z0-9]){2,})-([0-9]+(?:\.?[0-9])+)$`) 113 | 114 | func parseReleaseInfo(release string) (label, version string, err error) { 115 | match := releaseExp.FindStringSubmatch(release) 116 | if match == nil { 117 | return "", "", fmt.Errorf("invalid release reference: %q", release) 118 | } 119 | return match[1], match[2], nil 120 | } 121 | 122 | func readReleaseInfo() (label, version string, err error) { 123 | data, err := ioutil.ReadFile("/etc/lsb-release") 124 | if err == nil { 125 | const labelPrefix = "DISTRIB_ID=" 126 | const versionPrefix = "DISTRIB_RELEASE=" 127 | for _, line := range strings.Split(string(data), "\n") { 128 | switch { 129 | case strings.HasPrefix(line, labelPrefix): 130 | label = strings.ToLower(line[len(labelPrefix):]) 131 | case strings.HasPrefix(line, versionPrefix): 132 | version = line[len(versionPrefix):] 133 | } 134 | if label != "" && version != "" { 135 | return label, version, nil 136 | } 137 | } 138 | } 139 | return "", "", fmt.Errorf("cannot infer release via /etc/lsb-release, see the --release option") 140 | } 141 | -------------------------------------------------------------------------------- /internal/setup/tarjan.go: -------------------------------------------------------------------------------- 1 | // This file was copied from mgo, MongoDB driver for Go. 2 | // 3 | // Copyright (c) 2010-2013 - Gustavo Niemeyer 4 | // 5 | // All rights reserved. 6 | // 7 | // Redistribution and use in source and binary forms, with or without 8 | // modification, are permitted provided that the following conditions are met: 9 | // 10 | // 1. Redistributions of source code must retain the above copyright notice, this 11 | // list of conditions and the following disclaimer. 12 | // 2. Redistributions in binary form must reproduce the above copyright notice, 13 | // this list of conditions and the following disclaimer in the documentation 14 | // and/or other materials provided with the distribution. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | package setup 28 | 29 | import ( 30 | "sort" 31 | ) 32 | 33 | func tarjanSort(successors map[string][]string) [][]string { 34 | // http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm 35 | data := &tarjanData{ 36 | successors: successors, 37 | nodes: make([]tarjanNode, 0, len(successors)), 38 | index: make(map[string]int, len(successors)), 39 | } 40 | 41 | // Stabilize iteration through successors map to prevent 42 | // disjointed components producing unstable output due to 43 | // golang map randomized iteration. 44 | stableIDs := make([]string, 0, len(successors)) 45 | for id := range successors { 46 | stableIDs = append(stableIDs, id) 47 | } 48 | sort.Strings(stableIDs) 49 | for _, id := range stableIDs { 50 | if _, seen := data.index[id]; !seen { 51 | data.strongConnect(id) 52 | } 53 | } 54 | 55 | // Sort connected components to stabilize the algorithm. 56 | for _, ids := range data.output { 57 | if len(ids) > 1 { 58 | sort.Sort(idList(ids)) 59 | } 60 | } 61 | return data.output 62 | } 63 | 64 | type tarjanData struct { 65 | successors map[string][]string 66 | output [][]string 67 | 68 | nodes []tarjanNode 69 | stack []string 70 | index map[string]int 71 | } 72 | 73 | type tarjanNode struct { 74 | lowlink int 75 | stacked bool 76 | } 77 | 78 | type idList []string 79 | 80 | func (l idList) Len() int { return len(l) } 81 | func (l idList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 82 | func (l idList) Less(i, j int) bool { return l[i] < l[j] } 83 | 84 | func (data *tarjanData) strongConnect(id string) *tarjanNode { 85 | index := len(data.nodes) 86 | data.index[id] = index 87 | data.stack = append(data.stack, id) 88 | data.nodes = append(data.nodes, tarjanNode{index, true}) 89 | node := &data.nodes[index] 90 | 91 | for _, succid := range data.successors[id] { 92 | succindex, seen := data.index[succid] 93 | if !seen { 94 | succnode := data.strongConnect(succid) 95 | if succnode.lowlink < node.lowlink { 96 | node.lowlink = succnode.lowlink 97 | } 98 | } else if data.nodes[succindex].stacked { 99 | // Part of the current strongly-connected component. 100 | if succindex < node.lowlink { 101 | node.lowlink = succindex 102 | } 103 | } 104 | } 105 | 106 | if node.lowlink == index { 107 | // Root node; pop stack and output new 108 | // strongly-connected component. 109 | var scc []string 110 | i := len(data.stack) - 1 111 | for { 112 | stackid := data.stack[i] 113 | stackindex := data.index[stackid] 114 | data.nodes[stackindex].stacked = false 115 | scc = append(scc, stackid) 116 | if stackindex == index { 117 | break 118 | } 119 | i-- 120 | } 121 | data.stack = data.stack[:i] 122 | data.output = append(data.output, scc) 123 | } 124 | 125 | return node 126 | } 127 | -------------------------------------------------------------------------------- /internal/setup/fetch.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | 15 | "github.com/juju/fslock" 16 | 17 | "github.com/canonical/chisel/internal/cache" 18 | "github.com/canonical/chisel/internal/fsutil" 19 | ) 20 | 21 | type FetchOptions struct { 22 | Label string 23 | Version string 24 | CacheDir string 25 | } 26 | 27 | var bulkClient = &http.Client{ 28 | Timeout: 5 * time.Minute, 29 | } 30 | 31 | const baseURL = "https://codeload.github.com/canonical/chisel-releases/tar.gz/refs/heads/" 32 | 33 | func FetchRelease(options *FetchOptions) (*Release, error) { 34 | logf("Consulting release repository...") 35 | 36 | cacheDir := options.CacheDir 37 | if cacheDir == "" { 38 | cacheDir = cache.DefaultDir("chisel") 39 | } 40 | 41 | dirName := filepath.Join(cacheDir, "releases", options.Label + "-" + options.Version) 42 | err := os.MkdirAll(dirName, 0755) 43 | if err == nil { 44 | lockFile := fslock.New(filepath.Join(cacheDir, "releases", ".lock")) 45 | err = lockFile.LockWithTimeout(10 * time.Second) 46 | if err == nil { 47 | defer lockFile.Unlock() 48 | } 49 | } 50 | if err != nil { 51 | return nil, fmt.Errorf("cannot create cache directory: %w", err) 52 | } 53 | 54 | tagName := filepath.Join(dirName, ".etag") 55 | tagData, err := ioutil.ReadFile(tagName) 56 | if err != nil && !os.IsNotExist(err) { 57 | return nil, err 58 | } 59 | 60 | req, err := http.NewRequest("GET", baseURL + options.Label + "-" + options.Version, nil) 61 | if err != nil { 62 | return nil, fmt.Errorf("cannot create request for release information: %w", err) 63 | } 64 | req.Header.Add("If-None-Match", string(tagData)) 65 | 66 | resp, err := bulkClient.Do(req) 67 | if err != nil { 68 | return nil, fmt.Errorf("cannot talk to release repository: %w", err) 69 | } 70 | defer resp.Body.Close() 71 | 72 | cacheIsValid := false 73 | switch resp.StatusCode { 74 | case 200: 75 | // ok 76 | case 304: 77 | cacheIsValid = true 78 | case 401, 404: 79 | return nil, fmt.Errorf("no information for %s-%s release", options.Label, options.Version) 80 | default: 81 | return nil, fmt.Errorf("error from release repository: %v", resp.Status) 82 | } 83 | 84 | if cacheIsValid { 85 | logf("Cached %s-%s release is still up-to-date.", options.Label, options.Version) 86 | } else { 87 | logf("Fetching current %s-%s release...", options.Label, options.Version) 88 | if !strings.Contains(dirName, "/releases/") { 89 | // Better safe than sorry. 90 | return nil, fmt.Errorf("internal error: will not remove something unexpected: %s", dirName) 91 | } 92 | err = os.RemoveAll(dirName) 93 | if err != nil { 94 | return nil, fmt.Errorf("cannot remove previously cached release: %w", err) 95 | } 96 | err = extractTarGz(resp.Body, dirName) 97 | if err != nil { 98 | return nil, err 99 | } 100 | tag := resp.Header.Get("ETag") 101 | if tag != "" { 102 | err := ioutil.WriteFile(tagName, []byte(tag), 0644) 103 | if err != nil { 104 | return nil, fmt.Errorf("cannot write remote release tag file: %v", err) 105 | } 106 | } 107 | } 108 | 109 | return ReadRelease(dirName) 110 | } 111 | 112 | func extractTarGz(dataReader io.Reader, targetDir string) error { 113 | gzipReader, err := gzip.NewReader(dataReader) 114 | if err != nil { 115 | return err 116 | } 117 | defer gzipReader.Close() 118 | return extractTar(gzipReader, targetDir) 119 | } 120 | 121 | func extractTar(dataReader io.Reader, targetDir string) error { 122 | tarReader := tar.NewReader(dataReader) 123 | for { 124 | tarHeader, err := tarReader.Next() 125 | if err == io.EOF { 126 | break 127 | } 128 | if err != nil { 129 | return err 130 | } 131 | 132 | sourcePath := filepath.Clean(tarHeader.Name) 133 | if pos := strings.IndexByte(sourcePath, '/'); pos <= 0 || pos == len(sourcePath)-1 || sourcePath[0] == '.' { 134 | continue 135 | } else { 136 | sourcePath = sourcePath[pos+1:] 137 | } 138 | 139 | //debugf("Extracting header: %#v", tarHeader) 140 | 141 | err = fsutil.Create(&fsutil.CreateOptions{ 142 | Path: filepath.Join(targetDir, sourcePath), 143 | Mode: tarHeader.FileInfo().Mode(), 144 | Data: tarReader, 145 | Link: tarHeader.Linkname, 146 | }) 147 | if err != nil { 148 | return err 149 | } 150 | } 151 | return nil 152 | } 153 | -------------------------------------------------------------------------------- /internal/cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/canonical/chisel/internal/cache" 13 | ) 14 | 15 | const ( 16 | data1Digest = "5b41362bc82b7f3d56edc5a306db22105707d01ff4819e26faef9724a2d406c9" 17 | data2Digest = "d98cf53e0c8b77c14a96358d5b69584225b4bb9026423cbc2f7b0161894c402c" 18 | data3Digest = "f60f2d65da046fcaaf8a10bd96b5630104b629e111aff46ce89792e1caa11b18" 19 | ) 20 | 21 | func (s *S) TestDefaultDir(c *C) { 22 | oldA := os.Getenv("HOME") 23 | oldB := os.Getenv("XDG_CACHE_HOME") 24 | defer func() { 25 | os.Setenv("HOME", oldA) 26 | os.Setenv("XDG_CACHE_HOME", oldB) 27 | }() 28 | 29 | os.Setenv("HOME", "/home/user") 30 | os.Setenv("XDG_CACHE_HOME", "") 31 | c.Assert(cache.DefaultDir("foo/bar"), Equals, "/home/user/.cache/foo/bar") 32 | 33 | os.Setenv("HOME", "/home/user") 34 | os.Setenv("XDG_CACHE_HOME", "/xdg/cache") 35 | c.Assert(cache.DefaultDir("foo/bar"), Equals, "/xdg/cache/foo/bar") 36 | 37 | os.Setenv("HOME", "") 38 | os.Setenv("XDG_CACHE_HOME", "") 39 | defaultDir := cache.DefaultDir("foo/bar") 40 | c.Assert(strings.HasPrefix(defaultDir, os.TempDir()), Equals, true) 41 | c.Assert(strings.Contains(defaultDir, "/cache-"), Equals, true) 42 | c.Assert(strings.HasSuffix(defaultDir, "/foo/bar"), Equals, true) 43 | } 44 | 45 | 46 | func (s *S) TestCacheEmpty(c *C) { 47 | cc := cache.Cache{c.MkDir()} 48 | 49 | _, err := cc.Open(data1Digest) 50 | c.Assert(err, Equals, cache.MissErr) 51 | _, err = cc.Read(data1Digest) 52 | c.Assert(err, Equals, cache.MissErr) 53 | _, err = cc.Read("") 54 | c.Assert(err, Equals, cache.MissErr) 55 | } 56 | 57 | func (s *S) TestCacheReadWrite(c *C) { 58 | cc := cache.Cache{Dir: c.MkDir()} 59 | 60 | data1Path := filepath.Join(cc.Dir, "sha256", data1Digest) 61 | data2Path := filepath.Join(cc.Dir, "sha256", data2Digest) 62 | data3Path := filepath.Join(cc.Dir, "sha256", data3Digest) 63 | 64 | err := cc.Write(data1Digest, []byte("data1")) 65 | c.Assert(err, IsNil) 66 | data1, err := cc.Read(data1Digest) 67 | c.Assert(err, IsNil) 68 | c.Assert(string(data1), Equals, "data1") 69 | 70 | err = cc.Write("", []byte("data2")) 71 | c.Assert(err, IsNil) 72 | data2, err := cc.Read(data2Digest) 73 | c.Assert(err, IsNil) 74 | c.Assert(string(data2), Equals, "data2") 75 | 76 | _, err = cc.Read(data3Digest) 77 | c.Assert(err, Equals, cache.MissErr) 78 | _, err = cc.Read("") 79 | c.Assert(err, Equals, cache.MissErr) 80 | 81 | _, err = os.Stat(data1Path) 82 | c.Assert(err, IsNil) 83 | _, err = os.Stat(data2Path) 84 | c.Assert(err, IsNil) 85 | _, err = os.Stat(data3Path) 86 | c.Assert(os.IsNotExist(err), Equals, true) 87 | 88 | now := time.Now() 89 | expired := now.Add(-time.Hour - time.Second) 90 | err = os.Chtimes(data1Path, now, expired) 91 | c.Assert(err, IsNil) 92 | 93 | err = cc.Expire(time.Hour) 94 | c.Assert(err, IsNil) 95 | _, err = os.Stat(data1Path) 96 | c.Assert(os.IsNotExist(err), Equals, true) 97 | } 98 | 99 | func (s *S) TestCacheCreate(c *C) { 100 | cc := cache.Cache{Dir: c.MkDir()} 101 | 102 | w := cc.Create("") 103 | 104 | c.Assert(w.Digest(), Equals, "") 105 | 106 | _, err := w.Write([]byte("da")) 107 | c.Assert(err, IsNil) 108 | _, err = w.Write([]byte("ta")) 109 | c.Assert(err, IsNil) 110 | _, err = w.Write([]byte("1")) 111 | c.Assert(err, IsNil) 112 | err = w.Close() 113 | c.Assert(err, IsNil) 114 | 115 | c.Assert(w.Digest(), Equals, data1Digest) 116 | 117 | data1, err := cc.Read(data1Digest) 118 | c.Assert(err, IsNil) 119 | c.Assert(string(data1), Equals, "data1") 120 | } 121 | 122 | func (s *S) TestCacheWrongDigest(c *C) { 123 | cc := cache.Cache{Dir: c.MkDir()} 124 | 125 | w := cc.Create(data1Digest) 126 | 127 | c.Assert(w.Digest(), Equals, data1Digest) 128 | 129 | _, err := w.Write([]byte("data2")) 130 | errClose := w.Close() 131 | c.Assert(err, IsNil) 132 | c.Assert(errClose, ErrorMatches, "expected digest " + data1Digest + ", got " + data2Digest) 133 | 134 | _, err = cc.Read(data1Digest) 135 | c.Assert(err, Equals, cache.MissErr) 136 | _, err = cc.Read(data2Digest) 137 | c.Assert(err, Equals, cache.MissErr) 138 | } 139 | 140 | func (s *S) TestCacheOpen(c *C) { 141 | cc := cache.Cache{Dir: c.MkDir()} 142 | 143 | err := cc.Write(data1Digest, []byte("data1")) 144 | c.Assert(err, IsNil) 145 | 146 | f, err := cc.Open(data1Digest) 147 | c.Assert(err, IsNil) 148 | data1, err := ioutil.ReadAll(f) 149 | closeErr := f.Close() 150 | c.Assert(err, IsNil) 151 | c.Assert(closeErr, IsNil) 152 | 153 | c.Assert(string(data1), Equals, "data1") 154 | } 155 | -------------------------------------------------------------------------------- /internal/testutil/filecontentchecker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil_test 16 | 17 | import ( 18 | "io/ioutil" 19 | "path/filepath" 20 | "regexp" 21 | 22 | "gopkg.in/check.v1" 23 | 24 | . "github.com/canonical/chisel/internal/testutil" 25 | ) 26 | 27 | type fileContentCheckerSuite struct{} 28 | 29 | var _ = check.Suite(&fileContentCheckerSuite{}) 30 | 31 | type myStringer struct{ str string } 32 | 33 | func (m myStringer) String() string { return m.str } 34 | 35 | func (s *fileContentCheckerSuite) TestFileEquals(c *check.C) { 36 | d := c.MkDir() 37 | content := "not-so-random-string" 38 | filename := filepath.Join(d, "canary") 39 | c.Assert(ioutil.WriteFile(filename, []byte(content), 0644), check.IsNil) 40 | 41 | testInfo(c, FileEquals, "FileEquals", []string{"filename", "contents"}) 42 | testCheck(c, FileEquals, true, "", filename, content) 43 | testCheck(c, FileEquals, true, "", filename, []byte(content)) 44 | testCheck(c, FileEquals, true, "", filename, myStringer{content}) 45 | 46 | twofer := content + content 47 | testCheck(c, FileEquals, false, "Cannot match with file contents:\nnot-so-random-string", filename, twofer) 48 | testCheck(c, FileEquals, false, "Cannot match with file contents:\n", filename, []byte(twofer)) 49 | testCheck(c, FileEquals, false, "Cannot match with file contents:\nnot-so-random-string", filename, myStringer{twofer}) 50 | 51 | testCheck(c, FileEquals, false, `Cannot read file "": open : no such file or directory`, "", "") 52 | testCheck(c, FileEquals, false, "Filename must be a string", 42, "") 53 | testCheck(c, FileEquals, false, "Cannot compare file contents with something of type int", filename, 1) 54 | } 55 | 56 | func (s *fileContentCheckerSuite) TestFileContains(c *check.C) { 57 | d := c.MkDir() 58 | content := "not-so-random-string" 59 | filename := filepath.Join(d, "canary") 60 | c.Assert(ioutil.WriteFile(filename, []byte(content), 0644), check.IsNil) 61 | 62 | testInfo(c, FileContains, "FileContains", []string{"filename", "contents"}) 63 | testCheck(c, FileContains, true, "", filename, content[1:]) 64 | testCheck(c, FileContains, true, "", filename, []byte(content[1:])) 65 | testCheck(c, FileContains, true, "", filename, myStringer{content[1:]}) 66 | // undocumented 67 | testCheck(c, FileContains, true, "", filename, regexp.MustCompile(".*")) 68 | 69 | twofer := content + content 70 | testCheck(c, FileContains, false, "Cannot match with file contents:\nnot-so-random-string", filename, twofer) 71 | testCheck(c, FileContains, false, "Cannot match with file contents:\n", filename, []byte(twofer)) 72 | testCheck(c, FileContains, false, "Cannot match with file contents:\nnot-so-random-string", filename, myStringer{twofer}) 73 | // undocumented 74 | testCheck(c, FileContains, false, "Cannot match with file contents:\nnot-so-random-string", filename, regexp.MustCompile("^$")) 75 | 76 | testCheck(c, FileContains, false, `Cannot read file "": open : no such file or directory`, "", "") 77 | testCheck(c, FileContains, false, "Filename must be a string", 42, "") 78 | testCheck(c, FileContains, false, "Cannot compare file contents with something of type int", filename, 1) 79 | } 80 | 81 | func (s *fileContentCheckerSuite) TestFileMatches(c *check.C) { 82 | d := c.MkDir() 83 | content := "not-so-random-string" 84 | filename := filepath.Join(d, "canary") 85 | c.Assert(ioutil.WriteFile(filename, []byte(content), 0644), check.IsNil) 86 | 87 | testInfo(c, FileMatches, "FileMatches", []string{"filename", "regex"}) 88 | testCheck(c, FileMatches, true, "", filename, ".*") 89 | testCheck(c, FileMatches, true, "", filename, "^"+regexp.QuoteMeta(content)+"$") 90 | 91 | testCheck(c, FileMatches, false, "Cannot match with file contents:\nnot-so-random-string", filename, "^$") 92 | testCheck(c, FileMatches, false, "Cannot match with file contents:\nnot-so-random-string", filename, "123"+regexp.QuoteMeta(content)) 93 | 94 | testCheck(c, FileMatches, false, `Cannot read file "": open : no such file or directory`, "", "") 95 | testCheck(c, FileMatches, false, "Filename must be a string", 42, ".*") 96 | testCheck(c, FileMatches, false, "Regex must be a string", filename, 1) 97 | } 98 | -------------------------------------------------------------------------------- /internal/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "fmt" 7 | "hash" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "time" 13 | ) 14 | 15 | func DefaultDir(suffix string) string { 16 | cacheDir := os.Getenv("XDG_CACHE_HOME") 17 | if cacheDir == "" { 18 | homeDir := os.Getenv("HOME") 19 | if homeDir != "" { 20 | cacheDir = filepath.Join(homeDir, ".cache") 21 | } else { 22 | var err error 23 | cacheDir, err = os.MkdirTemp("", "cache-*") 24 | if err != nil { 25 | panic("no proper location for cache: " + err.Error()) 26 | } 27 | } 28 | } 29 | return filepath.Join(cacheDir, suffix) 30 | } 31 | 32 | type Cache struct { 33 | Dir string 34 | } 35 | 36 | type Writer struct { 37 | dir string 38 | digest string 39 | hash hash.Hash 40 | file *os.File 41 | err error 42 | } 43 | 44 | func (cw *Writer) fail(err error) error { 45 | if cw.err == nil { 46 | cw.err = err 47 | cw.file.Close() 48 | os.Remove(cw.file.Name()) 49 | } 50 | return err 51 | } 52 | 53 | func (cw *Writer) Write(data []byte) (n int, err error) { 54 | if cw.err != nil { 55 | return 0, cw.err 56 | } 57 | n, err = cw.file.Write(data) 58 | if err != nil { 59 | return n, cw.fail(err) 60 | } 61 | cw.hash.Write(data) 62 | return n, nil 63 | } 64 | 65 | func (cw *Writer) Close() error { 66 | if cw.err != nil { 67 | return cw.err 68 | } 69 | err := cw.file.Close() 70 | if err != nil { 71 | return cw.fail(err) 72 | } 73 | sum := cw.hash.Sum(nil) 74 | digest := hex.EncodeToString(sum[:]) 75 | if cw.digest == "" { 76 | cw.digest = digest 77 | } else if digest != cw.digest { 78 | return cw.fail(fmt.Errorf("expected digest %s, got %s", cw.digest, digest)) 79 | } 80 | fname := cw.file.Name() 81 | err = os.Rename(fname, filepath.Join(filepath.Dir(fname), cw.digest)) 82 | if err != nil { 83 | return cw.fail(err) 84 | } 85 | cw.err = io.EOF 86 | return nil 87 | } 88 | 89 | func (cw *Writer) Digest() string { 90 | return cw.digest 91 | } 92 | 93 | const digestKind = "sha256" 94 | 95 | var MissErr = fmt.Errorf("not cached") 96 | 97 | func (c *Cache) filePath(digest string) string { 98 | return filepath.Join(c.Dir, digestKind, digest) 99 | } 100 | 101 | func (c *Cache) Create(digest string) *Writer { 102 | if c.Dir == "" { 103 | return &Writer{err: fmt.Errorf("internal error: cache directory is unset")} 104 | } 105 | err := os.MkdirAll(filepath.Join(c.Dir, digestKind), 0755) 106 | if err != nil { 107 | return &Writer{err: fmt.Errorf("cannot create cache directory: %v", err)} 108 | } 109 | var file *os.File 110 | if digest == "" { 111 | file, err = os.CreateTemp(c.filePath(""), "tmp.*") 112 | } else { 113 | file, err = os.Create(c.filePath(digest + ".tmp")) 114 | } 115 | if err != nil { 116 | return &Writer{err: fmt.Errorf("cannot create cache file: %v", err)} 117 | } 118 | return &Writer{ 119 | dir: c.Dir, 120 | digest: digest, 121 | hash: sha256.New(), 122 | file: file, 123 | } 124 | } 125 | 126 | func (c *Cache) Write(digest string, data []byte) error { 127 | f := c.Create(digest) 128 | _, err1 := f.Write(data) 129 | err2 := f.Close() 130 | if err1 != nil { 131 | return err1 132 | } 133 | return err2 134 | } 135 | 136 | func (c *Cache) Open(digest string) (io.ReadCloser, error) { 137 | if c.Dir == "" || digest == "" { 138 | return nil, MissErr 139 | } 140 | filePath := c.filePath(digest) 141 | file, err := os.Open(filePath) 142 | if os.IsNotExist(err) { 143 | return nil, MissErr 144 | } else if err != nil { 145 | return nil, fmt.Errorf("cannot open cache file: %v", err) 146 | } 147 | // Use mtime as last reuse time. 148 | now := time.Now() 149 | if err := os.Chtimes(filePath, now, now); err != nil { 150 | return nil, fmt.Errorf("cannot update cached file timestamp: %v", err) 151 | } 152 | return file, nil 153 | } 154 | 155 | func (c *Cache) Read(digest string) ([]byte, error) { 156 | file, err := c.Open(digest) 157 | if err != nil { 158 | return nil, err 159 | } 160 | defer file.Close() 161 | data, err := ioutil.ReadAll(file) 162 | if err != nil { 163 | return nil, fmt.Errorf("cannot read file from cache: %v", err) 164 | } 165 | return data, nil 166 | } 167 | 168 | func (c *Cache) Expire(timeout time.Duration) error { 169 | list, err := ioutil.ReadDir(filepath.Join(c.Dir, digestKind)) 170 | if err != nil { 171 | return fmt.Errorf("cannot list cache directory: %v", err) 172 | } 173 | expired := time.Now().Add(-timeout) 174 | for _, finfo := range list { 175 | if finfo.ModTime().After(expired) { 176 | continue 177 | } 178 | err = os.Remove(filepath.Join(c.Dir, digestKind, finfo.Name())) 179 | if err != nil { 180 | return fmt.Errorf("cannot expire cache entry: %v", err) 181 | } 182 | } 183 | return nil 184 | } 185 | -------------------------------------------------------------------------------- /internal/archive/testarchive/testarchive.go: -------------------------------------------------------------------------------- 1 | package testarchive 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "crypto/sha256" 7 | "fmt" 8 | "path" 9 | "strings" 10 | 11 | "github.com/canonical/chisel/internal/testutil" 12 | ) 13 | 14 | type Item interface { 15 | Path() string 16 | Walk(f func(Item) error) error 17 | Section() []byte 18 | Content() []byte 19 | } 20 | 21 | func CallWalkFunc(this Item, f func(Item) error, items ...Item) error { 22 | if this != nil { 23 | err := f(this) 24 | if err != nil { 25 | return err 26 | } 27 | } 28 | for _, item := range items { 29 | err := item.Walk(f) 30 | if err != nil { 31 | return err 32 | } 33 | } 34 | return nil 35 | } 36 | 37 | type Gzip struct { 38 | Item Item 39 | } 40 | 41 | func (gz *Gzip) Path() string { 42 | return gz.Item.Path() + ".gz" 43 | } 44 | 45 | func (gz *Gzip) Walk(f func(Item) error) error { 46 | return CallWalkFunc(gz, f, gz.Item) 47 | } 48 | 49 | func (gz *Gzip) Section() []byte { 50 | return gz.Item.Section() 51 | } 52 | 53 | func (gz *Gzip) Content() []byte { 54 | return makeGzip(gz.Item.Content()) 55 | } 56 | 57 | type Package struct { 58 | Name string 59 | Version string 60 | Arch string 61 | Component string 62 | Data []byte 63 | } 64 | 65 | func (p *Package) Path() string { 66 | return fmt.Sprintf("pool/%s/%c/%s/%s_%subuntu1_%s.deb", p.Component, p.Name[0], p.Name, p.Name, p.Version, p.Arch) 67 | } 68 | 69 | func (p *Package) Walk(f func(Item) error) error { 70 | return CallWalkFunc(p, f) 71 | } 72 | 73 | func (p *Package) Section() []byte { 74 | content := p.Content() 75 | section := fmt.Sprintf(string(testutil.Reindent(` 76 | Package: %s 77 | Architecture: %s 78 | Version: %s 79 | Priority: required 80 | Essential: yes 81 | Section: admin 82 | Origin: Ubuntu 83 | Installed-Size: 10 84 | Filename: %s 85 | Size: %d 86 | SHA256: %s 87 | Description: Description of %s 88 | Task: minimal 89 | 90 | `)), p.Name, p.Arch, p.Version, p.Path(), len(content), makeSha256(content), p.Name) 91 | return []byte(section) 92 | } 93 | 94 | func (p *Package) Content() []byte { 95 | if len(p.Data) == 0 { 96 | return []byte(p.Name + " " + p.Version + " data") 97 | } 98 | return p.Data 99 | } 100 | 101 | type Release struct { 102 | Suite string 103 | Version string 104 | Items []Item 105 | } 106 | 107 | func (r *Release) Walk(f func(Item) error) error { 108 | return CallWalkFunc(r, f, r.Items...) 109 | } 110 | 111 | func (r *Release) Path() string { 112 | return "Release" 113 | } 114 | 115 | func (r *Release) Section() []byte { 116 | return nil 117 | } 118 | 119 | func (r *Release) Content() []byte { 120 | digests := bytes.Buffer{} 121 | for _, item := range r.Items { 122 | content := item.Content() 123 | digests.WriteString(fmt.Sprintf(" %s %d %s\n", makeSha256(content), len(content), item.Path())) 124 | } 125 | content := fmt.Sprintf(string(testutil.Reindent(` 126 | Origin: Ubuntu 127 | Label: Ubuntu 128 | Suite: %s 129 | Version: %s 130 | Codename: codename 131 | Date: Thu, 21 Apr 2022 17:16:08 UTC 132 | Architectures: amd64 arm64 armhf i386 ppc64el riscv64 s390x 133 | Components: main restricted universe multiverse 134 | Description: Ubuntu %s 135 | SHA256: 136 | %s 137 | `)), r.Suite, r.Version, r.Version, digests.String()) 138 | 139 | return []byte(content) 140 | } 141 | 142 | func (r *Release) Render(prefix string, content map[string][]byte) error { 143 | return r.Walk(func(item Item) error { 144 | itemPath := item.Path() 145 | if strings.HasPrefix(itemPath, "pool/") { 146 | itemPath = path.Join(prefix, itemPath) 147 | } else { 148 | itemPath = path.Join(prefix, "dists", r.Suite, itemPath) 149 | } 150 | content[itemPath] = item.Content() 151 | return nil 152 | }) 153 | } 154 | 155 | func MergeSections(items []Item) []byte { 156 | buf := bytes.Buffer{} 157 | for _, item := range items { 158 | buf.Write(item.Section()) 159 | } 160 | return buf.Bytes() 161 | } 162 | 163 | type PackageIndex struct { 164 | Component string 165 | Arch string 166 | Packages []Item 167 | } 168 | 169 | func (pi *PackageIndex) Path() string { 170 | return fmt.Sprintf("%s/binary-%s/Packages", pi.Component, pi.Arch) 171 | } 172 | 173 | func (pi *PackageIndex) Walk(f func(Item) error) error { 174 | return CallWalkFunc(pi, f, pi.Packages...) 175 | } 176 | 177 | func (pi *PackageIndex) Section() []byte { 178 | return nil 179 | } 180 | 181 | func (pi *PackageIndex) Content() []byte { 182 | return MergeSections(pi.Packages) 183 | } 184 | 185 | func makeSha256(b []byte) string { 186 | return fmt.Sprintf("%x", sha256.Sum256(b)) 187 | } 188 | 189 | func makeGzip(b []byte) []byte { 190 | var buf bytes.Buffer 191 | gz := gzip.NewWriter(&buf) 192 | _, err := gz.Write(b) 193 | if err != nil { 194 | panic(err) 195 | } 196 | err = gz.Close() 197 | if err != nil { 198 | panic(err) 199 | } 200 | return buf.Bytes() 201 | } 202 | -------------------------------------------------------------------------------- /internal/testutil/containschecker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | "strings" 21 | 22 | "gopkg.in/check.v1" 23 | ) 24 | 25 | type containsChecker struct { 26 | *check.CheckerInfo 27 | } 28 | 29 | // Contains is a Checker that looks for a elem in a container. 30 | // The elem can be any object. The container can be an array, slice or string. 31 | var Contains check.Checker = &containsChecker{ 32 | &check.CheckerInfo{Name: "Contains", Params: []string{"container", "elem"}}, 33 | } 34 | 35 | func commonEquals(container, elem interface{}, result *bool, error *string) bool { 36 | containerV := reflect.ValueOf(container) 37 | elemV := reflect.ValueOf(elem) 38 | switch containerV.Kind() { 39 | case reflect.Slice, reflect.Array, reflect.Map: 40 | containerElemType := containerV.Type().Elem() 41 | if containerElemType.Kind() == reflect.Interface { 42 | // Ensure that element implements the type of elements stored in the container. 43 | if !elemV.Type().Implements(containerElemType) { 44 | *result = false 45 | *error = fmt.Sprintf(""+ 46 | "container has items of interface type %s but expected"+ 47 | " element does not implement it", containerElemType) 48 | return true 49 | } 50 | } else { 51 | // Ensure that type of elements in container is compatible with elem 52 | if containerElemType != elemV.Type() { 53 | *result = false 54 | *error = fmt.Sprintf( 55 | "container has items of type %s but expected element is a %s", 56 | containerElemType, elemV.Type()) 57 | return true 58 | } 59 | } 60 | case reflect.String: 61 | // When container is a string, we expect elem to be a string as well 62 | if elemV.Kind() != reflect.String { 63 | *result = false 64 | *error = fmt.Sprintf("element is a %T but expected a string", elem) 65 | } else { 66 | *result = strings.Contains(containerV.String(), elemV.String()) 67 | *error = "" 68 | } 69 | return true 70 | } 71 | return false 72 | } 73 | 74 | func (c *containsChecker) Check(params []interface{}, names []string) (result bool, error string) { 75 | defer func() { 76 | if v := recover(); v != nil { 77 | result = false 78 | error = fmt.Sprint(v) 79 | } 80 | }() 81 | var container interface{} = params[0] 82 | var elem interface{} = params[1] 83 | if commonEquals(container, elem, &result, &error) { 84 | return 85 | } 86 | // Do the actual test using == 87 | switch containerV := reflect.ValueOf(container); containerV.Kind() { 88 | case reflect.Slice, reflect.Array: 89 | for length, i := containerV.Len(), 0; i < length; i++ { 90 | itemV := containerV.Index(i) 91 | if itemV.Interface() == elem { 92 | return true, "" 93 | } 94 | } 95 | return false, "" 96 | case reflect.Map: 97 | for _, keyV := range containerV.MapKeys() { 98 | itemV := containerV.MapIndex(keyV) 99 | if itemV.Interface() == elem { 100 | return true, "" 101 | } 102 | } 103 | return false, "" 104 | default: 105 | return false, fmt.Sprintf("%T is not a supported container", container) 106 | } 107 | } 108 | 109 | type deepContainsChecker struct { 110 | *check.CheckerInfo 111 | } 112 | 113 | // DeepContains is a Checker that looks for a elem in a container using 114 | // DeepEqual. The elem can be any object. The container can be an array, slice 115 | // or string. 116 | var DeepContains check.Checker = &deepContainsChecker{ 117 | &check.CheckerInfo{Name: "DeepContains", Params: []string{"container", "elem"}}, 118 | } 119 | 120 | func (c *deepContainsChecker) Check(params []interface{}, names []string) (result bool, error string) { 121 | var container interface{} = params[0] 122 | var elem interface{} = params[1] 123 | if commonEquals(container, elem, &result, &error) { 124 | return 125 | } 126 | // Do the actual test using reflect.DeepEqual 127 | switch containerV := reflect.ValueOf(container); containerV.Kind() { 128 | case reflect.Slice, reflect.Array: 129 | for length, i := containerV.Len(), 0; i < length; i++ { 130 | itemV := containerV.Index(i) 131 | if reflect.DeepEqual(itemV.Interface(), elem) { 132 | return true, "" 133 | } 134 | } 135 | return false, "" 136 | case reflect.Map: 137 | for _, keyV := range containerV.MapKeys() { 138 | itemV := containerV.MapIndex(keyV) 139 | if reflect.DeepEqual(itemV.Interface(), elem) { 140 | return true, "" 141 | } 142 | } 143 | return false, "" 144 | default: 145 | return false, fmt.Sprintf("%T is not a supported container", container) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /internal/scripts/scripts.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | import ( 4 | "go.starlark.net/resolve" 5 | "go.starlark.net/starlark" 6 | 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | func init() { 15 | resolve.AllowGlobalReassign = true 16 | } 17 | 18 | type Value = starlark.Value 19 | 20 | type RunOptions struct { 21 | Label string 22 | Namespace map[string]Value 23 | Script string 24 | } 25 | 26 | func Run(opts *RunOptions) error { 27 | thread := &starlark.Thread{Name: opts.Label} 28 | globals, err := starlark.ExecFile(thread, opts.Label, opts.Script, opts.Namespace) 29 | _ = globals 30 | return err 31 | } 32 | 33 | type ContentValue struct { 34 | RootDir string 35 | CheckRead func(path string) error 36 | CheckWrite func(path string) error 37 | } 38 | 39 | // Content starlark.Value interface 40 | // -------------------------------------------------------------------------- 41 | 42 | func (c *ContentValue) String() string { 43 | return "Content{...}" 44 | } 45 | 46 | func (c *ContentValue) Type() string { 47 | return "Content" 48 | } 49 | 50 | func (c *ContentValue) Freeze() { 51 | } 52 | 53 | func (c *ContentValue) Truth() starlark.Bool { 54 | return true 55 | } 56 | 57 | func (c *ContentValue) Hash() (uint32, error) { 58 | return starlark.String(c.RootDir).Hash() 59 | } 60 | 61 | // Content starlark.HasAttrs interface 62 | // -------------------------------------------------------------------------- 63 | 64 | var _ starlark.HasAttrs = new(ContentValue) 65 | 66 | func (c *ContentValue) Attr(name string) (Value, error) { 67 | switch name { 68 | case "read": 69 | return starlark.NewBuiltin("Content.read", c.Read), nil 70 | case "write": 71 | return starlark.NewBuiltin("Content.write", c.Write), nil 72 | case "list": 73 | return starlark.NewBuiltin("Content.list", c.List), nil 74 | } 75 | return nil, nil 76 | } 77 | 78 | func (c *ContentValue) AttrNames() []string { 79 | return []string{"read", "write", "list"} 80 | } 81 | 82 | // Content methods 83 | // -------------------------------------------------------------------------- 84 | 85 | type Check uint 86 | 87 | const ( 88 | CheckNone = 0 89 | CheckRead = 1 << iota 90 | CheckWrite 91 | ) 92 | 93 | func (c *ContentValue) RealPath(path string, what Check) (string, error) { 94 | if !filepath.IsAbs(c.RootDir) { 95 | return "", fmt.Errorf("internal error: content defined with relative root: %s", c.RootDir) 96 | } 97 | if !filepath.IsAbs(path) { 98 | return "", fmt.Errorf("content path must be absolute, got: %s", path) 99 | } 100 | cpath := filepath.Clean(path) 101 | if cpath != "/" && strings.HasSuffix(path, "/") { 102 | cpath += "/" 103 | } 104 | if c.CheckRead != nil && what&CheckRead != 0 { 105 | err := c.CheckRead(cpath) 106 | if err != nil { 107 | return "", err 108 | } 109 | } 110 | if c.CheckWrite != nil && what&CheckWrite != 0 { 111 | err := c.CheckWrite(cpath) 112 | if err != nil { 113 | return "", err 114 | } 115 | } 116 | rpath := filepath.Join(c.RootDir, path) 117 | if !filepath.IsAbs(rpath) || rpath != c.RootDir && !strings.HasPrefix(rpath, c.RootDir+string(filepath.Separator)) { 118 | return "", fmt.Errorf("invalid content path: %s", path) 119 | } 120 | if lname, err := os.Readlink(rpath); err == nil { 121 | lpath := filepath.Join(filepath.Dir(rpath), lname) 122 | lrel, err := filepath.Rel(c.RootDir, lpath) 123 | if err != nil || !filepath.IsAbs(lpath) || lpath != c.RootDir && !strings.HasPrefix(lpath, c.RootDir+string(filepath.Separator)) { 124 | return "", fmt.Errorf("invalid content symlink: %s", path) 125 | } 126 | _, err = c.RealPath("/"+lrel, what) 127 | if err != nil { 128 | return "", err 129 | } 130 | } 131 | return rpath, nil 132 | } 133 | 134 | func (c *ContentValue) polishError(path starlark.String, err error) error { 135 | if e, ok := err.(*os.PathError); ok { 136 | e.Path = path.GoString() 137 | } 138 | return err 139 | } 140 | 141 | func (c *ContentValue) Read(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (Value, error) { 142 | var path starlark.String 143 | err := starlark.UnpackArgs("Content.read", args, kwargs, "path", &path) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | fpath, err := c.RealPath(path.GoString(), CheckRead) 149 | if err != nil { 150 | return nil, err 151 | } 152 | data, err := ioutil.ReadFile(fpath) 153 | if err != nil { 154 | return nil, c.polishError(path, err) 155 | } 156 | return starlark.String(data), nil 157 | } 158 | 159 | func (c *ContentValue) Write(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (Value, error) { 160 | var path starlark.String 161 | var data starlark.String 162 | err := starlark.UnpackArgs("Content.write", args, kwargs, "path", &path, "data", &data) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | fpath, err := c.RealPath(path.GoString(), CheckWrite) 168 | if err != nil { 169 | return nil, err 170 | } 171 | fdata := []byte(data.GoString()) 172 | 173 | // No mode parameter for now as slices are supposed to list files 174 | // explicitly instead. 175 | err = ioutil.WriteFile(fpath, fdata, 0644) 176 | if err != nil { 177 | return nil, c.polishError(path, err) 178 | } 179 | return starlark.None, nil 180 | } 181 | 182 | func (c *ContentValue) List(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (Value, error) { 183 | var path starlark.String 184 | err := starlark.UnpackArgs("Content.list", args, kwargs, "path", &path) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | dpath := path.GoString() 190 | if !strings.HasSuffix(dpath, "/") { 191 | dpath += "/" 192 | } 193 | fpath, err := c.RealPath(dpath, CheckRead) 194 | if err != nil { 195 | return nil, err 196 | } 197 | entries, err := ioutil.ReadDir(fpath) 198 | if err != nil { 199 | return nil, c.polishError(path, err) 200 | } 201 | values := make([]Value, len(entries)) 202 | for i, entry := range entries { 203 | name := entry.Name() 204 | if entry.IsDir() { 205 | name += "/" 206 | } 207 | values[i] = starlark.String(name) 208 | } 209 | return starlark.NewList(values), nil 210 | } 211 | -------------------------------------------------------------------------------- /internal/testutil/exec.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "io/ioutil" 22 | "os" 23 | "os/exec" 24 | "path" 25 | "path/filepath" 26 | "strings" 27 | "sync" 28 | 29 | "gopkg.in/check.v1" 30 | ) 31 | 32 | var shellcheckPath string 33 | 34 | func init() { 35 | if p, err := exec.LookPath("shellcheck"); err == nil { 36 | shellcheckPath = p 37 | } 38 | } 39 | 40 | var ( 41 | shellchecked = make(map[string]bool, 16) 42 | shellcheckedMu sync.Mutex 43 | ) 44 | 45 | func shellcheckSeenAlready(script string) bool { 46 | shellcheckedMu.Lock() 47 | defer shellcheckedMu.Unlock() 48 | if shellchecked[script] { 49 | return true 50 | } 51 | shellchecked[script] = true 52 | return false 53 | } 54 | 55 | func maybeShellcheck(c *check.C, script string, wholeScript io.Reader) { 56 | // FakeCommand is used sometimes in SetUptTest, so it adds up 57 | // even for the empty script, don't recheck the essentially same 58 | // thing again and again! 59 | if shellcheckSeenAlready(script) { 60 | return 61 | } 62 | c.Logf("using shellcheck: %q", shellcheckPath) 63 | if shellcheckPath == "" { 64 | // no shellcheck, nothing to do 65 | return 66 | } 67 | cmd := exec.Command(shellcheckPath, "-s", "bash", "-") 68 | cmd.Stdin = wholeScript 69 | 70 | var out []byte 71 | var err error 72 | out, err = cmd.CombinedOutput() 73 | 74 | c.Check(err, check.IsNil, check.Commentf("shellcheck failed:\n%s", string(out))) 75 | } 76 | 77 | // FakeCmd allows faking commands for testing. 78 | type FakeCmd struct { 79 | binDir string 80 | exeFile string 81 | logFile string 82 | } 83 | 84 | // The top of the script generate the output to capture the 85 | // command that was run and the arguments used. To support 86 | // faking commands that need "\n" in their args (like zenity) 87 | // we use the following convention: 88 | // - generate \0 to separate args 89 | // - generate \0\0 to separate commands 90 | var scriptTpl = `#!/bin/bash 91 | printf "%%s" "$(basename "$0")" >> %[1]q 92 | printf '\0' >> %[1]q 93 | 94 | for arg in "$@"; do 95 | printf "%%s" "$arg" >> %[1]q 96 | printf '\0' >> %[1]q 97 | done 98 | 99 | printf '\0' >> %[1]q 100 | %s 101 | ` 102 | 103 | // FakeCommand adds a faked command. If the basename argument is a command 104 | // it is added to PATH. If it is an absolute path it is just created there. 105 | // the caller is responsible for the cleanup in this case. 106 | // 107 | // The command logs all invocations to a dedicated log file. If script is 108 | // non-empty then it is used as is and the caller is responsible for how the 109 | // script behaves (exit code and any extra behavior). If script is empty then 110 | // the command exits successfully without any other side-effect. 111 | func FakeCommand(c *check.C, basename, script string) *FakeCmd { 112 | var wholeScript bytes.Buffer 113 | var binDir, exeFile, logFile string 114 | if filepath.IsAbs(basename) { 115 | binDir = filepath.Dir(basename) 116 | exeFile = basename 117 | logFile = basename + ".log" 118 | } else { 119 | binDir = c.MkDir() 120 | exeFile = path.Join(binDir, basename) 121 | logFile = path.Join(binDir, basename+".log") 122 | os.Setenv("PATH", binDir+":"+os.Getenv("PATH")) 123 | } 124 | fmt.Fprintf(&wholeScript, scriptTpl, logFile, script) 125 | err := ioutil.WriteFile(exeFile, wholeScript.Bytes(), 0700) 126 | if err != nil { 127 | panic(err) 128 | } 129 | 130 | maybeShellcheck(c, script, &wholeScript) 131 | 132 | return &FakeCmd{binDir: binDir, exeFile: exeFile, logFile: logFile} 133 | } 134 | 135 | // Also fake this command, using the same bindir and log. 136 | // Useful when you want to check the ordering of things. 137 | func (cmd *FakeCmd) Also(basename, script string) *FakeCmd { 138 | exeFile := path.Join(cmd.binDir, basename) 139 | err := ioutil.WriteFile(exeFile, []byte(fmt.Sprintf(scriptTpl, cmd.logFile, script)), 0700) 140 | if err != nil { 141 | panic(err) 142 | } 143 | return &FakeCmd{binDir: cmd.binDir, exeFile: exeFile, logFile: cmd.logFile} 144 | } 145 | 146 | // Restore removes the faked command from PATH 147 | func (cmd *FakeCmd) Restore() { 148 | entries := strings.Split(os.Getenv("PATH"), ":") 149 | for i, entry := range entries { 150 | if entry == cmd.binDir { 151 | entries = append(entries[:i], entries[i+1:]...) 152 | break 153 | } 154 | } 155 | os.Setenv("PATH", strings.Join(entries, ":")) 156 | } 157 | 158 | // Calls returns a list of calls that were made to the fake command. 159 | // of the form: 160 | // [][]string{ 161 | // {"cmd", "arg1", "arg2"}, // first invocation of "cmd" 162 | // {"cmd", "arg1", "arg2"}, // second invocation of "cmd" 163 | // } 164 | func (cmd *FakeCmd) Calls() [][]string { 165 | raw, err := ioutil.ReadFile(cmd.logFile) 166 | if os.IsNotExist(err) { 167 | return nil 168 | } 169 | if err != nil { 170 | panic(err) 171 | } 172 | logContent := strings.TrimSuffix(string(raw), "\000") 173 | 174 | allCalls := [][]string{} 175 | calls := strings.Split(logContent, "\000\000") 176 | for _, call := range calls { 177 | call = strings.TrimSuffix(call, "\000") 178 | allCalls = append(allCalls, strings.Split(call, "\000")) 179 | } 180 | return allCalls 181 | } 182 | 183 | // ForgetCalls purges the list of calls made so far 184 | func (cmd *FakeCmd) ForgetCalls() { 185 | err := os.Remove(cmd.logFile) 186 | if os.IsNotExist(err) { 187 | return 188 | } 189 | if err != nil { 190 | panic(err) 191 | } 192 | } 193 | 194 | // BinDir returns the location of the directory holding overridden commands. 195 | func (cmd *FakeCmd) BinDir() string { 196 | return cmd.binDir 197 | } 198 | 199 | // Exe return the full path of the fake binary 200 | func (cmd *FakeCmd) Exe() string { 201 | return filepath.Join(cmd.exeFile) 202 | } 203 | -------------------------------------------------------------------------------- /internal/jsonwall/jsonwall_test.go: -------------------------------------------------------------------------------- 1 | package jsonwall_test 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | 6 | "bytes" 7 | 8 | "github.com/canonical/chisel/internal/jsonwall" 9 | ) 10 | 11 | type DataType struct { 12 | A string `json:"a,omitempty"` 13 | C string `json:"c,omitempty"` 14 | B string `json:"b,omitempty"` 15 | D string `json:"d,omitempty"` 16 | } 17 | 18 | type dataTypeGet struct { 19 | get any 20 | result any 21 | notFound bool 22 | getError string 23 | } 24 | 25 | type dataTypeIter struct { 26 | iter any 27 | results any 28 | } 29 | 30 | var dataTypeTests = []struct { 31 | summary string 32 | values []any 33 | options *jsonwall.DBWriterOptions 34 | database string 35 | dbError string 36 | getOps []dataTypeGet 37 | iterOps []dataTypeIter 38 | prefixOps []dataTypeIter 39 | }{{ 40 | summary: "Zero case", 41 | values: []any{}, 42 | database: `` + 43 | `{"jsonwall":"1.0","count":1}` + "\n" + 44 | ``, 45 | getOps: []dataTypeGet{{ 46 | get: &DataType{A: "foo"}, 47 | notFound: true, 48 | }}, 49 | iterOps: []dataTypeIter{{ 50 | iter: &DataType{A: "foo"}, 51 | results: []DataType(nil), 52 | }}, 53 | prefixOps: []dataTypeIter{{ 54 | iter: &DataType{A: "ba"}, 55 | results: []DataType(nil), 56 | }}, 57 | }, { 58 | summary: "Selection of basic tests", 59 | values: []any{ 60 | DataType{C: "3"}, 61 | DataType{A: "foo", B: "1"}, 62 | DataType{A: "baz", B: "3"}, 63 | &DataType{C: "1"}, 64 | &DataType{A: "bar", B: "2"}, 65 | &DataType{A: "baz", B: "4"}, 66 | &DataType{C: "2", B: "2"}, 67 | }, 68 | database: `` + 69 | `{"jsonwall":"1.0","count":8}` + "\n" + 70 | `{"a":"bar","b":"2"}` + "\n" + 71 | `{"a":"baz","b":"3"}` + "\n" + 72 | `{"a":"baz","b":"4"}` + "\n" + 73 | `{"a":"foo","b":"1"}` + "\n" + 74 | `{"c":"1"}` + "\n" + 75 | `{"c":"2","b":"2"}` + "\n" + 76 | `{"c":"3"}` + "\n" + 77 | ``, 78 | getOps: []dataTypeGet{{ 79 | get: &DataType{A: "foo"}, 80 | result: &DataType{A: "foo", B: "1"}, 81 | }, { 82 | get: &DataType{C: "2"}, 83 | result: &DataType{C: "2", B: "2"}, 84 | }}, 85 | iterOps: []dataTypeIter{{ 86 | iter: &DataType{A: "baz"}, 87 | results: []DataType{ 88 | {A: "baz", B: "3"}, 89 | {A: "baz", B: "4"}, 90 | }, 91 | }}, 92 | prefixOps: []dataTypeIter{{ 93 | iter: &DataType{A: "ba"}, 94 | results: []DataType{ 95 | {A: "bar", B: "2"}, 96 | {A: "baz", B: "3"}, 97 | {A: "baz", B: "4"}, 98 | }, 99 | }}, 100 | }, { 101 | summary: "Schema definition", 102 | options: &jsonwall.DBWriterOptions{Schema: "foo"}, 103 | values: []any{}, 104 | database: `` + 105 | `{"jsonwall":"1.0","schema":"foo","count":1}` + "\n" + 106 | ``, 107 | }, { 108 | summary: "Wrong format version", 109 | database: `` + 110 | `{"jsonwall":"2.0","count":1}` + "\n" + 111 | ``, 112 | dbError: `unsupported database format: "2\.0"`, 113 | }, { 114 | summary: "Compatible format version", 115 | database: `` + 116 | `{"jsonwall":"1.999","count":1}` + "\n" + 117 | `{"a":"foo","b":"1"}` + "\n" + 118 | ``, 119 | getOps: []dataTypeGet{{ 120 | get: &DataType{A: "foo"}, 121 | result: &DataType{A: "foo", B: "1"}, 122 | }}, 123 | }, { 124 | summary: "Extra newlines and spaces", 125 | database: `` + 126 | `{"jsonwall":"1.0","count":2}` + " \n \n \n" + 127 | `{"a":"bar","b":"1"}` + " \n \n \n" + 128 | `{"a":"foo","b":"2"}` + " \n \n \n" + 129 | ``, 130 | getOps: []dataTypeGet{{ 131 | get: &DataType{A: "foo"}, 132 | result: &DataType{A: "foo", B: "2"}, 133 | }, { 134 | get: &DataType{A: "bar"}, 135 | result: &DataType{A: "bar", B: "1"}, 136 | }}, 137 | }, { 138 | summary: "No trailing newline", 139 | database: `` + 140 | `{"jsonwall":"1.0","count":2}` + "\n" + 141 | `{"a":"bar","b":"1"}` + "\n" + 142 | `{"a":"foo","b":"2"}` + 143 | ``, 144 | getOps: []dataTypeGet{{ 145 | get: &DataType{A: "foo"}, 146 | result: &DataType{A: "foo", B: "2"}, 147 | }, { 148 | get: &DataType{A: "bar"}, 149 | result: &DataType{A: "bar", B: "1"}, 150 | }}, 151 | }, { 152 | summary: "Invalid add request", 153 | values: []any{ 154 | 42, 155 | }, 156 | dbError: "invalid database value: 42", 157 | }, { 158 | summary: "Invalid search request", 159 | values: []any{}, 160 | database: `` + 161 | `{"jsonwall":"1.0","count":1}` + "\n" + 162 | ``, 163 | getOps: []dataTypeGet{{ 164 | get: 42, 165 | getError: "invalid database search value: 42", 166 | }}, 167 | }} 168 | 169 | func (s *S) TestDataTypeTable(c *C) { 170 | for _, test := range dataTypeTests { 171 | c.Logf("Summary: %s", test.summary) 172 | buf := &bytes.Buffer{} 173 | if test.values == nil { 174 | buf.WriteString(test.database) 175 | } else { 176 | dbw := jsonwall.NewDBWriter(test.options) 177 | for _, value := range test.values { 178 | err := dbw.Add(value) 179 | if test.dbError != "" { 180 | c.Assert(err, ErrorMatches, test.dbError) 181 | } 182 | } 183 | if len(test.values) > 0 && test.dbError != "" { 184 | continue 185 | } 186 | _, err := dbw.WriteTo(buf) 187 | c.Assert(err, IsNil) 188 | c.Assert(buf.String(), Equals, test.database) 189 | } 190 | db, err := jsonwall.ReadDB(buf) 191 | if test.dbError != "" { 192 | c.Assert(err, ErrorMatches, test.dbError) 193 | continue 194 | } 195 | c.Assert(err, IsNil) 196 | if test.options != nil { 197 | c.Assert(db.Schema(), Equals, test.options.Schema) 198 | } 199 | for _, op := range test.getOps { 200 | err := db.Get(op.get) 201 | if op.notFound { 202 | c.Assert(err, Equals, jsonwall.ErrNotFound) 203 | } else if op.getError != "" { 204 | c.Assert(err, ErrorMatches, op.getError) 205 | } else { 206 | c.Assert(err, IsNil) 207 | c.Assert(op.get, DeepEquals, op.result) 208 | } 209 | } 210 | for _, op := range test.iterOps { 211 | iter, err := db.Iterate(op.iter) 212 | c.Assert(err, IsNil) 213 | var results []DataType 214 | for iter.Next() { 215 | var result DataType 216 | err := iter.Get(&result) 217 | c.Assert(err, IsNil) 218 | results = append(results, result) 219 | } 220 | c.Assert(results, DeepEquals, op.results) 221 | } 222 | for _, op := range test.prefixOps { 223 | iter, err := db.IteratePrefix(op.iter) 224 | c.Assert(err, IsNil) 225 | var results []DataType 226 | for iter.Next() { 227 | var result DataType 228 | err := iter.Get(&result) 229 | c.Assert(err, IsNil) 230 | results = append(results, result) 231 | } 232 | c.Assert(results, DeepEquals, op.results) 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /internal/deb/extract.go: -------------------------------------------------------------------------------- 1 | package deb 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "sort" 13 | "strings" 14 | "syscall" 15 | 16 | "github.com/blakesmith/ar" 17 | "github.com/klauspost/compress/zstd" 18 | "github.com/ulikunitz/xz" 19 | 20 | "github.com/canonical/chisel/internal/fsutil" 21 | "github.com/canonical/chisel/internal/strdist" 22 | ) 23 | 24 | type ExtractOptions struct { 25 | Package string 26 | TargetDir string 27 | Extract map[string][]ExtractInfo 28 | Globbed map[string][]string 29 | } 30 | 31 | type ExtractInfo struct { 32 | Path string 33 | Mode uint 34 | Optional bool 35 | } 36 | 37 | func checkExtractOptions(options *ExtractOptions) error { 38 | for extractPath, extractInfos := range options.Extract { 39 | isGlob := strings.ContainsAny(extractPath, "*?") 40 | if isGlob { 41 | if len(extractInfos) != 1 || extractInfos[0].Path != extractPath || extractInfos[0].Mode != 0 { 42 | return fmt.Errorf("when using wildcards source and target paths must match: %s", extractPath) 43 | } 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | func Extract(pkgReader io.Reader, options *ExtractOptions) (err error) { 50 | defer func() { 51 | if err != nil { 52 | err = fmt.Errorf("cannot extract from package %q: %w", options.Package, err) 53 | } 54 | }() 55 | 56 | logf("Extracting files from package %q...", options.Package) 57 | 58 | err = checkExtractOptions(options) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | _, err = os.Stat(options.TargetDir) 64 | if os.IsNotExist(err) { 65 | return fmt.Errorf("target directory does not exist") 66 | } else if err != nil { 67 | return err 68 | } 69 | 70 | arReader := ar.NewReader(pkgReader) 71 | var dataReader io.Reader 72 | for dataReader == nil { 73 | arHeader, err := arReader.Next() 74 | if err == io.EOF { 75 | return fmt.Errorf("no data payload") 76 | } 77 | if err != nil { 78 | return err 79 | } 80 | switch arHeader.Name { 81 | case "data.tar.gz": 82 | gzipReader, err := gzip.NewReader(arReader) 83 | if err != nil { 84 | return err 85 | } 86 | defer gzipReader.Close() 87 | dataReader = gzipReader 88 | case "data.tar.xz": 89 | xzReader, err := xz.NewReader(arReader) 90 | if err != nil { 91 | return err 92 | } 93 | dataReader = xzReader 94 | case "data.tar.zst": 95 | zstdReader, err := zstd.NewReader(arReader) 96 | if err != nil { 97 | return err 98 | } 99 | defer zstdReader.Close() 100 | dataReader = zstdReader 101 | } 102 | } 103 | return extractData(dataReader, options) 104 | } 105 | 106 | func extractData(dataReader io.Reader, options *ExtractOptions) error { 107 | 108 | oldUmask := syscall.Umask(0) 109 | defer func() { 110 | syscall.Umask(oldUmask) 111 | }() 112 | 113 | shouldExtract := func(pkgPath string) (globPath string, ok bool) { 114 | if pkgPath == "" { 115 | return "", false 116 | } 117 | pkgPathIsDir := pkgPath[len(pkgPath)-1] == '/' 118 | for extractPath, extractInfos := range options.Extract { 119 | if extractPath == "" { 120 | continue 121 | } 122 | switch { 123 | case strings.ContainsAny(extractPath, "*?"): 124 | if strdist.GlobPath(extractPath, pkgPath) { 125 | return extractPath, true 126 | } 127 | case extractPath == pkgPath: 128 | return "", true 129 | case pkgPathIsDir: 130 | for _, extractInfo := range extractInfos { 131 | if strings.HasPrefix(extractInfo.Path, pkgPath) { 132 | return "", true 133 | } 134 | } 135 | } 136 | } 137 | return "", false 138 | } 139 | 140 | pendingPaths := make(map[string]bool) 141 | for extractPath, extractInfos := range options.Extract { 142 | for _, extractInfo := range extractInfos { 143 | if !extractInfo.Optional { 144 | pendingPaths[extractPath] = true 145 | break 146 | } 147 | } 148 | } 149 | 150 | tarReader := tar.NewReader(dataReader) 151 | for { 152 | tarHeader, err := tarReader.Next() 153 | if err == io.EOF { 154 | break 155 | } 156 | if err != nil { 157 | return err 158 | } 159 | 160 | sourcePath := tarHeader.Name 161 | if len(sourcePath) < 3 || sourcePath[0] != '.' || sourcePath[1] != '/' { 162 | continue 163 | } 164 | sourcePath = sourcePath[1:] 165 | globPath, ok := shouldExtract(sourcePath) 166 | if !ok { 167 | continue 168 | } 169 | 170 | sourceIsDir := sourcePath[len(sourcePath)-1] == '/' 171 | 172 | //debugf("Extracting header: %#v", tarHeader) 173 | 174 | var extractInfos []ExtractInfo 175 | if globPath != "" { 176 | extractInfos = options.Extract[globPath] 177 | delete(pendingPaths, globPath) 178 | if options.Globbed != nil { 179 | options.Globbed[globPath] = append(options.Globbed[globPath], sourcePath) 180 | } 181 | } else { 182 | extractInfos, ok = options.Extract[sourcePath] 183 | if ok { 184 | delete(pendingPaths, sourcePath) 185 | } else { 186 | // Base directory for extracted content. Relevant mainly to preserve 187 | // the metadata, since the extracted content itself will also create 188 | // any missing directories unaccounted for in the options. 189 | err := fsutil.Create(&fsutil.CreateOptions{ 190 | Path: filepath.Join(options.TargetDir, sourcePath), 191 | Mode: tarHeader.FileInfo().Mode(), 192 | }) 193 | if err != nil { 194 | return err 195 | } 196 | continue 197 | } 198 | } 199 | 200 | var contentCache []byte 201 | var contentIsCached = len(extractInfos) > 1 && !sourceIsDir && globPath == "" 202 | if contentIsCached { 203 | // Read and cache the content so it may be reused. 204 | // As an alternative, to avoid having an entire file in 205 | // memory at once this logic might open the first file 206 | // written and copy it every time. For now, the choice 207 | // is speed over memory efficiency. 208 | data, err := ioutil.ReadAll(tarReader) 209 | if err != nil { 210 | return err 211 | } 212 | contentCache = data 213 | } 214 | 215 | var pathReader io.Reader = tarReader 216 | for _, extractInfo := range extractInfos { 217 | if contentIsCached { 218 | pathReader = bytes.NewReader(contentCache) 219 | } 220 | var targetPath string 221 | if globPath == "" { 222 | targetPath = filepath.Join(options.TargetDir, extractInfo.Path) 223 | } else { 224 | targetPath = filepath.Join(options.TargetDir, sourcePath) 225 | } 226 | if extractInfo.Mode != 0 { 227 | tarHeader.Mode = int64(extractInfo.Mode) 228 | } 229 | err := fsutil.Create(&fsutil.CreateOptions{ 230 | Path: targetPath, 231 | Mode: tarHeader.FileInfo().Mode(), 232 | Data: pathReader, 233 | Link: tarHeader.Linkname, 234 | }) 235 | if err != nil { 236 | return err 237 | } 238 | if globPath != "" { 239 | break 240 | } 241 | } 242 | } 243 | 244 | if len(pendingPaths) > 0 { 245 | pendingList := make([]string, 0, len(pendingPaths)) 246 | for pendingPath := range pendingPaths { 247 | pendingList = append(pendingList, pendingPath) 248 | } 249 | if len(pendingList) == 1 { 250 | return fmt.Errorf("no content at %s", pendingList[0]) 251 | } else { 252 | sort.Strings(pendingList) 253 | return fmt.Errorf("no content at:\n- %s", strings.Join(pendingList, "\n- ")) 254 | } 255 | } 256 | 257 | return nil 258 | } 259 | -------------------------------------------------------------------------------- /internal/scripts/scripts_test.go: -------------------------------------------------------------------------------- 1 | package scripts_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | . "gopkg.in/check.v1" 10 | 11 | "github.com/canonical/chisel/internal/scripts" 12 | "github.com/canonical/chisel/internal/testutil" 13 | ) 14 | 15 | type scriptsTest struct { 16 | summary string 17 | content map[string]string 18 | hackdir func(c *C, dir string) 19 | script string 20 | result map[string]string 21 | checkr func(path string) error 22 | checkw func(path string) error 23 | error string 24 | } 25 | 26 | var scriptsTests = []scriptsTest{{ 27 | summary: "Allow reassignment (non-standard Starlark)", 28 | script: ` 29 | data = 1 30 | data = 2 31 | `, 32 | result: map[string]string{}, 33 | }, { 34 | summary: "Overwrite a couple of files", 35 | content: map[string]string{ 36 | "foo/file1.txt": ``, 37 | "foo/file2.txt": ``, 38 | }, 39 | script: ` 40 | content.write("/foo/file1.txt", "data1") 41 | content.write("/foo/file2.txt", "data2") 42 | `, 43 | result: map[string]string{ 44 | "/foo/": "dir 0755", 45 | "/foo/file1.txt": "file 0644 5b41362b", 46 | "/foo/file2.txt": "file 0644 d98cf53e", 47 | }, 48 | }, { 49 | summary: "Read a file", 50 | content: map[string]string{ 51 | "foo/file1.txt": `data1`, 52 | "foo/file2.txt": ``, 53 | }, 54 | script: ` 55 | data = content.read("/foo/file1.txt") 56 | content.write("/foo/file2.txt", data) 57 | `, 58 | result: map[string]string{ 59 | "/foo/": "dir 0755", 60 | "/foo/file1.txt": "file 0644 5b41362b", 61 | "/foo/file2.txt": "file 0644 5b41362b", 62 | }, 63 | }, { 64 | summary: "List a directory", 65 | content: map[string]string{ 66 | "foo/file1.txt": `data1`, 67 | "foo/file2.txt": `data1`, 68 | "bar/file3.txt": `data1`, 69 | }, 70 | script: ` 71 | content.write("/foo/file1.txt", ",".join(content.list("/foo"))) 72 | content.write("/foo/file2.txt", ",".join(content.list("/"))) 73 | `, 74 | result: map[string]string{ 75 | "/foo/": "dir 0755", 76 | "/foo/file1.txt": "file 0644 98139a06", // "file1.txt,file2.txt" 77 | "/foo/file2.txt": "file 0644 47c22b01", // "bar/,foo/" 78 | "/bar/": "dir 0755", 79 | "/bar/file3.txt": "file 0644 5b41362b", 80 | }, 81 | }, { 82 | summary: "Forbid relative paths", 83 | content: map[string]string{ 84 | "foo/file1.txt": `data1`, 85 | }, 86 | script: ` 87 | content.read("foo/file1.txt") 88 | `, 89 | error: `content path must be absolute, got: foo/file1.txt`, 90 | }, { 91 | summary: "Forbid leaving the content root", 92 | content: map[string]string{ 93 | "foo/file1.txt": `data1`, 94 | }, 95 | script: ` 96 | content.read("/foo/../../file1.txt") 97 | `, 98 | error: `invalid content path: /foo/../../file1.txt`, 99 | }, { 100 | summary: "Forbid leaving the content via bad symlinks", 101 | content: map[string]string{ 102 | "foo/file3.txt": ``, 103 | }, 104 | hackdir: func(c *C, dir string) { 105 | fpath1 := filepath.Join(dir, "foo/file1.txt") 106 | fpath2 := filepath.Join(dir, "foo/file2.txt") 107 | c.Assert(os.Symlink("file2.txt", fpath1), IsNil) 108 | c.Assert(os.Symlink("../../bar", fpath2), IsNil) 109 | }, 110 | script: ` 111 | content.read("/foo/file1.txt") 112 | `, 113 | error: `invalid content symlink: /foo/file2.txt`, 114 | }, { 115 | summary: "Path errors refer to the root", 116 | content: map[string]string{}, 117 | script: ` 118 | content.read("/foo/file1.txt") 119 | `, 120 | error: `open /foo/file1.txt: no such file or directory`, 121 | }, { 122 | summary: "Check reads", 123 | content: map[string]string{ 124 | "bar/file1.txt": `data1`, 125 | }, 126 | script: ` 127 | content.write("/foo/../bar/file2.txt", "data2") 128 | content.read("/foo/../bar/file2.txt") 129 | `, 130 | checkr: func(p string) error { return fmt.Errorf("no read: %s", p) }, 131 | error: `no read: /bar/file2.txt`, 132 | }, { 133 | summary: "Check writes", 134 | content: map[string]string{ 135 | "bar/file1.txt": `data1`, 136 | }, 137 | script: ` 138 | content.read("/foo/../bar/file1.txt") 139 | content.write("/foo/../bar/file1.txt", "data1") 140 | `, 141 | checkw: func(p string) error { return fmt.Errorf("no write: %s", p) }, 142 | error: `no write: /bar/file1.txt`, 143 | }, { 144 | summary: "Check lists", 145 | content: map[string]string{ 146 | "bar/file1.txt": `data1`, 147 | }, 148 | script: ` 149 | content.write("/foo/../bar/file2.txt", "data2") 150 | content.list("/foo/../bar/") 151 | `, 152 | checkr: func(p string) error { return fmt.Errorf("no read: %s", p) }, 153 | error: `no read: /bar/`, 154 | }, { 155 | summary: "Check lists", 156 | content: map[string]string{ 157 | "bar/file1.txt": `data1`, 158 | }, 159 | script: ` 160 | content.write("/foo/../bar/file2.txt", "data2") 161 | content.list("/foo/../bar") 162 | `, 163 | checkr: func(p string) error { return fmt.Errorf("no read: %s", p) }, 164 | error: `no read: /bar/`, 165 | }, { 166 | summary: "Check reads on symlinks", 167 | content: map[string]string{ 168 | "foo/file2.txt": ``, 169 | }, 170 | hackdir: func(c *C, dir string) { 171 | fpath1 := filepath.Join(dir, "foo/file1.txt") 172 | c.Assert(os.Symlink("file2.txt", fpath1), IsNil) 173 | }, 174 | script: ` 175 | content.read("/foo/file1.txt") 176 | `, 177 | checkr: func(p string) error { 178 | if p == "/foo/file2.txt" { 179 | return fmt.Errorf("no read: %s", p) 180 | } 181 | return nil 182 | }, 183 | error: `no read: /foo/file2.txt`, 184 | }, { 185 | summary: "Check writes on symlinks", 186 | content: map[string]string{ 187 | "foo/file2.txt": ``, 188 | }, 189 | hackdir: func(c *C, dir string) { 190 | fpath1 := filepath.Join(dir, "foo/file1.txt") 191 | c.Assert(os.Symlink("file2.txt", fpath1), IsNil) 192 | }, 193 | script: ` 194 | content.write("/foo/file1.txt", "") 195 | `, 196 | checkw: func(p string) error { 197 | if p == "/foo/file2.txt" { 198 | return fmt.Errorf("no write: %s", p) 199 | } 200 | return nil 201 | }, 202 | error: `no write: /foo/file2.txt`, 203 | }} 204 | 205 | func (s *S) TestScripts(c *C) { 206 | for _, test := range scriptsTests { 207 | c.Logf("Summary: %s", test.summary) 208 | 209 | rootDir := c.MkDir() 210 | for path, data := range test.content { 211 | fpath := filepath.Join(rootDir, path) 212 | err := os.MkdirAll(filepath.Dir(fpath), 0755) 213 | c.Assert(err, IsNil) 214 | err = ioutil.WriteFile(fpath, []byte(data), 0644) 215 | c.Assert(err, IsNil) 216 | } 217 | if test.hackdir != nil { 218 | test.hackdir(c, rootDir) 219 | } 220 | 221 | content := &scripts.ContentValue{ 222 | RootDir: rootDir, 223 | CheckRead: test.checkr, 224 | CheckWrite: test.checkw, 225 | } 226 | namespace := map[string]scripts.Value{ 227 | "content": content, 228 | } 229 | err := scripts.Run(&scripts.RunOptions{ 230 | Namespace: namespace, 231 | Script: string(testutil.Reindent(test.script)), 232 | }) 233 | if test.error == "" { 234 | c.Assert(err, IsNil) 235 | } else { 236 | c.Assert(err, ErrorMatches, test.error) 237 | continue 238 | } 239 | 240 | c.Assert(testutil.TreeDump(rootDir), DeepEquals, test.result) 241 | } 242 | } 243 | 244 | func (s *S) TestContentRelative(c *C) { 245 | content := scripts.ContentValue{RootDir: "foo"} 246 | _, err := content.RealPath("/bar", scripts.CheckNone) 247 | c.Assert(err, ErrorMatches, "internal error: content defined with relative root: foo") 248 | } 249 | -------------------------------------------------------------------------------- /cmd/chisel/cmd_help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "strings" 9 | "unicode/utf8" 10 | 11 | "github.com/jessevdk/go-flags" 12 | ) 13 | 14 | var shortHelpHelp = "Show help about a command" 15 | var longHelpHelp = ` 16 | The help command displays information about commands. 17 | ` 18 | 19 | // addHelp adds --help like what go-flags would do for us, but hidden 20 | func addHelp(parser *flags.Parser) error { 21 | var help struct { 22 | ShowHelp func() error `short:"h" long:"help"` 23 | } 24 | help.ShowHelp = func() error { 25 | // this function is called via --help (or -h). In that 26 | // case, parser.Command.Active should be the command 27 | // on which help is being requested (like "chisel foo 28 | // --help", active is foo), or nil in the toplevel. 29 | if parser.Command.Active == nil { 30 | // this means *either* a bare 'chisel --help', 31 | // *or* 'chisel --help command' 32 | // 33 | // If we return nil in the first case go-flags 34 | // will throw up an ErrCommandRequired on its 35 | // own, but in the second case it'll go on to 36 | // run the command, which is very unexpected. 37 | // 38 | // So we force the ErrCommandRequired here. 39 | 40 | // toplevel --help gets handled via ErrCommandRequired 41 | return &flags.Error{Type: flags.ErrCommandRequired} 42 | } 43 | // not toplevel, so ask for regular help 44 | return &flags.Error{Type: flags.ErrHelp} 45 | } 46 | hlpgrp, err := parser.AddGroup("Help Options", "", &help) 47 | if err != nil { 48 | return err 49 | } 50 | hlpgrp.Hidden = true 51 | hlp := parser.FindOptionByLongName("help") 52 | hlp.Description = "Show this help message" 53 | hlp.Hidden = true 54 | 55 | return nil 56 | } 57 | 58 | type cmdHelp struct { 59 | All bool `long:"all"` 60 | Manpage bool `long:"man" hidden:"true"` 61 | Positional struct { 62 | Subs []string `positional-arg-name:""` 63 | } `positional-args:"yes"` 64 | parser *flags.Parser 65 | } 66 | 67 | func init() { 68 | addCommand("help", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdHelp{} }, 69 | map[string]string{ 70 | "all": "Show a short summary of all commands", 71 | "man": "Generate the manpage", 72 | }, nil) 73 | } 74 | 75 | func (cmd *cmdHelp) setParser(parser *flags.Parser) { 76 | cmd.parser = parser 77 | } 78 | 79 | // manfixer is a hackish way to fix drawbacks in the generated manpage: 80 | // - no way to get it into section 8 81 | // - duplicated TP lines that break older groff (e.g. 14.04), lp:1814767 82 | type manfixer struct { 83 | bytes.Buffer 84 | done bool 85 | } 86 | 87 | func (w *manfixer) Write(buf []byte) (int, error) { 88 | if !w.done { 89 | w.done = true 90 | if bytes.HasPrefix(buf, []byte(".TH chisel 1 ")) { 91 | // io.Writer.Write must not modify the buffer, even temporarily 92 | n, _ := w.Buffer.Write(buf[:9]) 93 | w.Buffer.Write([]byte{'8'}) 94 | m, err := w.Buffer.Write(buf[10:]) 95 | return n + m + 1, err 96 | } 97 | } 98 | return w.Buffer.Write(buf) 99 | } 100 | 101 | var tpRegexp = regexp.MustCompile(`(?m)(?:^\.TP\n)+`) 102 | 103 | func (w *manfixer) flush() { 104 | str := tpRegexp.ReplaceAllLiteralString(w.Buffer.String(), ".TP\n") 105 | io.Copy(Stdout, strings.NewReader(str)) 106 | } 107 | 108 | func (cmd cmdHelp) Execute(args []string) error { 109 | if len(args) > 0 { 110 | return ErrExtraArgs 111 | } 112 | if cmd.Manpage { 113 | // you shouldn't try to to combine --man with --all nor a 114 | // subcommand, but --man is hidden so no real need to check. 115 | out := &manfixer{} 116 | cmd.parser.WriteManPage(out) 117 | out.flush() 118 | return nil 119 | } 120 | if cmd.All { 121 | if len(cmd.Positional.Subs) > 0 { 122 | return fmt.Errorf("help accepts a command, or '--all', but not both.") 123 | } 124 | printLongHelp(cmd.parser) 125 | return nil 126 | } 127 | 128 | var subcmd = cmd.parser.Command 129 | for _, subname := range cmd.Positional.Subs { 130 | subcmd = subcmd.Find(subname) 131 | if subcmd == nil { 132 | sug := "chisel help" 133 | if x := cmd.parser.Command.Active; x != nil && x.Name != "help" { 134 | sug = "chisel help " + x.Name 135 | } 136 | return fmt.Errorf("unknown command %q, see '%s'.", subname, sug) 137 | } 138 | // this makes "chisel help foo" work the same as "chisel foo --help" 139 | cmd.parser.Command.Active = subcmd 140 | } 141 | if subcmd != cmd.parser.Command { 142 | return &flags.Error{Type: flags.ErrHelp} 143 | } 144 | return &flags.Error{Type: flags.ErrCommandRequired} 145 | } 146 | 147 | type helpCategory struct { 148 | Label string 149 | Description string 150 | Commands []string 151 | } 152 | 153 | // helpCategories helps us by grouping commands 154 | var helpCategories = []helpCategory{{ 155 | Label: "Basic", 156 | Description: "general operations", 157 | Commands: []string{"help", "version"}, 158 | }, { 159 | Label: "Action", 160 | Description: "make things happen", 161 | Commands: []string{"cut"}, 162 | }} 163 | 164 | var ( 165 | longChiselDescription = strings.TrimSpace(` 166 | Chisel can slice a Linux distribution using a release database 167 | and construct a new filesystem using the finely defined parts. 168 | `) 169 | chiselUsage = "Usage: chisel [...]" 170 | chiselHelpCategoriesIntro = "Commands can be classified as follows:" 171 | chiselHelpAllFooter = "For more information about a command, run 'chisel help '." 172 | chiselHelpFooter = "For a short summary of all commands, run 'chisel help --all'." 173 | ) 174 | 175 | func printHelpHeader() { 176 | fmt.Fprintln(Stdout, longChiselDescription) 177 | fmt.Fprintln(Stdout) 178 | fmt.Fprintln(Stdout, chiselUsage) 179 | fmt.Fprintln(Stdout) 180 | fmt.Fprintln(Stdout, chiselHelpCategoriesIntro) 181 | } 182 | 183 | func printHelpAllFooter() { 184 | fmt.Fprintln(Stdout) 185 | fmt.Fprintln(Stdout, chiselHelpAllFooter) 186 | } 187 | 188 | func printHelpFooter() { 189 | printHelpAllFooter() 190 | fmt.Fprintln(Stdout, chiselHelpFooter) 191 | } 192 | 193 | // this is called when the Execute returns a flags.Error with ErrCommandRequired 194 | func printShortHelp() { 195 | printHelpHeader() 196 | fmt.Fprintln(Stdout) 197 | maxLen := 0 198 | for _, categ := range helpCategories { 199 | if l := utf8.RuneCountInString(categ.Label); l > maxLen { 200 | maxLen = l 201 | } 202 | } 203 | for _, categ := range helpCategories { 204 | fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, categ.Label, strings.Join(categ.Commands, ", ")) 205 | } 206 | printHelpFooter() 207 | } 208 | 209 | // this is "chisel help --all" 210 | func printLongHelp(parser *flags.Parser) { 211 | printHelpHeader() 212 | maxLen := 0 213 | for _, categ := range helpCategories { 214 | for _, command := range categ.Commands { 215 | if l := len(command); l > maxLen { 216 | maxLen = l 217 | } 218 | } 219 | } 220 | 221 | // flags doesn't have a LookupCommand? 222 | commands := parser.Commands() 223 | cmdLookup := make(map[string]*flags.Command, len(commands)) 224 | for _, cmd := range commands { 225 | cmdLookup[cmd.Name] = cmd 226 | } 227 | 228 | for _, categ := range helpCategories { 229 | fmt.Fprintln(Stdout) 230 | fmt.Fprintf(Stdout, " %s (%s):\n", categ.Label, categ.Description) 231 | for _, name := range categ.Commands { 232 | cmd := cmdLookup[name] 233 | if cmd == nil { 234 | fmt.Fprintf(Stderr, "??? Cannot find command %q mentioned in help categories, please report!\n", name) 235 | } else { 236 | fmt.Fprintf(Stdout, " %*s %s\n", -maxLen, name, cmd.ShortDescription) 237 | } 238 | } 239 | } 240 | printHelpAllFooter() 241 | } 242 | -------------------------------------------------------------------------------- /internal/archive/archive.go: -------------------------------------------------------------------------------- 1 | package archive 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/canonical/chisel/internal/cache" 12 | "github.com/canonical/chisel/internal/control" 13 | "github.com/canonical/chisel/internal/deb" 14 | ) 15 | 16 | type Archive interface { 17 | Options() *Options 18 | Fetch(pkg string) (io.ReadCloser, error) 19 | Exists(pkg string) bool 20 | } 21 | 22 | type Options struct { 23 | Label string 24 | Version string 25 | Arch string 26 | Suites []string 27 | Components []string 28 | CacheDir string 29 | } 30 | 31 | func Open(options *Options) (Archive, error) { 32 | var err error 33 | if options.Arch == "" { 34 | options.Arch, err = deb.InferArch() 35 | } else { 36 | err = deb.ValidateArch(options.Arch) 37 | } 38 | if err != nil { 39 | return nil, err 40 | } 41 | return openUbuntu(options) 42 | } 43 | 44 | var httpClient = &http.Client{ 45 | Timeout: 30 * time.Second, 46 | } 47 | 48 | var httpDo = httpClient.Do 49 | 50 | var bulkClient = &http.Client{ 51 | Timeout: 5 * time.Minute, 52 | } 53 | 54 | var bulkDo = bulkClient.Do 55 | 56 | type ubuntuArchive struct { 57 | options Options 58 | indexes []*ubuntuIndex 59 | cache *cache.Cache 60 | } 61 | 62 | type ubuntuIndex struct { 63 | label string 64 | version string 65 | arch string 66 | suite string 67 | component string 68 | release control.Section 69 | packages control.File 70 | cache *cache.Cache 71 | } 72 | 73 | func (a *ubuntuArchive) Options() *Options { 74 | return &a.options 75 | } 76 | 77 | func (a *ubuntuArchive) Exists(pkg string) bool { 78 | _, _, err := a.selectPackage(pkg) 79 | return err == nil 80 | } 81 | 82 | func (a *ubuntuArchive) selectPackage(pkg string) (control.Section, *ubuntuIndex, error) { 83 | var selectedVersion string 84 | var selectedSection control.Section 85 | var selectedIndex *ubuntuIndex 86 | for _, index := range a.indexes { 87 | section := index.packages.Section(pkg) 88 | if section != nil && section.Get("Filename") != "" { 89 | version := section.Get("Version") 90 | if selectedVersion == "" || deb.CompareVersions(selectedVersion, version) < 0 { 91 | selectedVersion = version 92 | selectedSection = section 93 | selectedIndex = index 94 | } 95 | } 96 | } 97 | if selectedVersion == "" { 98 | return nil, nil, fmt.Errorf("cannot find package %q in archive", pkg) 99 | } 100 | return selectedSection, selectedIndex, nil 101 | } 102 | 103 | func (a *ubuntuArchive) Fetch(pkg string) (io.ReadCloser, error) { 104 | section, index, err := a.selectPackage(pkg) 105 | if err != nil { 106 | return nil, err 107 | } 108 | suffix := section.Get("Filename") 109 | logf("Fetching %s...", suffix) 110 | reader, err := index.fetch("../../"+suffix, section.Get("SHA256")) 111 | if err != nil { 112 | return nil, err 113 | } 114 | return reader, nil 115 | } 116 | 117 | const ubuntuURL = "http://archive.ubuntu.com/ubuntu/" 118 | const ubuntuPortsURL = "http://ports.ubuntu.com/ubuntu-ports/" 119 | 120 | func openUbuntu(options *Options) (Archive, error) { 121 | if len(options.Components) == 0 { 122 | return nil, fmt.Errorf("archive options missing components") 123 | } 124 | if len(options.Suites) == 0 { 125 | return nil, fmt.Errorf("archive options missing suites") 126 | } 127 | if len(options.Version) == 0 { 128 | return nil, fmt.Errorf("archive options missing version") 129 | } 130 | 131 | archive := &ubuntuArchive{ 132 | options: *options, 133 | cache: &cache.Cache{ 134 | Dir: options.CacheDir, 135 | }, 136 | } 137 | 138 | for _, suite := range options.Suites { 139 | var release control.Section 140 | for _, component := range options.Components { 141 | index := &ubuntuIndex{ 142 | label: options.Label, 143 | version: options.Version, 144 | arch: options.Arch, 145 | suite: suite, 146 | component: component, 147 | release: release, 148 | cache: archive.cache, 149 | } 150 | if release == nil { 151 | err := index.fetchRelease() 152 | if err != nil { 153 | return nil, err 154 | } 155 | release = index.release 156 | err = index.checkComponents(options.Components) 157 | if err != nil { 158 | return nil, err 159 | } 160 | } 161 | err := index.fetchIndex() 162 | if err != nil { 163 | return nil, err 164 | } 165 | archive.indexes = append(archive.indexes, index) 166 | } 167 | } 168 | 169 | return archive, nil 170 | } 171 | 172 | func (index *ubuntuIndex) fetchRelease() error { 173 | logf("Fetching %s %s %s suite details...", index.label, index.version, index.suite) 174 | reader, err := index.fetch("Release", "") 175 | if err != nil { 176 | return err 177 | } 178 | 179 | ctrl, err := control.ParseReader("Label", reader) 180 | if err != nil { 181 | return fmt.Errorf("parsing archive Release file: %v", err) 182 | } 183 | section := ctrl.Section("Ubuntu") 184 | if section == nil { 185 | return fmt.Errorf("corrupted archive Release file: no Ubuntu section") 186 | } 187 | logf("Release date: %s", section.Get("Date")) 188 | 189 | index.release = section 190 | return nil 191 | } 192 | 193 | func (index *ubuntuIndex) fetchIndex() error { 194 | digests := index.release.Get("SHA256") 195 | packagesPath := fmt.Sprintf("%s/binary-%s/Packages", index.component, index.arch) 196 | digest, _, _ := control.ParsePathInfo(digests, packagesPath) 197 | if digest == "" { 198 | return fmt.Errorf("%s is missing from %s %s component digests", packagesPath, index.suite, index.component) 199 | } 200 | 201 | logf("Fetching index for %s %s %s %s component...", index.label, index.version, index.suite, index.component) 202 | reader, err := index.fetch(packagesPath+".gz", digest) 203 | if err != nil { 204 | return err 205 | } 206 | ctrl, err := control.ParseReader("Package", reader) 207 | if err != nil { 208 | return fmt.Errorf("parsing archive Package file: %v", err) 209 | } 210 | 211 | index.packages = ctrl 212 | return nil 213 | } 214 | 215 | func (index *ubuntuIndex) checkComponents(components []string) error { 216 | releaseComponents := strings.Fields(index.release.Get("Components")) 217 | for _, c1 := range components { 218 | found := false 219 | for _, c2 := range releaseComponents { 220 | if c1 == c2 { 221 | found = true 222 | break 223 | } 224 | } 225 | if !found { 226 | return fmt.Errorf("archive has no component %q", c1) 227 | } 228 | } 229 | return nil 230 | } 231 | 232 | func (index *ubuntuIndex) fetch(suffix, digest string) (io.ReadCloser, error) { 233 | reader, err := index.cache.Open(digest) 234 | if err == nil { 235 | return reader, nil 236 | } else if err != cache.MissErr { 237 | return nil, err 238 | } 239 | 240 | baseURL := ubuntuURL 241 | if index.arch != "amd64" && index.arch != "i386" { 242 | baseURL = ubuntuPortsURL 243 | } 244 | 245 | var url string 246 | if strings.HasPrefix(suffix, "pool/") { 247 | url = baseURL + suffix 248 | } else { 249 | url = baseURL + "dists/" + index.suite + "/" + suffix 250 | } 251 | 252 | req, err := http.NewRequest("GET", url, nil) 253 | if err != nil { 254 | return nil, fmt.Errorf("cannot create HTTP request: %v", err) 255 | } 256 | resp, err := httpDo(req) 257 | if err != nil { 258 | return nil, fmt.Errorf("cannot talk to archive: %v", err) 259 | } 260 | defer resp.Body.Close() 261 | 262 | switch resp.StatusCode { 263 | case 200: 264 | // ok 265 | case 401, 404: 266 | return nil, fmt.Errorf("cannot find archive data") 267 | default: 268 | return nil, fmt.Errorf("error from archive: %v", resp.Status) 269 | } 270 | 271 | body := resp.Body 272 | if strings.HasSuffix(suffix, ".gz") { 273 | reader, err := gzip.NewReader(body) 274 | if err != nil { 275 | return nil, fmt.Errorf("cannot decompress data: %v", err) 276 | } 277 | defer reader.Close() 278 | body = reader 279 | } 280 | 281 | writer := index.cache.Create(digest) 282 | defer writer.Close() 283 | 284 | _, err = io.Copy(writer, body) 285 | if err == nil { 286 | err = writer.Close() 287 | } 288 | if err != nil { 289 | return nil, fmt.Errorf("cannot fetch from archive: %v", err) 290 | } 291 | 292 | return index.cache.Open(writer.Digest()) 293 | } 294 | -------------------------------------------------------------------------------- /internal/testutil/pkgdata_test.go: -------------------------------------------------------------------------------- 1 | package testutil_test 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "io" 7 | "time" 8 | 9 | "github.com/blakesmith/ar" 10 | "github.com/canonical/chisel/internal/testutil" 11 | "github.com/klauspost/compress/zstd" 12 | . "gopkg.in/check.v1" 13 | ) 14 | 15 | type pkgdataSuite struct{} 16 | 17 | var _ = Suite(&pkgdataSuite{}) 18 | 19 | type checkTarEntry struct { 20 | tarEntry testutil.TarEntry 21 | checkHeader tar.Header 22 | } 23 | 24 | var epochStartTime time.Time = time.Unix(0, 0) 25 | 26 | var pkgdataCheckEntries = []checkTarEntry{{ 27 | testutil.TarEntry{ 28 | Header: tar.Header{ 29 | Name: "./", 30 | }, 31 | }, 32 | tar.Header{ 33 | Typeflag: tar.TypeDir, 34 | Name: "./", 35 | Mode: 00755, 36 | Uname: "root", 37 | Gname: "root", 38 | ModTime: epochStartTime, 39 | Format: tar.FormatGNU, 40 | }, 41 | }, { 42 | testutil.TarEntry{ 43 | Header: tar.Header{ 44 | Name: "./admin/", 45 | Mode: 00700, 46 | Uname: "admin", 47 | }, 48 | }, 49 | tar.Header{ 50 | Typeflag: tar.TypeDir, 51 | Name: "./admin/", 52 | Mode: 00700, 53 | Uname: "admin", 54 | Gname: "root", 55 | ModTime: epochStartTime, 56 | Format: tar.FormatGNU, 57 | }, 58 | }, { 59 | testutil.TarEntry{ 60 | Header: tar.Header{ 61 | Name: "./admin/password", 62 | Mode: 00600, 63 | Uname: "admin", 64 | }, 65 | Content: []byte("swordf1sh"), 66 | }, 67 | tar.Header{ 68 | Typeflag: tar.TypeReg, 69 | Name: "./admin/password", 70 | Size: 9, 71 | Mode: 00600, 72 | Uname: "admin", 73 | Gname: "root", 74 | ModTime: epochStartTime, 75 | Format: tar.FormatGNU, 76 | }, 77 | }, { 78 | testutil.TarEntry{ 79 | Header: tar.Header{ 80 | Name: "./admin/setpassword", 81 | Mode: 04711, 82 | Uname: "admin", 83 | }, 84 | Content: []byte{0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01}, 85 | }, 86 | tar.Header{ 87 | Typeflag: tar.TypeReg, 88 | Name: "./admin/setpassword", 89 | Size: 7, 90 | Mode: 04711, 91 | Uname: "admin", 92 | Gname: "root", 93 | ModTime: epochStartTime, 94 | Format: tar.FormatGNU, 95 | }, 96 | }, { 97 | testutil.TarEntry{ 98 | Header: tar.Header{ 99 | Name: "./data/", 100 | }, 101 | }, 102 | tar.Header{ 103 | Typeflag: tar.TypeDir, 104 | Name: "./data/", 105 | Mode: 00755, 106 | Uname: "root", 107 | Gname: "root", 108 | ModTime: epochStartTime, 109 | Format: tar.FormatGNU, 110 | }, 111 | }, { 112 | testutil.TarEntry{ 113 | Header: tar.Header{ 114 | Name: "./data/invoice.txt", 115 | }, 116 | Content: []byte("$ 10"), 117 | }, 118 | tar.Header{ 119 | Typeflag: tar.TypeReg, 120 | Name: "./data/invoice.txt", 121 | Size: 4, 122 | Mode: 00644, 123 | Uname: "root", 124 | Gname: "root", 125 | ModTime: epochStartTime, 126 | Format: tar.FormatGNU, 127 | }, 128 | }, { 129 | testutil.TarEntry{ 130 | Header: tar.Header{ 131 | Name: "./data/logs/", 132 | }, 133 | }, 134 | tar.Header{ 135 | Typeflag: tar.TypeDir, 136 | Name: "./data/logs/", 137 | Mode: 00755, 138 | Uname: "root", 139 | Gname: "root", 140 | ModTime: epochStartTime, 141 | Format: tar.FormatGNU, 142 | }, 143 | }, { 144 | testutil.TarEntry{ 145 | Header: tar.Header{ 146 | Name: "./data/logs/task.log", 147 | ModTime: time.Date(2022, 3, 1, 12, 0, 0, 0, time.Local), 148 | }, 149 | Content: []byte("starting\nfinished\n"), 150 | }, 151 | tar.Header{ 152 | Typeflag: tar.TypeReg, 153 | Name: "./data/logs/task.log", 154 | Size: 18, 155 | Mode: 00644, 156 | Uname: "root", 157 | Gname: "root", 158 | ModTime: time.Date(2022, 3, 1, 12, 0, 0, 0, time.Local), 159 | Format: tar.FormatGNU, 160 | }, 161 | }, { 162 | testutil.TarEntry{ 163 | Header: tar.Header{ 164 | Name: "./data/shared/", 165 | Mode: 02777, 166 | }, 167 | }, 168 | tar.Header{ 169 | Typeflag: tar.TypeDir, 170 | Name: "./data/shared/", 171 | Mode: 02777, 172 | Uname: "root", 173 | Gname: "root", 174 | ModTime: epochStartTime, 175 | Format: tar.FormatGNU, 176 | }, 177 | }, { 178 | testutil.TarEntry{ 179 | Header: tar.Header{ 180 | Name: "./home/", 181 | }, 182 | }, 183 | tar.Header{ 184 | Typeflag: tar.TypeDir, 185 | Name: "./home/", 186 | Mode: 00755, 187 | Uname: "root", 188 | Gname: "root", 189 | ModTime: epochStartTime, 190 | Format: tar.FormatGNU, 191 | }, 192 | }, { 193 | testutil.TarEntry{ 194 | Header: tar.Header{ 195 | Name: "./home/alice/", 196 | Uid: 1000, 197 | Gid: 1000, 198 | }, 199 | }, 200 | tar.Header{ 201 | Typeflag: tar.TypeDir, 202 | Name: "./home/alice/", 203 | Mode: 00755, 204 | Uid: 1000, 205 | Gid: 1000, 206 | ModTime: epochStartTime, 207 | Format: tar.FormatGNU, 208 | }, 209 | }, { 210 | testutil.TarEntry{ 211 | Header: tar.Header{ 212 | Name: "./home/alice/notes", 213 | Uid: 1000, 214 | }, 215 | Content: []byte("check the cat"), 216 | }, 217 | tar.Header{ 218 | Typeflag: tar.TypeReg, 219 | Name: "./home/alice/notes", 220 | Size: 13, 221 | Mode: 00644, 222 | Uid: 1000, 223 | Gname: "root", 224 | ModTime: epochStartTime, 225 | Format: tar.FormatGNU, 226 | }, 227 | }, { 228 | testutil.TarEntry{ 229 | Header: tar.Header{ 230 | Name: "./home/bob/", 231 | Uname: "bob", 232 | Uid: 1001, 233 | }, 234 | }, 235 | tar.Header{ 236 | Typeflag: tar.TypeDir, 237 | Name: "./home/bob/", 238 | Mode: 00755, 239 | Uid: 1001, 240 | Uname: "bob", 241 | Gname: "root", 242 | ModTime: epochStartTime, 243 | Format: tar.FormatGNU, 244 | }, 245 | }, { 246 | testutil.TarEntry{ 247 | Header: tar.Header{ 248 | Name: "./home/bob/task.sh", 249 | Mode: 00700, 250 | Uname: "bob", 251 | Uid: 1001, 252 | }, 253 | Content: []byte("#!/bin/sh\n"), 254 | }, 255 | tar.Header{ 256 | Typeflag: tar.TypeReg, 257 | Name: "./home/bob/task.sh", 258 | Size: 10, 259 | Mode: 00700, 260 | Uid: 1001, 261 | Uname: "bob", 262 | Gname: "root", 263 | ModTime: epochStartTime, 264 | Format: tar.FormatGNU, 265 | }, 266 | }, { 267 | testutil.TarEntry{ 268 | Header: tar.Header{ 269 | Name: "./logs/", 270 | Linkname: "data/logs", 271 | }, 272 | }, 273 | tar.Header{ 274 | Typeflag: tar.TypeSymlink, 275 | Name: "./logs/", 276 | Linkname: "data/logs", 277 | Mode: 00777, 278 | Uname: "root", 279 | Gname: "root", 280 | ModTime: epochStartTime, 281 | Format: tar.FormatGNU, 282 | }, 283 | }, { 284 | testutil.TarEntry{ 285 | Header: tar.Header{ 286 | Typeflag: tar.TypeFifo, 287 | Name: "./pipe", 288 | }, 289 | }, 290 | tar.Header{ 291 | Typeflag: tar.TypeFifo, 292 | Name: "./pipe", 293 | Mode: 00644, 294 | Uname: "root", 295 | Gname: "root", 296 | ModTime: epochStartTime, 297 | Format: tar.FormatGNU, 298 | }, 299 | }, { 300 | testutil.TarEntry{ 301 | Header: tar.Header{ 302 | Typeflag: tar.TypeReg, 303 | Name: "./restricted.txt", 304 | Size: 3, 305 | ModTime: epochStartTime, 306 | Format: tar.FormatGNU, 307 | }, 308 | Content: []byte("123"), 309 | NoFixup: true, 310 | }, 311 | tar.Header{ 312 | Typeflag: tar.TypeReg, 313 | Name: "./restricted.txt", 314 | Size: 3, 315 | ModTime: epochStartTime, 316 | Format: tar.FormatGNU, 317 | }, 318 | }} 319 | 320 | func (s *pkgdataSuite) TestMakeDeb(c *C) { 321 | var size int64 322 | var err error 323 | 324 | inputEntries := make([]testutil.TarEntry, len(pkgdataCheckEntries)) 325 | for i, checkEntry := range pkgdataCheckEntries { 326 | inputEntries[i] = checkEntry.tarEntry 327 | } 328 | debBytes, err := testutil.MakeDeb(inputEntries) 329 | c.Assert(err, IsNil) 330 | 331 | debBuf := bytes.NewBuffer(debBytes) 332 | arReader := ar.NewReader(debBuf) 333 | 334 | arHeader, err := arReader.Next() 335 | c.Assert(err, IsNil) 336 | c.Assert(arHeader.Name, Equals, "data.tar.zst") 337 | c.Assert(arHeader.Mode, Equals, int64(0644)) 338 | c.Assert(int(arHeader.Size), testutil.IntGreaterThan, 0) 339 | 340 | var tarZstdBuf bytes.Buffer 341 | size, err = io.Copy(&tarZstdBuf, arReader) 342 | c.Assert(err, IsNil) 343 | c.Assert(int(size), testutil.IntGreaterThan, 0) 344 | 345 | var tarBuf bytes.Buffer 346 | zstdReader, err := zstd.NewReader(&tarZstdBuf) 347 | size, err = zstdReader.WriteTo(&tarBuf) 348 | c.Assert(err, IsNil) 349 | c.Assert(int(size), testutil.IntGreaterThan, 0) 350 | 351 | tarReader := tar.NewReader(&tarBuf) 352 | 353 | for _, checkEntry := range pkgdataCheckEntries { 354 | tarHeader, err := tarReader.Next() 355 | c.Assert(err, IsNil) 356 | c.Assert(*tarHeader, DeepEquals, checkEntry.checkHeader) 357 | var dataBuf bytes.Buffer 358 | size, err = io.Copy(&dataBuf, tarReader) 359 | c.Assert(err, IsNil) 360 | if checkEntry.tarEntry.Content != nil { 361 | c.Assert(dataBuf.Bytes(), DeepEquals, checkEntry.tarEntry.Content) 362 | } else { 363 | c.Assert(int(size), Equals, 0) 364 | } 365 | } 366 | 367 | _, err = tarReader.Next() 368 | c.Assert(err, Equals, io.EOF) 369 | 370 | _, err = arReader.Next() 371 | c.Assert(err, Equals, io.EOF) 372 | } 373 | -------------------------------------------------------------------------------- /internal/testutil/containschecker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2020 Canonical Ltd 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3 as 5 | // published by the Free Software Foundation. 6 | // 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | // 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | package testutil_test 16 | 17 | import ( 18 | "runtime" 19 | 20 | "gopkg.in/check.v1" 21 | 22 | . "github.com/canonical/chisel/internal/testutil" 23 | ) 24 | 25 | type containsCheckerSuite struct{} 26 | 27 | var _ = check.Suite(&containsCheckerSuite{}) 28 | 29 | func (*containsCheckerSuite) TestUnsupportedTypes(c *check.C) { 30 | testInfo(c, Contains, "Contains", []string{"container", "elem"}) 31 | testCheck(c, Contains, false, "int is not a supported container", 5, nil) 32 | testCheck(c, Contains, false, "bool is not a supported container", false, nil) 33 | testCheck(c, Contains, false, "element is a int but expected a string", "container", 1) 34 | } 35 | 36 | func (*containsCheckerSuite) TestContainsVerifiesTypes(c *check.C) { 37 | testInfo(c, Contains, "Contains", []string{"container", "elem"}) 38 | testCheck(c, Contains, 39 | false, "container has items of type int but expected element is a string", 40 | [...]int{1, 2, 3}, "foo") 41 | testCheck(c, Contains, 42 | false, "container has items of type int but expected element is a string", 43 | []int{1, 2, 3}, "foo") 44 | // This looks tricky, Contains looks at _values_, not at keys 45 | testCheck(c, Contains, 46 | false, "container has items of type int but expected element is a string", 47 | map[string]int{"foo": 1, "bar": 2}, "foo") 48 | testCheck(c, Contains, 49 | false, "container has items of type int but expected element is a string", 50 | map[string]int{"foo": 1, "bar": 2}, "foo") 51 | } 52 | 53 | type animal interface { 54 | Sound() string 55 | } 56 | 57 | type dog struct{} 58 | 59 | func (d *dog) Sound() string { 60 | return "bark" 61 | } 62 | 63 | type cat struct{} 64 | 65 | func (c *cat) Sound() string { 66 | return "meow" 67 | } 68 | 69 | type tree struct{} 70 | 71 | func (*containsCheckerSuite) TestContainsVerifiesInterfaceTypes(c *check.C) { 72 | testCheck(c, Contains, 73 | false, "container has items of interface type testutil_test.animal but expected element does not implement it", 74 | [...]animal{&dog{}, &cat{}}, &tree{}) 75 | testCheck(c, Contains, 76 | false, "container has items of interface type testutil_test.animal but expected element does not implement it", 77 | []animal{&dog{}, &cat{}}, &tree{}) 78 | testCheck(c, Contains, 79 | false, "container has items of interface type testutil_test.animal but expected element does not implement it", 80 | map[string]animal{"dog": &dog{}, "cat": &cat{}}, &tree{}) 81 | } 82 | 83 | func (*containsCheckerSuite) TestContainsString(c *check.C) { 84 | c.Assert("foo", Contains, "f") 85 | c.Assert("foo", Contains, "fo") 86 | c.Assert("foo", check.Not(Contains), "foobar") 87 | } 88 | 89 | type myString string 90 | 91 | func (*containsCheckerSuite) TestContainsCustomString(c *check.C) { 92 | c.Assert(myString("foo"), Contains, myString("f")) 93 | c.Assert(myString("foo"), Contains, myString("fo")) 94 | c.Assert(myString("foo"), check.Not(Contains), myString("foobar")) 95 | c.Assert("foo", Contains, myString("f")) 96 | c.Assert("foo", Contains, myString("fo")) 97 | c.Assert("foo", check.Not(Contains), myString("foobar")) 98 | c.Assert(myString("foo"), Contains, "f") 99 | c.Assert(myString("foo"), Contains, "fo") 100 | c.Assert(myString("foo"), check.Not(Contains), "foobar") 101 | } 102 | 103 | func (*containsCheckerSuite) TestContainsArray(c *check.C) { 104 | c.Assert([...]int{1, 2, 3}, Contains, 1) 105 | c.Assert([...]int{1, 2, 3}, Contains, 2) 106 | c.Assert([...]int{1, 2, 3}, Contains, 3) 107 | c.Assert([...]int{1, 2, 3}, check.Not(Contains), 4) 108 | c.Assert([...]animal{&dog{}, &cat{}}, Contains, &dog{}) 109 | c.Assert([...]animal{&cat{}}, check.Not(Contains), &dog{}) 110 | } 111 | 112 | func (*containsCheckerSuite) TestContainsSlice(c *check.C) { 113 | c.Assert([]int{1, 2, 3}, Contains, 1) 114 | c.Assert([]int{1, 2, 3}, Contains, 2) 115 | c.Assert([]int{1, 2, 3}, Contains, 3) 116 | c.Assert([]int{1, 2, 3}, check.Not(Contains), 4) 117 | c.Assert([]animal{&dog{}, &cat{}}, Contains, &dog{}) 118 | c.Assert([]animal{&cat{}}, check.Not(Contains), &dog{}) 119 | } 120 | 121 | func (*containsCheckerSuite) TestContainsMap(c *check.C) { 122 | c.Assert(map[string]int{"foo": 1, "bar": 2}, Contains, 1) 123 | c.Assert(map[string]int{"foo": 1, "bar": 2}, Contains, 2) 124 | c.Assert(map[string]int{"foo": 1, "bar": 2}, check.Not(Contains), 3) 125 | c.Assert(map[string]animal{"dog": &dog{}, "cat": &cat{}}, Contains, &dog{}) 126 | c.Assert(map[string]animal{"cat": &cat{}}, check.Not(Contains), &dog{}) 127 | } 128 | 129 | // Arbitrary type that is not comparable 130 | type myStruct struct { 131 | attrs map[string]string 132 | } 133 | 134 | func (*containsCheckerSuite) TestContainsUncomparableType(c *check.C) { 135 | if runtime.Compiler != "gc" { 136 | c.Skip("this test only works on go (not gccgo)") 137 | } 138 | 139 | elem := myStruct{map[string]string{"k": "v"}} 140 | containerArray := [...]myStruct{elem} 141 | containerSlice := []myStruct{elem} 142 | containerMap := map[string]myStruct{"foo": elem} 143 | errMsg := "runtime error: comparing uncomparable type testutil_test.myStruct" 144 | testInfo(c, Contains, "Contains", []string{"container", "elem"}) 145 | testCheck(c, Contains, false, errMsg, containerArray, elem) 146 | testCheck(c, Contains, false, errMsg, containerSlice, elem) 147 | testCheck(c, Contains, false, errMsg, containerMap, elem) 148 | } 149 | 150 | func (*containsCheckerSuite) TestDeepContainsUnsupportedTypes(c *check.C) { 151 | testInfo(c, DeepContains, "DeepContains", []string{"container", "elem"}) 152 | testCheck(c, DeepContains, false, "int is not a supported container", 5, nil) 153 | testCheck(c, DeepContains, false, "bool is not a supported container", false, nil) 154 | testCheck(c, DeepContains, false, "element is a int but expected a string", "container", 1) 155 | } 156 | 157 | func (*containsCheckerSuite) TestDeepContainsVerifiesTypes(c *check.C) { 158 | testInfo(c, DeepContains, "DeepContains", []string{"container", "elem"}) 159 | testCheck(c, DeepContains, 160 | false, "container has items of type int but expected element is a string", 161 | [...]int{1, 2, 3}, "foo") 162 | testCheck(c, DeepContains, 163 | false, "container has items of type int but expected element is a string", 164 | []int{1, 2, 3}, "foo") 165 | // This looks tricky, DeepContains looks at _values_, not at keys 166 | testCheck(c, DeepContains, 167 | false, "container has items of type int but expected element is a string", 168 | map[string]int{"foo": 1, "bar": 2}, "foo") 169 | } 170 | 171 | func (*containsCheckerSuite) TestDeepContainsString(c *check.C) { 172 | c.Assert("foo", DeepContains, "f") 173 | c.Assert("foo", DeepContains, "fo") 174 | c.Assert("foo", check.Not(DeepContains), "foobar") 175 | } 176 | 177 | func (*containsCheckerSuite) TestDeepContainsCustomString(c *check.C) { 178 | c.Assert(myString("foo"), DeepContains, myString("f")) 179 | c.Assert(myString("foo"), DeepContains, myString("fo")) 180 | c.Assert(myString("foo"), check.Not(DeepContains), myString("foobar")) 181 | c.Assert("foo", DeepContains, myString("f")) 182 | c.Assert("foo", DeepContains, myString("fo")) 183 | c.Assert("foo", check.Not(DeepContains), myString("foobar")) 184 | c.Assert(myString("foo"), DeepContains, "f") 185 | c.Assert(myString("foo"), DeepContains, "fo") 186 | c.Assert(myString("foo"), check.Not(DeepContains), "foobar") 187 | } 188 | 189 | func (*containsCheckerSuite) TestDeepContainsArray(c *check.C) { 190 | c.Assert([...]int{1, 2, 3}, DeepContains, 1) 191 | c.Assert([...]int{1, 2, 3}, DeepContains, 2) 192 | c.Assert([...]int{1, 2, 3}, DeepContains, 3) 193 | c.Assert([...]int{1, 2, 3}, check.Not(DeepContains), 4) 194 | } 195 | 196 | func (*containsCheckerSuite) TestDeepContainsSlice(c *check.C) { 197 | c.Assert([]int{1, 2, 3}, DeepContains, 1) 198 | c.Assert([]int{1, 2, 3}, DeepContains, 2) 199 | c.Assert([]int{1, 2, 3}, DeepContains, 3) 200 | c.Assert([]int{1, 2, 3}, check.Not(DeepContains), 4) 201 | } 202 | 203 | func (*containsCheckerSuite) TestDeepContainsMap(c *check.C) { 204 | c.Assert(map[string]int{"foo": 1, "bar": 2}, DeepContains, 1) 205 | c.Assert(map[string]int{"foo": 1, "bar": 2}, DeepContains, 2) 206 | c.Assert(map[string]int{"foo": 1, "bar": 2}, check.Not(DeepContains), 3) 207 | } 208 | 209 | func (*containsCheckerSuite) TestDeepContainsUncomparableType(c *check.C) { 210 | elem := myStruct{map[string]string{"k": "v"}} 211 | containerArray := [...]myStruct{elem} 212 | containerSlice := []myStruct{elem} 213 | containerMap := map[string]myStruct{"foo": elem} 214 | testInfo(c, DeepContains, "DeepContains", []string{"container", "elem"}) 215 | testCheck(c, DeepContains, true, "", containerArray, elem) 216 | testCheck(c, DeepContains, true, "", containerSlice, elem) 217 | testCheck(c, DeepContains, true, "", containerMap, elem) 218 | } 219 | -------------------------------------------------------------------------------- /internal/deb/extract_test.go: -------------------------------------------------------------------------------- 1 | package deb_test 2 | 3 | import ( 4 | "bytes" 5 | 6 | . "gopkg.in/check.v1" 7 | 8 | "github.com/canonical/chisel/internal/deb" 9 | "github.com/canonical/chisel/internal/testutil" 10 | ) 11 | 12 | type extractTest struct { 13 | summary string 14 | pkgdata []byte 15 | options deb.ExtractOptions 16 | globbed map[string][]string 17 | result map[string]string 18 | error string 19 | } 20 | 21 | var extractTests = []extractTest{{ 22 | summary: "Extract nothing", 23 | pkgdata: testutil.PackageData["base-files"], 24 | options: deb.ExtractOptions{ 25 | Extract: nil, 26 | }, 27 | result: map[string]string{}, 28 | }, { 29 | summary: "Extract a few entries", 30 | pkgdata: testutil.PackageData["base-files"], 31 | options: deb.ExtractOptions{ 32 | Extract: map[string][]deb.ExtractInfo{ 33 | "/usr/bin/hello": []deb.ExtractInfo{{ 34 | Path: "/usr/bin/hello", 35 | }}, 36 | "/etc/os-release": []deb.ExtractInfo{{ 37 | Path: "/etc/os-release", 38 | }}, 39 | "/usr/lib/os-release": []deb.ExtractInfo{{ 40 | Path: "/usr/lib/os-release", 41 | }}, 42 | "/usr/share/doc/": []deb.ExtractInfo{{ 43 | Path: "/usr/share/doc/", 44 | }}, 45 | "/tmp/": []deb.ExtractInfo{{ 46 | Path: "/tmp/", 47 | }}, 48 | }, 49 | }, 50 | result: map[string]string{ 51 | "/tmp/": "dir 01777", 52 | "/usr/": "dir 0755", 53 | "/usr/bin/": "dir 0755", 54 | "/usr/bin/hello": "file 0775 eaf29575", 55 | "/usr/share/": "dir 0755", 56 | "/usr/share/doc/": "dir 0755", 57 | "/usr/lib/": "dir 0755", 58 | "/usr/lib/os-release": "file 0644 ec6fae43", 59 | "/etc/": "dir 0755", 60 | "/etc/os-release": "symlink ../usr/lib/os-release", 61 | }, 62 | }, { 63 | 64 | summary: "Copy a couple of entries elsewhere", 65 | pkgdata: testutil.PackageData["base-files"], 66 | options: deb.ExtractOptions{ 67 | Extract: map[string][]deb.ExtractInfo{ 68 | "/usr/bin/hello": []deb.ExtractInfo{{ 69 | Path: "/usr/foo/bin/hello-2", 70 | Mode: 0600, 71 | }}, 72 | "/usr/share/": []deb.ExtractInfo{{ 73 | Path: "/usr/other/", 74 | Mode: 0700, 75 | }}, 76 | }, 77 | }, 78 | result: map[string]string{ 79 | "/usr/": "dir 0755", 80 | "/usr/foo/": "dir 0755", 81 | "/usr/foo/bin/": "dir 0755", 82 | "/usr/foo/bin/hello-2": "file 0600 eaf29575", 83 | "/usr/other/": "dir 0700", 84 | }, 85 | }, { 86 | 87 | summary: "Copy same file twice", 88 | pkgdata: testutil.PackageData["base-files"], 89 | options: deb.ExtractOptions{ 90 | Extract: map[string][]deb.ExtractInfo{ 91 | "/usr/bin/hello": []deb.ExtractInfo{{ 92 | Path: "/usr/bin/hello", 93 | }, { 94 | Path: "/usr/bin/hallo", 95 | }}, 96 | }, 97 | }, 98 | result: map[string]string{ 99 | "/usr/": "dir 0755", 100 | "/usr/bin/": "dir 0755", 101 | "/usr/bin/hello": "file 0775 eaf29575", 102 | "/usr/bin/hallo": "file 0775 eaf29575", 103 | }, 104 | }, { 105 | summary: "Globbing a single dir level", 106 | pkgdata: testutil.PackageData["base-files"], 107 | options: deb.ExtractOptions{ 108 | Extract: map[string][]deb.ExtractInfo{ 109 | "/etc/d*/": []deb.ExtractInfo{{ 110 | Path: "/etc/d*/", 111 | }}, 112 | }, 113 | }, 114 | result: map[string]string{ 115 | "/etc/": "dir 0755", 116 | "/etc/dpkg/": "dir 0755", 117 | "/etc/default/": "dir 0755", 118 | }, 119 | }, { 120 | summary: "Globbing for files with multiple levels at once", 121 | pkgdata: testutil.PackageData["base-files"], 122 | options: deb.ExtractOptions{ 123 | Extract: map[string][]deb.ExtractInfo{ 124 | "/etc/d**": []deb.ExtractInfo{{ 125 | Path: "/etc/d**", 126 | }}, 127 | }, 128 | }, 129 | result: map[string]string{ 130 | "/etc/": "dir 0755", 131 | "/etc/dpkg/": "dir 0755", 132 | "/etc/dpkg/origins/": "dir 0755", 133 | "/etc/dpkg/origins/debian": "file 0644 50f35af8", 134 | "/etc/dpkg/origins/ubuntu": "file 0644 d2537b95", 135 | "/etc/default/": "dir 0755", 136 | "/etc/debian_version": "file 0644 cce26cfe", 137 | }, 138 | }, { 139 | summary: "Globbing with reporting of globbed paths", 140 | pkgdata: testutil.PackageData["base-files"], 141 | options: deb.ExtractOptions{ 142 | Extract: map[string][]deb.ExtractInfo{ 143 | "/etc/de**": []deb.ExtractInfo{{ 144 | Path: "/etc/de**", 145 | }}, 146 | "/etc/dp*/": []deb.ExtractInfo{{ 147 | Path: "/etc/dp*/", 148 | }}, 149 | }, 150 | }, 151 | result: map[string]string{ 152 | "/etc/": "dir 0755", 153 | "/etc/dpkg/": "dir 0755", 154 | "/etc/default/": "dir 0755", 155 | "/etc/debian_version": "file 0644 cce26cfe", 156 | }, 157 | globbed: map[string][]string{ 158 | "/etc/dp*/": []string{"/etc/dpkg/"}, 159 | "/etc/de**": []string{"/etc/debian_version", "/etc/default/"}, 160 | }, 161 | }, { 162 | summary: "Globbing must have matching source and target", 163 | pkgdata: testutil.PackageData["base-files"], 164 | options: deb.ExtractOptions{ 165 | Extract: map[string][]deb.ExtractInfo{ 166 | "/etc/d**": []deb.ExtractInfo{{ 167 | Path: "/etc/g**", 168 | }}, 169 | }, 170 | }, 171 | error: `cannot extract .*: when using wildcards source and target paths must match: /etc/d\*\*`, 172 | }, { 173 | summary: "Globbing must also have a single target", 174 | pkgdata: testutil.PackageData["base-files"], 175 | options: deb.ExtractOptions{ 176 | Extract: map[string][]deb.ExtractInfo{ 177 | "/etc/d**": []deb.ExtractInfo{{ 178 | Path: "/etc/d**", 179 | }, { 180 | Path: "/etc/d**", 181 | }}, 182 | }, 183 | }, 184 | error: `cannot extract .*: when using wildcards source and target paths must match: /etc/d\*\*`, 185 | }, { 186 | summary: "Globbing cannot change modes", 187 | pkgdata: testutil.PackageData["base-files"], 188 | options: deb.ExtractOptions{ 189 | Extract: map[string][]deb.ExtractInfo{ 190 | "/etc/d**": []deb.ExtractInfo{{ 191 | Path: "/etc/d**", 192 | Mode: 0777, 193 | }}, 194 | }, 195 | }, 196 | error: `cannot extract .*: when using wildcards source and target paths must match: /etc/d\*\*`, 197 | }, { 198 | summary: "Missing file", 199 | pkgdata: testutil.PackageData["base-files"], 200 | options: deb.ExtractOptions{ 201 | Extract: map[string][]deb.ExtractInfo{ 202 | "/etc/passwd": []deb.ExtractInfo{{ 203 | Path: "/etc/passwd", 204 | }}, 205 | }, 206 | }, 207 | error: `cannot extract from package "base-files": no content at /etc/passwd`, 208 | }, { 209 | summary: "Missing directory", 210 | pkgdata: testutil.PackageData["base-files"], 211 | options: deb.ExtractOptions{ 212 | Extract: map[string][]deb.ExtractInfo{ 213 | "/etd/": []deb.ExtractInfo{{ 214 | Path: "/etd/", 215 | }}, 216 | }, 217 | }, 218 | error: `cannot extract from package "base-files": no content at /etd/`, 219 | }, { 220 | summary: "Missing glob", 221 | pkgdata: testutil.PackageData["base-files"], 222 | options: deb.ExtractOptions{ 223 | Extract: map[string][]deb.ExtractInfo{ 224 | "/etd/**": []deb.ExtractInfo{{ 225 | Path: "/etd/**", 226 | }}, 227 | }, 228 | }, 229 | error: `cannot extract from package "base-files": no content at /etd/\*\*`, 230 | }, { 231 | summary: "Missing multiple entries", 232 | pkgdata: testutil.PackageData["base-files"], 233 | options: deb.ExtractOptions{ 234 | Extract: map[string][]deb.ExtractInfo{ 235 | "/etc/passwd": []deb.ExtractInfo{{ 236 | Path: "/etc/passwd", 237 | }}, 238 | "/etd/": []deb.ExtractInfo{{ 239 | Path: "/etd/", 240 | }}, 241 | }, 242 | }, 243 | error: `cannot extract from package "base-files": no content at:\n- /etc/passwd\n- /etd/`, 244 | }, { 245 | summary: "Optional entries may be missing", 246 | pkgdata: testutil.PackageData["base-files"], 247 | options: deb.ExtractOptions{ 248 | Extract: map[string][]deb.ExtractInfo{ 249 | "/etc/": []deb.ExtractInfo{{ 250 | Path: "/etc/", 251 | }}, 252 | "/usr/foo/hallo": []deb.ExtractInfo{{ 253 | Path: "/usr/bin/foo/hallo", 254 | Optional: true, 255 | }}, 256 | "/other/path/": []deb.ExtractInfo{{ 257 | Path: "/tmp/new/path/", 258 | Optional: true, 259 | }}, 260 | }, 261 | }, 262 | result: map[string]string{ 263 | "/etc/": "dir 0755", 264 | "/usr/": "dir 0755", 265 | "/usr/bin/": "dir 0755", 266 | "/tmp/": "dir 01777", 267 | }, 268 | }, { 269 | summary: "Optional entries mixed in cannot be missing", 270 | pkgdata: testutil.PackageData["base-files"], 271 | options: deb.ExtractOptions{ 272 | Extract: map[string][]deb.ExtractInfo{ 273 | "/usr/bin/hallo": []deb.ExtractInfo{{ 274 | Path: "/usr/bin/hallo", 275 | Optional: true, 276 | }, { 277 | Path: "/usr/bin/hallow", 278 | Optional: false, 279 | }}, 280 | }, 281 | }, 282 | error: `cannot extract from package "base-files": no content at /usr/bin/hallo`, 283 | }} 284 | 285 | func (s *S) TestExtract(c *C) { 286 | 287 | for _, test := range extractTests { 288 | c.Logf("Test: %s", test.summary) 289 | dir := c.MkDir() 290 | options := test.options 291 | options.Package = "base-files" 292 | options.TargetDir = dir 293 | 294 | if test.globbed != nil { 295 | options.Globbed = make(map[string][]string) 296 | } 297 | 298 | err := deb.Extract(bytes.NewBuffer(test.pkgdata), &options) 299 | if test.error != "" { 300 | c.Assert(err, ErrorMatches, test.error) 301 | continue 302 | } else { 303 | c.Assert(err, IsNil) 304 | } 305 | 306 | if test.globbed != nil { 307 | c.Assert(options.Globbed, DeepEquals, test.globbed) 308 | } 309 | 310 | result := testutil.TreeDump(dir) 311 | c.Assert(result, DeepEquals, test.result) 312 | } 313 | } 314 | --------------------------------------------------------------------------------