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