snapd-2.37.4~14.04.1/ 0000775 0000000 0000000 00000000000 13435556260 010541 5 ustar snapd-2.37.4~14.04.1/i18n/ 0000775 0000000 0000000 00000000000 13435556260 011320 5 ustar snapd-2.37.4~14.04.1/i18n/i18n.go 0000664 0000000 0000000 00000005615 13435556260 012435 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package i18n
//go:generate update-pot
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/ojii/gettext.go"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
)
// TEXTDOMAIN is the message domain used by snappy; see dgettext(3)
// for more information.
var (
TEXTDOMAIN = "snappy"
locale gettext.Catalog
translations gettext.Translations
)
func init() {
bindTextDomain(TEXTDOMAIN, "/usr/share/locale")
setLocale("")
}
func langpackResolver(baseRoot string, locale string, domain string) string {
// first check for the real locale (e.g. de_DE)
// then try to simplify the locale (e.g. de_DE -> de)
locales := []string{locale, strings.SplitN(locale, "_", 2)[0]}
for _, locale := range locales {
r := filepath.Join(locale, "LC_MESSAGES", fmt.Sprintf("%s.mo", domain))
// look into the core snaps first for translations,
// then the main system
candidateDirs := []string{
filepath.Join(dirs.SnapMountDir, "/core/current/", baseRoot),
baseRoot,
}
for _, root := range candidateDirs {
// ubuntu uses /usr/lib/locale-langpack and patches the glibc gettext
// implementation
langpack := filepath.Join(root, "..", "locale-langpack", r)
if osutil.FileExists(langpack) {
return langpack
}
regular := filepath.Join(root, r)
if osutil.FileExists(regular) {
return regular
}
}
}
return ""
}
func bindTextDomain(domain, dir string) {
translations = gettext.NewTranslations(dir, domain, langpackResolver)
}
func setLocale(loc string) {
if loc == "" {
loc = os.Getenv("LC_MESSAGES")
if loc == "" {
loc = os.Getenv("LANG")
}
}
// de_DE.UTF-8, de_DE@euro all need to get simplified
loc = strings.Split(loc, "@")[0]
loc = strings.Split(loc, ".")[0]
locale = translations.Locale(loc)
}
// G is the shorthand for Gettext
func G(msgid string) string {
return locale.Gettext(msgid)
}
// https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html
// (search for 1000)
func ngn(d int) uint32 {
const max = 1000000
if d < 0 {
d = -d
}
if d > max {
return uint32((d % max) + max)
}
return uint32(d)
}
// NG is the shorthand for NGettext
func NG(msgid string, msgidPlural string, n int) string {
return locale.NGettext(msgid, msgidPlural, ngn(n))
}
snapd-2.37.4~14.04.1/i18n/xgettext-go/ 0000775 0000000 0000000 00000000000 13435556260 013577 5 ustar snapd-2.37.4~14.04.1/i18n/xgettext-go/main.go 0000664 0000000 0000000 00000021235 13435556260 015055 0 ustar package main
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io"
"io/ioutil"
"log"
"os"
"sort"
"strings"
"time"
"github.com/jessevdk/go-flags"
)
type msgID struct {
msgidPlural string
comment string
fname string
line int
formatHint string
}
var msgIDs map[string][]msgID
func formatComment(com string) string {
out := ""
for _, rawline := range strings.Split(com, "\n") {
line := rawline
line = strings.TrimPrefix(line, "//")
line = strings.TrimPrefix(line, "/*")
line = strings.TrimSuffix(line, "*/")
line = strings.TrimSpace(line)
if line != "" {
out += fmt.Sprintf("#. %s\n", line)
}
}
return out
}
func findCommentsForTranslation(fset *token.FileSet, f *ast.File, posCall token.Position) string {
com := ""
for _, cg := range f.Comments {
// search for all comments in the previous line
for i := len(cg.List) - 1; i >= 0; i-- {
c := cg.List[i]
posComment := fset.Position(c.End())
//println(posCall.Line, posComment.Line, c.Text)
if posCall.Line == posComment.Line+1 {
posCall = posComment
com = fmt.Sprintf("%s\n%s", c.Text, com)
}
}
}
// only return if we have a matching prefix
formatedComment := formatComment(com)
needle := fmt.Sprintf("#. %s", opts.AddCommentsTag)
if !strings.HasPrefix(formatedComment, needle) {
formatedComment = ""
}
return formatedComment
}
func constructValue(val interface{}) string {
switch val.(type) {
case *ast.BasicLit:
return val.(*ast.BasicLit).Value
// this happens for constructs like:
// gettext.Gettext("foo" + "bar")
case *ast.BinaryExpr:
// we only support string concat
if val.(*ast.BinaryExpr).Op != token.ADD {
return ""
}
left := constructValue(val.(*ast.BinaryExpr).X)
// strip right " (or `)
left = left[0 : len(left)-1]
right := constructValue(val.(*ast.BinaryExpr).Y)
// strip left " (or `)
right = right[1:]
return left + right
default:
panic(fmt.Sprintf("unknown type: %v", val))
}
}
func inspectNodeForTranslations(fset *token.FileSet, f *ast.File, n ast.Node) bool {
// FIXME: this assume we always have a "gettext.Gettext" style keyword
l := strings.Split(opts.Keyword, ".")
gettextSelector := l[0]
gettextFuncName := l[1]
l = strings.Split(opts.KeywordPlural, ".")
gettextSelectorPlural := l[0]
gettextFuncNamePlural := l[1]
switch x := n.(type) {
case *ast.CallExpr:
if sel, ok := x.Fun.(*ast.SelectorExpr); ok {
i18nStr := ""
i18nStrPlural := ""
if sel.Sel.Name == gettextFuncNamePlural && sel.X.(*ast.Ident).Name == gettextSelectorPlural {
i18nStr = x.Args[0].(*ast.BasicLit).Value
i18nStrPlural = x.Args[1].(*ast.BasicLit).Value
}
if sel.Sel.Name == gettextFuncName && sel.X.(*ast.Ident).Name == gettextSelector {
i18nStr = constructValue(x.Args[0])
}
formatI18nStr := func(s string) string {
if s == "" {
return ""
}
// the "`" is special
if s[0] == '`' {
// replace inner " with \"
s = strings.Replace(s, "\"", "\\\"", -1)
// replace \n with \\n
s = strings.Replace(s, "\n", "\\n", -1)
}
// strip leading and trailing " (or `)
s = s[1 : len(s)-1]
return s
}
// FIXME: too simplistic(?), no %% is considered
formatHint := ""
if strings.Contains(i18nStr, "%") || strings.Contains(i18nStrPlural, "%") {
// well, not quite correct but close enough
formatHint = "c-format"
}
if i18nStr != "" {
msgidStr := formatI18nStr(i18nStr)
posCall := fset.Position(n.Pos())
msgIDs[msgidStr] = append(msgIDs[msgidStr], msgID{
formatHint: formatHint,
msgidPlural: formatI18nStr(i18nStrPlural),
fname: posCall.Filename,
line: posCall.Line,
comment: findCommentsForTranslation(fset, f, posCall),
})
}
}
}
return true
}
func processFiles(args []string) error {
// go over the input files
msgIDs = make(map[string][]msgID)
fset := token.NewFileSet()
for _, fname := range args {
if err := processSingleGoSource(fset, fname); err != nil {
return err
}
}
return nil
}
func processSingleGoSource(fset *token.FileSet, fname string) error {
fnameContent, err := ioutil.ReadFile(fname)
if err != nil {
return err
}
// Create the AST by parsing src.
f, err := parser.ParseFile(fset, fname, fnameContent, parser.ParseComments)
if err != nil {
return err
}
ast.Inspect(f, func(n ast.Node) bool {
return inspectNodeForTranslations(fset, f, n)
})
return nil
}
var formatTime = func() string {
return time.Now().Format("2006-01-02 15:04-0700")
}
// mustFprintf will write the given format string to the given
// writer. Any error will make it panic.
func mustFprintf(w io.Writer, format string, a ...interface{}) {
_, err := fmt.Fprintf(w, format, a...)
if err != nil {
panic(fmt.Sprintf("cannot write output: %v", err))
}
}
func writePotFile(out io.Writer) {
header := fmt.Sprintf(`# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR , YEAR.
#
#, fuzzy
msgid ""
msgstr "Project-Id-Version: %s\n"
"Report-Msgid-Bugs-To: %s\n"
"POT-Creation-Date: %s\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
`, opts.PackageName, opts.MsgIDBugsAddress, formatTime())
mustFprintf(out, "%s", header)
// yes, this is the way to do it in go
sortedKeys := []string{}
for k := range msgIDs {
sortedKeys = append(sortedKeys, k)
}
if opts.SortOutput {
sort.Strings(sortedKeys)
}
// FIXME: use template here?
for _, k := range sortedKeys {
msgidList := msgIDs[k]
for _, msgid := range msgidList {
if opts.AddComments || opts.AddCommentsTag != "" {
mustFprintf(out, "%s", msgid.comment)
}
}
if !opts.NoLocation {
mustFprintf(out, "#:")
for _, msgid := range msgidList {
mustFprintf(out, " %s:%d", msgid.fname, msgid.line)
}
mustFprintf(out, "\n")
}
msgid := msgidList[0]
if msgid.formatHint != "" {
mustFprintf(out, "#, %s\n", msgid.formatHint)
}
var formatOutput = func(in string) string {
// split string with \n into multiple lines
// to make the output nicer
out := strings.Replace(in, "\\n", "\\n\"\n \"", -1)
// cleanup too aggressive splitting (empty "" lines)
return strings.TrimSuffix(out, "\"\n \"")
}
mustFprintf(out, "msgid \"%v\"\n", formatOutput(k))
if msgid.msgidPlural != "" {
mustFprintf(out, "msgid_plural \"%v\"\n", formatOutput(msgid.msgidPlural))
mustFprintf(out, "msgstr[0] \"\"\n")
mustFprintf(out, "msgstr[1] \"\"\n")
} else {
mustFprintf(out, "msgstr \"\"\n")
}
mustFprintf(out, "\n")
}
}
// FIXME: this must be setable via go-flags
var opts struct {
FilesFrom string `short:"f" long:"files-from" description:"get list of input files from FILE"`
Output string `short:"o" long:"output" description:"output to specified file"`
AddComments bool `short:"c" long:"add-comments" description:"place all comment blocks preceding keyword lines in output file"`
AddCommentsTag string `long:"add-comments-tag" description:"place comment blocks starting with TAG and prceding keyword lines in output file"`
SortOutput bool `short:"s" long:"sort-output" description:"generate sorted output"`
NoLocation bool `long:"no-location" description:"do not write '#: filename:line' lines"`
MsgIDBugsAddress string `long:"msgid-bugs-address" default:"EMAIL" description:"set report address for msgid bugs"`
PackageName string `long:"package-name" description:"set package name in output"`
Keyword string `short:"k" long:"keyword" default:"gettext.Gettext" description:"look for WORD as the keyword for singular strings"`
KeywordPlural string `long:"keyword-plural" default:"gettext.NGettext" description:"look for WORD as the keyword for plural strings"`
}
func main() {
// parse args
args, err := flags.ParseArgs(&opts, os.Args)
if err != nil {
log.Fatalf("ParseArgs failed %s", err)
}
var files []string
if opts.FilesFrom != "" {
content, err := ioutil.ReadFile(opts.FilesFrom)
if err != nil {
log.Fatalf("cannot read file %v: %v", opts.FilesFrom, err)
}
content = bytes.TrimSpace(content)
files = strings.Split(string(content), "\n")
} else {
files = args[1:]
}
if err := processFiles(files); err != nil {
log.Fatalf("processFiles failed with: %s", err)
}
out := os.Stdout
if opts.Output != "" {
var err error
out, err = os.Create(opts.Output)
if err != nil {
log.Fatalf("failed to create %s: %s", opts.Output, err)
}
}
writePotFile(out)
}
snapd-2.37.4~14.04.1/i18n/xgettext-go/main_test.go 0000664 0000000 0000000 00000023030 13435556260 016107 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/testutil"
)
// Hook up check.v1 into the "go test" runner
func TestT(t *testing.T) { TestingT(t) }
type xgettextTestSuite struct {
}
var _ = Suite(&xgettextTestSuite{})
// test helper
func makeGoSourceFile(c *C, content []byte) string {
fname := filepath.Join(c.MkDir(), "foo.go")
err := ioutil.WriteFile(fname, []byte(content), 0644)
c.Assert(err, IsNil)
return fname
}
func (s *xgettextTestSuite) SetUpTest(c *C) {
// our test defaults
opts.NoLocation = false
opts.AddCommentsTag = "TRANSLATORS:"
opts.Keyword = "i18n.G"
opts.KeywordPlural = "i18n.NG"
opts.SortOutput = true
opts.PackageName = "snappy"
opts.MsgIDBugsAddress = "snappy-devel@lists.ubuntu.com"
// mock time
formatTime = func() string {
return "2015-06-30 14:48+0200"
}
}
func (s *xgettextTestSuite) TestFormatComment(c *C) {
var tests = []struct {
in string
out string
}{
{in: "// foo ", out: "#. foo\n"},
{in: "/* foo */", out: "#. foo\n"},
{in: "/* foo\n */", out: "#. foo\n"},
{in: "/* foo\nbar */", out: "#. foo\n#. bar\n"},
}
for _, test := range tests {
c.Assert(formatComment(test.in), Equals, test.out)
}
}
func (s *xgettextTestSuite) TestProcessFilesSimple(c *C) {
fname := makeGoSourceFile(c, []byte(`package main
func main() {
// TRANSLATORS: foo comment
i18n.G("foo")
}
`))
err := processFiles([]string{fname})
c.Assert(err, IsNil)
c.Assert(msgIDs, DeepEquals, map[string][]msgID{
"foo": {
{
comment: "#. TRANSLATORS: foo comment\n",
fname: fname,
line: 5,
},
},
})
}
func (s *xgettextTestSuite) TestProcessFilesMultiple(c *C) {
fname := makeGoSourceFile(c, []byte(`package main
func main() {
// TRANSLATORS: foo comment
i18n.G("foo")
// TRANSLATORS: bar comment
i18n.G("foo")
}
`))
err := processFiles([]string{fname})
c.Assert(err, IsNil)
c.Assert(msgIDs, DeepEquals, map[string][]msgID{
"foo": {
{
comment: "#. TRANSLATORS: foo comment\n",
fname: fname,
line: 5,
},
{
comment: "#. TRANSLATORS: bar comment\n",
fname: fname,
line: 8,
},
},
})
}
const header = `# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR , YEAR.
#
#, fuzzy
msgid ""
msgstr "Project-Id-Version: snappy\n"
"Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n"
"POT-Creation-Date: 2015-06-30 14:48+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
`
func (s *xgettextTestSuite) TestWriteOutputSimple(c *C) {
msgIDs = map[string][]msgID{
"foo": {
{
fname: "fname",
line: 2,
comment: "#. foo\n",
},
},
}
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#. foo
#: fname:2
msgid "foo"
msgstr ""
`, header)
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputMultiple(c *C) {
msgIDs = map[string][]msgID{
"foo": {
{
fname: "fname",
line: 2,
comment: "#. comment1\n",
},
{
fname: "fname",
line: 4,
comment: "#. comment2\n",
},
},
}
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#. comment1
#. comment2
#: fname:2 fname:4
msgid "foo"
msgstr ""
`, header)
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputNoComment(c *C) {
msgIDs = map[string][]msgID{
"foo": {
{
fname: "fname",
line: 2,
},
},
}
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#: fname:2
msgid "foo"
msgstr ""
`, header)
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputNoLocation(c *C) {
msgIDs = map[string][]msgID{
"foo": {
{
fname: "fname",
line: 2,
},
},
}
opts.NoLocation = true
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
msgid "foo"
msgstr ""
`, header)
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputFormatHint(c *C) {
msgIDs = map[string][]msgID{
"foo": {
{
fname: "fname",
line: 2,
formatHint: "c-format",
},
},
}
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#: fname:2
#, c-format
msgid "foo"
msgstr ""
`, header)
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputPlural(c *C) {
msgIDs = map[string][]msgID{
"foo": {
{
msgidPlural: "plural",
fname: "fname",
line: 2,
},
},
}
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#: fname:2
msgid "foo"
msgid_plural "plural"
msgstr[0] ""
msgstr[1] ""
`, header)
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputSorted(c *C) {
msgIDs = map[string][]msgID{
"aaa": {
{
fname: "fname",
line: 2,
},
},
"zzz": {
{
fname: "fname",
line: 2,
},
},
}
opts.SortOutput = true
// we need to run this a bunch of times as the ordering might
// be right by pure chance
for i := 0; i < 10; i++ {
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#: fname:2
msgid "aaa"
msgstr ""
#: fname:2
msgid "zzz"
msgstr ""
`, header)
c.Assert(out.String(), Equals, expected)
}
}
func (s *xgettextTestSuite) TestIntegration(c *C) {
fname := makeGoSourceFile(c, []byte(`package main
func main() {
// TRANSLATORS: foo comment
// with multiple lines
i18n.G("foo")
// this comment has no translators tag
i18n.G("abc")
// TRANSLATORS: plural
i18n.NG("singular", "plural", 99)
i18n.G("zz %s")
}
`))
// a real integration test :)
outName := filepath.Join(c.MkDir(), "snappy.pot")
os.Args = []string{"test-binary",
"--output", outName,
"--keyword", "i18n.G",
"--keyword-plural", "i18n.NG",
"--msgid-bugs-address", "snappy-devel@lists.ubuntu.com",
"--package-name", "snappy",
fname,
}
main()
// verify its what we expect
c.Assert(outName, testutil.FileEquals, fmt.Sprintf(`%s
#: %[2]s:9
msgid "abc"
msgstr ""
#. TRANSLATORS: foo comment
#. with multiple lines
#: %[2]s:6
msgid "foo"
msgstr ""
#. TRANSLATORS: plural
#: %[2]s:12
msgid "singular"
msgid_plural "plural"
msgstr[0] ""
msgstr[1] ""
#: %[2]s:14
#, c-format
msgid "zz %%s"
msgstr ""
`, header, fname))
}
func (s *xgettextTestSuite) TestProcessFilesConcat(c *C) {
fname := makeGoSourceFile(c, []byte(`package main
func main() {
// TRANSLATORS: foo comment
i18n.G("foo\n" + "bar\n" + "baz")
}
`))
err := processFiles([]string{fname})
c.Assert(err, IsNil)
c.Assert(msgIDs, DeepEquals, map[string][]msgID{
"foo\\nbar\\nbaz": {
{
comment: "#. TRANSLATORS: foo comment\n",
fname: fname,
line: 5,
},
},
})
}
func (s *xgettextTestSuite) TestProcessFilesWithQuote(c *C) {
fname := makeGoSourceFile(c, []byte(fmt.Sprintf(`package main
func main() {
i18n.G(%[1]s foo "bar"%[1]s)
}
`, "`")))
err := processFiles([]string{fname})
c.Assert(err, IsNil)
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#: %[2]s:4
msgid " foo \"bar\""
msgstr ""
`, header, fname)
c.Check(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputMultilines(c *C) {
msgIDs = map[string][]msgID{
"foo\\nbar\\nbaz": {
{
fname: "fname",
line: 2,
comment: "#. foo\n",
},
},
}
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#. foo
#: fname:2
msgid "foo\n"
"bar\n"
"baz"
msgstr ""
`, header)
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputTidy(c *C) {
msgIDs = map[string][]msgID{
"foo\\nbar\\nbaz": {
{
fname: "fname",
line: 2,
},
},
"zzz\\n": {
{
fname: "fname",
line: 4,
},
},
}
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#: fname:2
msgid "foo\n"
"bar\n"
"baz"
msgstr ""
#: fname:4
msgid "zzz\n"
msgstr ""
`, header)
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestProcessFilesWithDoubleQuote(c *C) {
fname := makeGoSourceFile(c, []byte(`package main
func main() {
i18n.G("foo \"bar\"")
}
`))
err := processFiles([]string{fname})
c.Assert(err, IsNil)
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#: %[2]s:4
msgid "foo \"bar\""
msgstr ""
`, header, fname)
c.Check(out.String(), Equals, expected)
}
snapd-2.37.4~14.04.1/i18n/i18n_test.go 0000664 0000000 0000000 00000010602 13435556260 013464 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package i18n
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"testing"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/dirs"
)
// Hook up check.v1 into the "go test" runner
func Test(t *testing.T) { TestingT(t) }
var mockLocalePo = []byte(`
msgid ""
msgstr ""
"Project-Id-Version: snappy-test\n"
"Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n"
"POT-Creation-Date: 2015-06-16 09:08+0200\n"
"Language: en_DK\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;>\n"
msgid "plural_1"
msgid_plural "plural_2"
msgstr[0] "translated plural_1"
msgstr[1] "translated plural_2"
msgid "singular"
msgstr "translated singular"
`)
func makeMockTranslations(c *C, localeDir string) {
fullLocaleDir := filepath.Join(localeDir, "en_DK", "LC_MESSAGES")
err := os.MkdirAll(fullLocaleDir, 0755)
c.Assert(err, IsNil)
po := filepath.Join(fullLocaleDir, "snappy-test.po")
mo := filepath.Join(fullLocaleDir, "snappy-test.mo")
err = ioutil.WriteFile(po, mockLocalePo, 0644)
c.Assert(err, IsNil)
cmd := exec.Command("msgfmt", po, "--output-file", mo)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
c.Assert(err, IsNil)
}
type i18nTestSuite struct {
origLang string
origLcMessages string
}
var _ = Suite(&i18nTestSuite{})
func (s *i18nTestSuite) SetUpTest(c *C) {
// this dir contains a special hand-crafted en_DK/snappy-test.mo
// file
localeDir := c.MkDir()
makeMockTranslations(c, localeDir)
// we use a custom test mo file
TEXTDOMAIN = "snappy-test"
s.origLang = os.Getenv("LANG")
s.origLcMessages = os.Getenv("LC_MESSAGES")
bindTextDomain("snappy-test", localeDir)
os.Setenv("LANG", "en_DK.UTF-8")
setLocale("")
}
func (s *i18nTestSuite) TearDownTest(c *C) {
os.Setenv("LANG", s.origLang)
os.Setenv("LC_MESSAGES", s.origLcMessages)
}
func (s *i18nTestSuite) TestTranslatedSingular(c *C) {
// no G() to avoid adding the test string to snappy-pot
var Gtest = G
c.Assert(Gtest("singular"), Equals, "translated singular")
}
func (s *i18nTestSuite) TestTranslatesPlural(c *C) {
// no NG() to avoid adding the test string to snappy-pot
var NGtest = NG
c.Assert(NGtest("plural_1", "plural_2", 1), Equals, "translated plural_1")
}
func (s *i18nTestSuite) TestTranslatedMissingLangNoCrash(c *C) {
setLocale("invalid")
// no G() to avoid adding the test string to snappy-pot
var Gtest = G
c.Assert(Gtest("singular"), Equals, "singular")
}
func (s *i18nTestSuite) TestInvalidTextDomainDir(c *C) {
bindTextDomain("snappy-test", "/random/not/existing/dir")
setLocale("invalid")
// no G() to avoid adding the test string to snappy-pot
var Gtest = G
c.Assert(Gtest("singular"), Equals, "singular")
}
func (s *i18nTestSuite) TestLangpackResolverFromLangpack(c *C) {
root := c.MkDir()
localeDir := filepath.Join(root, "/usr/share/locale")
err := os.MkdirAll(localeDir, 0755)
c.Assert(err, IsNil)
d := filepath.Join(root, "/usr/share/locale-langpack")
makeMockTranslations(c, d)
bindTextDomain("snappy-test", localeDir)
setLocale("")
// no G() to avoid adding the test string to snappy-pot
var Gtest = G
c.Assert(Gtest("singular"), Equals, "translated singular", Commentf("test with %q failed", d))
}
func (s *i18nTestSuite) TestLangpackResolverFromCore(c *C) {
origSnapMountDir := dirs.SnapMountDir
defer func() { dirs.SnapMountDir = origSnapMountDir }()
dirs.SnapMountDir = c.MkDir()
d := filepath.Join(dirs.SnapMountDir, "/core/current/usr/share/locale")
makeMockTranslations(c, d)
bindTextDomain("snappy-test", "/usr/share/locale")
setLocale("")
// no G() to avoid adding the test string to snappy-pot
var Gtest = G
c.Assert(Gtest("singular"), Equals, "translated singular", Commentf("test with %q failed", d))
}
snapd-2.37.4~14.04.1/.travis.yml 0000664 0000000 0000000 00000010411 13435556260 012647 0 ustar language: go
git:
quiet: true
jobs:
include:
- stage: quick
name: go 1.6/xenial static and unit test suites
dist: xenial
go: "1.6"
before_install:
- sudo apt --quiet -o Dpkg::Progress-Fancy=false update
install:
- sudo apt --quiet -o Dpkg::Progress-Fancy=false build-dep snapd
- ./get-deps.sh
script:
- set -e
- ./run-checks --static
- ./run-checks --short-unit
- stage: quick
go: "1.10"
name: OSX build and minimal runtime sanity check
os: osx
addons:
homebrew:
packages: [squashfs]
install:
- ./get-deps.sh
# extra dependency on darwin:
- go get golang.org/x/sys/unix
before_script:
- ./mkversion.sh
- go build -o /tmp/snp ./cmd/snap
script:
- /tmp/snp download hello
- /tmp/snp version
- /tmp/snp pack tests/lib/snaps/test-snapd-tools/ /tmp
- stage: quick
name: CLA check
if: type = pull_request
language: bash
addons:
apt:
packages:
python-launchpadlib
script:
- ./tests/lib/cla_check.py
- stage: integration
name: spread
os: linux
addons:
apt:
packages:
- xdelta3
install:
# override the default install for language:go
- true
script:
- ./run-checks --spread
env:
global:
# SPREAD_LINODE_KEY
- secure: "bzALrfNSLwM0bjceal1PU5rFErvqVhi00Sygx8jruo6htpZay3hrC2sHCKCQKPn1kvCfHidrHX1vnomg5N+B9o25GZEYSjKSGxuvdNDfCZYqPNjMbz5y7xXYfKWgyo+xtrKRM85Nqy121SfRz3KLDvrOLwwreb+pZv8DG1WraFTd7D6rK7nLnnYNUyw665XBMFVnM8ue3Zu9496Ih/TfQXhnNpsZY8xFWte4+cH7JvVCVTs8snjoGVZi3972PzinNkfBgJa24cUzxFMfiN/AwSBXJQKdVv+FsbB4uRgXAqTNwuus7PptiPNxpWWojuhm1Qgbk0XhGIdJxyUYkmNA4UrZ3C29nIRWbuAiHJ6ZWd1ur3dqphqOcgFInltSHkpfEdlL3YK4dCa2SmJESzotUGnyowCUUCXkWdDaZmFTwyK0Y6He9oyXDK5f+/U7SFlPvok0caJCvB9HbTQR1kYdh048I/R+Ht5QrFOZPk21DYWDOYhn7SzthBDZLsaL6n5gX7Y547SsL4B35YVbpaeHzccG6Mox8rI4bqlGFvP1U5i8uXD4uQjJChlVxpmozUEMok9T5RVediJs540p5uc8DQl48Nke02tXzC/XpGAvpnXT7eiiRNW67zOj2QcIV+ni3lBj3HvZeB9cgjzLNrZSl/t9vseqnNwQWpl3V6nd/bU="
# SPREAD_STORE_USER
- secure: "LjqfvJ2xz/7cxt1Cywaw5l8gaj5jOhUsf502UeaH+rOnj+9tCdWTtyP8U4nOjjQwiJ0xuygba+FgdnXEyxV+THeXHOF69SRF/1N8JIc3i9G6JK/CqDfFTRMqiRaCf5u7KuOrYZ0ssYNBXyZ8X4Ahls3uFu2DgEuAim1J6wOVSgIoUkduLVrbsn6uB9G5Uuc+C4NMA3TH21IJ6ct35t3T+/EjvoGUHcKtoOsPXdBZvz96xw5mKGIBaLpZdy5WxmhPUsz3MIlZgvi4DR3YIa/9u+QoGNU05f8upJRhwdwkuu9vJwqekXNXDJi/ZGlpkkAPx0feJbyhtz68551Pn1TtmA3TS5JtuMeMZWxCL9SudA7/C3oBRNGnKI3LwvP20pPjdlEYMOCq/oHlxoJylGVdpynZXTtaFS+s4Qhnr+WuNcG3zFa9bJvXPyy1vxPKcjI2DojneTrCTW/L6zg7tBIVQGzTxmC7QWsbTvOQzu+YICyeeS3g+iJ+QyP6+/oTyER3a3vmZCtXqsBJTznesS0SL5AkK+8moBGct96S6kT55XCDVgThWV0OGH6l4LwVSOjPioNzXNhVLZ8GKkXrMZXKSaWAeYptzWl4Gfz0Y4nFCu3aqIOyie7janPPgeEL0E2ZjndIs+ZigtN1LCol+GJN7fXzUFy8Fichqhhwvb3YLyE="
# SPREAD_STORE_PASSWORD
- secure: "Le4CMhklfadi4aBQIEaEMbsFIB608GOvSHjVUxkDxkkUAVwl/Ov4Dni5d0Tn4e/xcxPkcm+pPg64dn0Jxzwx6XfWlxhWC10vYh+/GjpZW1znahtb/Gf9CNZOJJEy5LSeI7/uJ3LYcFd0FU0EJSerNeQJc5d8jmJH8UnuqObHOk29YD//XILiLRa1XALEimwXeQyGQePBmDTxPQQv1VLFjgfaJa5Xy55Us7AKTML2V7lhaeKCSEIp3x9liLAtnKlJhyXaXO/e4b3ZJTgXwYh+vENK1E2pxalpjBNPaJNkvtbsjFtYNXJoXca+hBVs5Sq1PCBhkEGxqFUsD8VLQd+MEXp4MYOF5fBhxIa3qOSjtuR+WmZ9G6fEysEBV6Y3F3D6HYWTpNkcHNXJCwdtOM+n92zNEBDIrufwzTPpyJXpoxZCCXrk3HHRdyDktvJYLrHdn1bM19mgYguesMZHTC5xMD6ifwdRoylmApjImXOvVxf2HdQiNvNLDqvaHgmYwNfl0+KbaVz+O2EDPCRnT5wOCpSeSUet47EPITdjr5OnTwLpOVaY+iSvn90EUB/8+ZU01TRYgc+6VNPHokLVjuiQJSrE4yTx/c2MnY9eRaOosVXngYfoS/L3XwDwZiQoeLZs04bScvxzGQIGCJ+CBzNPENtZ4AUh55Yl/vVNReZJeaY="
# SPREAD_GOOGLE_KEY
- secure: "dIA2HrartowFL2Gl5jXiVMd9hIJyIeummYwxeBL9MzO48E/BIJyIGHudEOo8oCnZ5a0yb8TqYgND2FCgJU1V5I2LyxH6T9kizHjtmIGgeM4qlEGKRlptb2v7DFkaHeW4Mpp4gLk8hYIeWyq9OR+SlK6f0Jj049LLKfQoX6GzTPug5+MMEQOJs55OJ6f6gvCv2o3oj6WFybaohMCO4GbNYQSPLwheyTSkT0efnW9QqTN0w62pDMqscVURO90/CUeZyCcXw2uOBegwPNTBoo/+4+nZsfSNeupV8wX4vVYL0ZFL6IO3mViDoZBD4SGTNF/9x8Lc1WeKm9HlELzy5krdLqsvdV/fQSWhBzwkdykKVA3Aae5dAMIGRt7e5bJaUg+/HdtOgA5jr+qey/c/BN11MyaSOMNPNGjRuv9NAcEjxoN2JkiDXfpA3lE9kjd7TBTexGe4RJGJLJjT9s8XxdKufBfruC/yhVGdVkRoc2tsAJPZ72Ds9qH0FH28zNFAgAitCLDfInjhPMPvZJhb3Bqx5P/0DE5zUbduE9kYK0iiZRJ4AaytQy+R4nJCXE42mWv5cxoE84opVqO9cBu1TPCC8gTRQFWpJt1rP+DvwjaFiswvptG8obxNpHmkhcItPGmRVN9P9Yjd9nHvegS83tsbrd2KOyMmCk3/1KWhLufisHE="
snapd-2.37.4~14.04.1/spread-shellcheck 0000775 0000000 0000000 00000017574 13435556260 014066 0 ustar #!/usr/bin/env python3
# Copyright (C) 2018 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import logging
import os
import subprocess
import argparse
from concurrent.futures import ThreadPoolExecutor
from multiprocessing import cpu_count
import yaml
# default shell for shellcheck
SHELLCHECK_SHELL = os.getenv('SHELLCHECK_SHELL', 'bash')
# set to non-empty to ignore all errors
NO_FAIL = os.getenv('NO_FAIL')
# set to non empty to enable 'set -x'
D = os.getenv('D')
# set to non-empty to enable verbose logging
V = os.getenv('V')
# set to a number to use these many threads
N = int(os.getenv('N') or cpu_count())
# file with list of files that can fail validation
CAN_FAIL = os.getenv('CAN_FAIL')
# names of sections
SECTIONS = ['prepare', 'prepare-each', 'restore', 'restore-each',
'debug', 'debug-each', 'execute', 'repack']
def parse_arguments():
parser = argparse.ArgumentParser(description='spread shellcheck helper')
parser.add_argument('-s', '--shell', default='bash',
help='shell')
parser.add_argument('-n', '--no-errors', action='store_true',
default=False, help='ignore all errors ')
parser.add_argument('-v', '--verbose', action='store_true',
default=False, help='verbose logging')
parser.add_argument('--can-fail', default=None,
help=('file with list of files that are can fail '
'validation'))
parser.add_argument('-P', '--max-procs', default=N, type=int, metavar='N',
help='run these many shellchecks in parallel (default: %(default)s)')
parser.add_argument('paths', nargs='+', help='paths to check')
return parser.parse_args()
class ShellcheckRunError(Exception):
def __init__(self, stderr):
super().__init__()
self.stderr = stderr
class ShellcheckError(Exception):
def __init__(self, path):
super().__init__()
self.sectionerrors = {}
self.path = path
def addfailure(self, section, error):
self.sectionerrors[section] = error
def __len__(self):
return len(self.sectionerrors)
class ShellcheckFailures(Exception):
def __init__(self, failures=None):
super().__init__()
self.failures = set()
if failures:
self.failures = set(failures)
def merge(self, otherfailures):
self.failures = self.failures.union(otherfailures.failures)
def __len__(self):
return len(self.failures)
def intersection(self, other):
return self.failures.intersection(other)
def difference(self, other):
return self.failures.difference(other)
def __iter__(self):
return iter(self.failures)
def checksection(data):
# spread shell snippets are executed under 'set -e' shell, make sure
# shellcheck knows about that
data = 'set -e\n' + data
proc = subprocess.Popen("shellcheck -s {} -x -".format(SHELLCHECK_SHELL),
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=True)
stdout, _ = proc.communicate(input=data.encode('utf-8'), timeout=10)
if proc.returncode != 0:
raise ShellcheckRunError(stdout)
def checkfile(path):
logging.debug("checking file %s", path)
with open(path) as inf:
data = yaml.load(inf)
errors = ShellcheckError(path)
for section in SECTIONS:
if section not in data:
continue
try:
logging.debug("%s: checking section %s", path, section)
checksection(data[section])
except ShellcheckRunError as serr:
errors.addfailure(section, serr.stderr.decode('utf-8'))
if path.endswith('spread.yaml') and 'suites' in data:
# check suites
for suite in data['suites'].keys():
for section in SECTIONS:
if section not in data['suites'][suite]:
continue
try:
logging.debug("%s (suite %s): checking section %s", path, suite, section)
checksection(data['suites'][suite][section])
except ShellcheckRunError as serr:
errors.addfailure('suites/' + suite + '/' + section,
serr.stderr.decode('utf-8'))
if errors:
raise errors
def findfiles(indir):
for root, _, files in os.walk(indir, topdown=True):
for name in files:
if name in ['spread.yaml', 'task.yaml']:
yield os.path.join(root, name)
def checkpath(loc, max_workers):
if os.path.isdir(loc):
# setup iterator
locations = findfiles(loc)
else:
locations = [loc]
failed = []
def check1path(path):
try:
checkfile(path)
except ShellcheckError as err:
return err
return None
with ThreadPoolExecutor(max_workers=max_workers) as executor:
for serr in executor.map(check1path, locations):
if serr is None:
continue
logging.error(('shellcheck failed for file %s in sections: '
'%s; error log follows'),
serr.path, ', '.join(serr.sectionerrors.keys()))
for section, error in serr.sectionerrors.items():
logging.error("%s: section '%s':\n%s", serr.path, section, error)
failed.append(serr.path)
if failed:
raise ShellcheckFailures(failures=failed)
def loadfilelist(flistpath):
flist = set()
with open(flistpath) as inf:
for line in inf:
if not line.startswith('#'):
flist.add(line.strip())
return flist
def main(opts):
paths = opts.paths or ['.']
failures = ShellcheckFailures()
for pth in paths:
try:
checkpath(pth, opts.max_procs)
except ShellcheckFailures as sf:
failures.merge(sf)
if failures:
if opts.can_fail:
can_fail = loadfilelist(opts.can_fail)
unexpected = failures.difference(can_fail)
if unexpected:
logging.error(('validation failed for the following '
'non-whitelisted files:\n%s'),
'\n'.join([' - ' + f for f in
sorted(unexpected)]))
raise SystemExit(1)
did_not_fail = can_fail - failures.intersection(can_fail)
if did_not_fail:
logging.error(('the following files are whitelisted '
'but validated successfully:\n%s'),
'\n'.join([' - ' + f for f in
sorted(did_not_fail)]))
raise SystemExit(1)
# no unexpected failures
return
logging.error('validation failed for the following files:\n%s',
'\n'.join([' - ' + f for f in sorted(failures)]))
if NO_FAIL or opts.no_errors:
logging.warning("ignoring errors")
else:
raise SystemExit(1)
if __name__ == '__main__':
opts = parse_arguments()
if opts.verbose or D or V:
lvl = logging.DEBUG
else:
lvl = logging.INFO
logging.basicConfig(level=lvl)
if CAN_FAIL:
opts.can_fail = CAN_FAIL
if NO_FAIL:
opts.no_errors = True
main(opts)
snapd-2.37.4~14.04.1/snap/ 0000775 0000000 0000000 00000000000 13435556260 011502 5 ustar snapd-2.37.4~14.04.1/snap/info_snap_yaml.go 0000664 0000000 0000000 00000043510 13435556260 015032 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"fmt"
"os"
"sort"
"strconv"
"strings"
"gopkg.in/yaml.v2"
"github.com/snapcore/snapd/strutil"
"github.com/snapcore/snapd/timeout"
)
type snapYaml struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Type Type `yaml:"type"`
Architectures []string `yaml:"architectures,omitempty"`
Assumes []string `yaml:"assumes"`
Title string `yaml:"title"`
Description string `yaml:"description"`
Summary string `yaml:"summary"`
License string `yaml:"license,omitempty"`
LicenseAgreement string `yaml:"license-agreement,omitempty"`
LicenseVersion string `yaml:"license-version,omitempty"`
Epoch Epoch `yaml:"epoch,omitempty"`
Base string `yaml:"base,omitempty"`
Confinement ConfinementType `yaml:"confinement,omitempty"`
Environment strutil.OrderedMap `yaml:"environment,omitempty"`
Plugs map[string]interface{} `yaml:"plugs,omitempty"`
Slots map[string]interface{} `yaml:"slots,omitempty"`
Apps map[string]appYaml `yaml:"apps,omitempty"`
Hooks map[string]hookYaml `yaml:"hooks,omitempty"`
Layout map[string]layoutYaml `yaml:"layout,omitempty"`
// TypoLayouts is used to detect the use of the incorrect plural form of "layout"
TypoLayouts typoDetector `yaml:"layouts,omitempty"`
}
type typoDetector struct {
Hint string
}
func (td *typoDetector) UnmarshalYAML(func(interface{}) error) error {
return fmt.Errorf("typo detected: %s", td.Hint)
}
type appYaml struct {
Aliases []string `yaml:"aliases,omitempty"`
Command string `yaml:"command"`
CommandChain []string `yaml:"command-chain,omitempty"`
Daemon string `yaml:"daemon"`
StopCommand string `yaml:"stop-command,omitempty"`
ReloadCommand string `yaml:"reload-command,omitempty"`
PostStopCommand string `yaml:"post-stop-command,omitempty"`
StopTimeout timeout.Timeout `yaml:"stop-timeout,omitempty"`
WatchdogTimeout timeout.Timeout `yaml:"watchdog-timeout,omitempty"`
Completer string `yaml:"completer,omitempty"`
RefreshMode string `yaml:"refresh-mode,omitempty"`
StopMode StopModeType `yaml:"stop-mode,omitempty"`
RestartCond RestartCondition `yaml:"restart-condition,omitempty"`
RestartDelay timeout.Timeout `yaml:"restart-delay,omitempty"`
SlotNames []string `yaml:"slots,omitempty"`
PlugNames []string `yaml:"plugs,omitempty"`
BusName string `yaml:"bus-name,omitempty"`
CommonID string `yaml:"common-id,omitempty"`
Environment strutil.OrderedMap `yaml:"environment,omitempty"`
Sockets map[string]socketsYaml `yaml:"sockets,omitempty"`
After []string `yaml:"after,omitempty"`
Before []string `yaml:"before,omitempty"`
Timer string `yaml:"timer,omitempty"`
Autostart string `yaml:"autostart,omitempty"`
}
type hookYaml struct {
PlugNames []string `yaml:"plugs,omitempty"`
SlotNames []string `yaml:"slots,omitempty"`
Environment strutil.OrderedMap `yaml:"environment,omitempty"`
CommandChain []string `yaml:"command-chain,omitempty"`
}
type layoutYaml struct {
Bind string `yaml:"bind,omitempty"`
BindFile string `yaml:"bind-file,omitempty"`
Type string `yaml:"type,omitempty"`
User string `yaml:"user,omitempty"`
Group string `yaml:"group,omitempty"`
Mode string `yaml:"mode,omitempty"`
Symlink string `yaml:"symlink,omitempty"`
}
type socketsYaml struct {
ListenStream string `yaml:"listen-stream,omitempty"`
SocketMode os.FileMode `yaml:"socket-mode,omitempty"`
}
// InfoFromSnapYaml creates a new info based on the given snap.yaml data
func InfoFromSnapYaml(yamlData []byte) (*Info, error) {
var y snapYaml
// Customize hints for the typo detector.
y.TypoLayouts.Hint = `use singular "layout" instead of plural "layouts"`
err := yaml.Unmarshal(yamlData, &y)
if err != nil {
return nil, fmt.Errorf("cannot parse snap.yaml: %s", err)
}
snap := infoSkeletonFromSnapYaml(y)
// Collect top-level definitions of plugs and slots
if err := setPlugsFromSnapYaml(y, snap); err != nil {
return nil, err
}
if err := setSlotsFromSnapYaml(y, snap); err != nil {
return nil, err
}
// At this point snap.Plugs and snap.Slots only contain globally-declared
// plugs and slots, so copy them for later.
snap.toplevelPlugs = make([]*PlugInfo, 0, len(snap.Plugs))
snap.toplevelSlots = make([]*SlotInfo, 0, len(snap.Slots))
for _, plug := range snap.Plugs {
snap.toplevelPlugs = append(snap.toplevelPlugs, plug)
}
for _, slot := range snap.Slots {
snap.toplevelSlots = append(snap.toplevelSlots, slot)
}
// Collect all apps, their aliases and hooks
if err := setAppsFromSnapYaml(y, snap); err != nil {
return nil, err
}
setHooksFromSnapYaml(y, snap)
// Bind unbound plugs to all apps and hooks
bindUnboundPlugs(snap)
// Bind unbound slots to all apps and hooks
bindUnboundSlots(snap)
// Collect layout elements.
if y.Layout != nil {
snap.Layout = make(map[string]*Layout, len(y.Layout))
for path, l := range y.Layout {
var mode os.FileMode = 0755
if l.Mode != "" {
m, err := strconv.ParseUint(l.Mode, 8, 32)
if err != nil {
return nil, err
}
mode = os.FileMode(m)
}
user := "root"
if l.User != "" {
user = l.User
}
group := "root"
if l.Group != "" {
group = l.Group
}
snap.Layout[path] = &Layout{
Snap: snap, Path: path,
Bind: l.Bind, Type: l.Type, Symlink: l.Symlink, BindFile: l.BindFile,
User: user, Group: group, Mode: mode,
}
}
}
// Rename specific plugs on the core snap.
snap.renameClashingCorePlugs()
snap.BadInterfaces = make(map[string]string)
SanitizePlugsSlots(snap)
// FIXME: validation of the fields
return snap, nil
}
// infoSkeletonFromSnapYaml initializes an Info without apps, hook, plugs, or
// slots
func infoSkeletonFromSnapYaml(y snapYaml) *Info {
// Prepare defaults
architectures := []string{"all"}
if len(y.Architectures) != 0 {
architectures = y.Architectures
}
typ := TypeApp
if y.Type != "" {
typ = y.Type
}
// TODO: once we have epochs transition to the snapd type for real
if y.Name == "snapd" {
typ = TypeSnapd
}
if len(y.Epoch.Read) == 0 {
// normalize
y.Epoch.Read = []uint32{0}
y.Epoch.Write = []uint32{0}
}
confinement := StrictConfinement
if y.Confinement != "" {
confinement = y.Confinement
}
// Construct snap skeleton without apps, hooks, plugs, or slots
snap := &Info{
SuggestedName: y.Name,
Version: y.Version,
Type: typ,
Architectures: architectures,
Assumes: y.Assumes,
OriginalTitle: y.Title,
OriginalDescription: y.Description,
OriginalSummary: y.Summary,
License: y.License,
LicenseAgreement: y.LicenseAgreement,
LicenseVersion: y.LicenseVersion,
Epoch: y.Epoch,
Confinement: confinement,
Base: y.Base,
Apps: make(map[string]*AppInfo),
LegacyAliases: make(map[string]*AppInfo),
Hooks: make(map[string]*HookInfo),
Plugs: make(map[string]*PlugInfo),
Slots: make(map[string]*SlotInfo),
Environment: y.Environment,
}
sort.Strings(snap.Assumes)
return snap
}
func setPlugsFromSnapYaml(y snapYaml, snap *Info) error {
for name, data := range y.Plugs {
iface, label, attrs, err := convertToSlotOrPlugData("plug", name, data)
if err != nil {
return err
}
snap.Plugs[name] = &PlugInfo{
Snap: snap,
Name: name,
Interface: iface,
Attrs: attrs,
Label: label,
}
if len(y.Apps) > 0 {
snap.Plugs[name].Apps = make(map[string]*AppInfo)
}
if len(y.Hooks) > 0 {
snap.Plugs[name].Hooks = make(map[string]*HookInfo)
}
}
return nil
}
func setSlotsFromSnapYaml(y snapYaml, snap *Info) error {
for name, data := range y.Slots {
iface, label, attrs, err := convertToSlotOrPlugData("slot", name, data)
if err != nil {
return err
}
snap.Slots[name] = &SlotInfo{
Snap: snap,
Name: name,
Interface: iface,
Attrs: attrs,
Label: label,
}
if len(y.Apps) > 0 {
snap.Slots[name].Apps = make(map[string]*AppInfo)
}
if len(y.Hooks) > 0 {
snap.Slots[name].Hooks = make(map[string]*HookInfo)
}
}
return nil
}
func setAppsFromSnapYaml(y snapYaml, snap *Info) error {
for appName, yApp := range y.Apps {
// Collect all apps
app := &AppInfo{
Snap: snap,
Name: appName,
LegacyAliases: yApp.Aliases,
Command: yApp.Command,
CommandChain: yApp.CommandChain,
Daemon: yApp.Daemon,
StopTimeout: yApp.StopTimeout,
StopCommand: yApp.StopCommand,
ReloadCommand: yApp.ReloadCommand,
PostStopCommand: yApp.PostStopCommand,
RestartCond: yApp.RestartCond,
RestartDelay: yApp.RestartDelay,
BusName: yApp.BusName,
CommonID: yApp.CommonID,
Environment: yApp.Environment,
Completer: yApp.Completer,
StopMode: yApp.StopMode,
RefreshMode: yApp.RefreshMode,
Before: yApp.Before,
After: yApp.After,
Autostart: yApp.Autostart,
WatchdogTimeout: yApp.WatchdogTimeout,
}
if len(y.Plugs) > 0 || len(yApp.PlugNames) > 0 {
app.Plugs = make(map[string]*PlugInfo)
}
if len(y.Slots) > 0 || len(yApp.SlotNames) > 0 {
app.Slots = make(map[string]*SlotInfo)
}
if len(yApp.Sockets) > 0 {
app.Sockets = make(map[string]*SocketInfo, len(yApp.Sockets))
}
snap.Apps[appName] = app
for _, alias := range app.LegacyAliases {
if snap.LegacyAliases[alias] != nil {
return fmt.Errorf("cannot set %q as alias for both %q and %q", alias, snap.LegacyAliases[alias].Name, appName)
}
snap.LegacyAliases[alias] = app
}
// Bind all plugs/slots listed in this app
for _, plugName := range yApp.PlugNames {
plug, ok := snap.Plugs[plugName]
if !ok {
// Create implicit plug definitions if required
plug = &PlugInfo{
Snap: snap,
Name: plugName,
Interface: plugName,
Apps: make(map[string]*AppInfo),
}
snap.Plugs[plugName] = plug
}
app.Plugs[plugName] = plug
plug.Apps[appName] = app
}
for _, slotName := range yApp.SlotNames {
slot, ok := snap.Slots[slotName]
if !ok {
slot = &SlotInfo{
Snap: snap,
Name: slotName,
Interface: slotName,
Apps: make(map[string]*AppInfo),
}
snap.Slots[slotName] = slot
}
app.Slots[slotName] = slot
slot.Apps[appName] = app
}
for name, data := range yApp.Sockets {
app.Sockets[name] = &SocketInfo{
App: app,
Name: name,
ListenStream: data.ListenStream,
SocketMode: data.SocketMode,
}
}
if yApp.Timer != "" {
app.Timer = &TimerInfo{
App: app,
Timer: yApp.Timer,
}
}
// collect all common IDs
if app.CommonID != "" {
snap.CommonIDs = append(snap.CommonIDs, app.CommonID)
}
}
return nil
}
func setHooksFromSnapYaml(y snapYaml, snap *Info) {
for hookName, yHook := range y.Hooks {
if !IsHookSupported(hookName) {
continue
}
// Collect all hooks
hook := &HookInfo{
Snap: snap,
Name: hookName,
Environment: yHook.Environment,
CommandChain: yHook.CommandChain,
Explicit: true,
}
if len(y.Plugs) > 0 || len(yHook.PlugNames) > 0 {
hook.Plugs = make(map[string]*PlugInfo)
}
if len(y.Slots) > 0 || len(yHook.SlotNames) > 0 {
hook.Slots = make(map[string]*SlotInfo)
}
snap.Hooks[hookName] = hook
// Bind all plugs/slots listed in this hook
for _, plugName := range yHook.PlugNames {
plug, ok := snap.Plugs[plugName]
if !ok {
// Create implicit plug definitions if required
plug = &PlugInfo{
Snap: snap,
Name: plugName,
Interface: plugName,
Hooks: make(map[string]*HookInfo),
}
snap.Plugs[plugName] = plug
} else if plug.Hooks == nil {
plug.Hooks = make(map[string]*HookInfo)
}
hook.Plugs[plugName] = plug
plug.Hooks[hookName] = hook
}
for _, slotName := range yHook.SlotNames {
slot, ok := snap.Slots[slotName]
if !ok {
// Create implicit slot definitions if required
slot = &SlotInfo{
Snap: snap,
Name: slotName,
Interface: slotName,
Hooks: make(map[string]*HookInfo),
}
snap.Slots[slotName] = slot
} else if slot.Hooks == nil {
slot.Hooks = make(map[string]*HookInfo)
}
hook.Slots[slotName] = slot
slot.Hooks[hookName] = hook
}
}
}
func bindUnboundPlugs(snap *Info) {
for plugName, plug := range snap.Plugs {
// A plug is considered unbound if it isn't being used by any apps
// or hooks. In which case we bind them to all apps and hooks.
if len(plug.Apps) == 0 && len(plug.Hooks) == 0 {
for appName, app := range snap.Apps {
app.Plugs[plugName] = plug
plug.Apps[appName] = app
}
for hookName, hook := range snap.Hooks {
hook.Plugs[plugName] = plug
plug.Hooks[hookName] = hook
}
}
}
}
func bindUnboundSlots(snap *Info) {
for slotName, slot := range snap.Slots {
// A slot is considered unbound if it isn't being used by any apps
// or hooks. In which case we bind them to all apps and hooks.
if len(slot.Apps) == 0 && len(slot.Hooks) == 0 {
for appName, app := range snap.Apps {
app.Slots[slotName] = slot
slot.Apps[appName] = app
}
for hookName, hook := range snap.Hooks {
hook.Slots[slotName] = slot
slot.Hooks[hookName] = hook
}
}
}
}
// bindImplicitHooks binds all global plugs and slots to implicit hooks
func bindImplicitHooks(snap *Info) {
for _, plug := range snap.toplevelPlugs {
for hookName, hook := range snap.Hooks {
if hook.Explicit {
continue
}
if hook.Plugs == nil {
hook.Plugs = make(map[string]*PlugInfo)
}
hook.Plugs[plug.Name] = plug
if plug.Hooks == nil {
plug.Hooks = make(map[string]*HookInfo)
}
plug.Hooks[hookName] = hook
}
}
for _, slot := range snap.toplevelSlots {
for hookName, hook := range snap.Hooks {
if hook.Explicit {
continue
}
if hook.Slots == nil {
hook.Slots = make(map[string]*SlotInfo)
}
hook.Slots[slot.Name] = slot
if slot.Hooks == nil {
slot.Hooks = make(map[string]*HookInfo)
}
slot.Hooks[hookName] = hook
}
}
}
func convertToSlotOrPlugData(plugOrSlot, name string, data interface{}) (iface, label string, attrs map[string]interface{}, err error) {
iface = name
switch data.(type) {
case string:
return data.(string), "", nil, nil
case nil:
return name, "", nil, nil
case map[interface{}]interface{}:
for keyData, valueData := range data.(map[interface{}]interface{}) {
key, ok := keyData.(string)
if !ok {
err := fmt.Errorf("%s %q has attribute that is not a string (found %T)",
plugOrSlot, name, keyData)
return "", "", nil, err
}
if strings.HasPrefix(key, "$") {
err := fmt.Errorf("%s %q uses reserved attribute %q", plugOrSlot, name, key)
return "", "", nil, err
}
switch key {
case "interface":
value, ok := valueData.(string)
if !ok {
err := fmt.Errorf("interface name on %s %q is not a string (found %T)",
plugOrSlot, name, valueData)
return "", "", nil, err
}
iface = value
case "label":
value, ok := valueData.(string)
if !ok {
err := fmt.Errorf("label of %s %q is not a string (found %T)",
plugOrSlot, name, valueData)
return "", "", nil, err
}
label = value
default:
if attrs == nil {
attrs = make(map[string]interface{})
}
value, err := normalizeYamlValue(valueData)
if err != nil {
return "", "", nil, fmt.Errorf("attribute %q of %s %q: %v", key, plugOrSlot, name, err)
}
attrs[key] = value
}
}
return iface, label, attrs, nil
default:
err := fmt.Errorf("%s %q has malformed definition (found %T)", plugOrSlot, name, data)
return "", "", nil, err
}
}
// normalizeYamlValue validates values and returns a normalized version of it (map[interface{}]interface{} is turned into map[string]interface{})
func normalizeYamlValue(v interface{}) (interface{}, error) {
switch x := v.(type) {
case string:
return x, nil
case bool:
return x, nil
case int:
return int64(x), nil
case int64:
return x, nil
case float64:
return x, nil
case float32:
return float64(x), nil
case []interface{}:
l := make([]interface{}, len(x))
for i, el := range x {
el, err := normalizeYamlValue(el)
if err != nil {
return nil, err
}
l[i] = el
}
return l, nil
case map[interface{}]interface{}:
m := make(map[string]interface{}, len(x))
for k, item := range x {
kStr, ok := k.(string)
if !ok {
return nil, fmt.Errorf("non-string key: %v", k)
}
item, err := normalizeYamlValue(item)
if err != nil {
return nil, err
}
m[kStr] = item
}
return m, nil
case map[string]interface{}:
m := make(map[string]interface{}, len(x))
for k, item := range x {
item, err := normalizeYamlValue(item)
if err != nil {
return nil, err
}
m[k] = item
}
return m, nil
default:
return nil, fmt.Errorf("invalid scalar: %v", v)
}
}
snapd-2.37.4~14.04.1/snap/implicit.go 0000664 0000000 0000000 00000004547 13435556260 013655 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016-2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"fmt"
"io/ioutil"
"github.com/snapcore/snapd/osutil"
)
// addImplicitHooks adds hooks from the installed snap's hookdir to the snap info.
//
// Existing hooks (i.e. ones defined in the YAML) are not changed; only missing
// hooks are added.
func addImplicitHooks(snapInfo *Info) error {
// First of all, check to ensure the hooks directory exists. If it doesn't,
// it's not an error-- there's just nothing to do.
hooksDir := snapInfo.HooksDir()
if !osutil.IsDirectory(hooksDir) {
return nil
}
fileInfos, err := ioutil.ReadDir(hooksDir)
if err != nil {
return fmt.Errorf("unable to read hooks directory: %s", err)
}
for _, fileInfo := range fileInfos {
addHookIfValid(snapInfo, fileInfo.Name())
}
return nil
}
// addImplicitHooksFromContainer adds hooks from the snap file's hookdir to the snap info.
//
// Existing hooks (i.e. ones defined in the YAML) are not changed; only missing
// hooks are added.
func addImplicitHooksFromContainer(snapInfo *Info, snapf Container) error {
// Read the hooks directory. If this fails we assume the hooks directory
// doesn't exist, which means there are no implicit hooks to load (not an
// error).
fileNames, err := snapf.ListDir("meta/hooks")
if err != nil {
return nil
}
for _, fileName := range fileNames {
addHookIfValid(snapInfo, fileName)
}
return nil
}
func addHookIfValid(snapInfo *Info, hookName string) {
// Verify that the hook name is actually supported. If not, ignore it.
if !IsHookSupported(hookName) {
return
}
// Don't overwrite a hook that has already been loaded from the YAML
if _, ok := snapInfo.Hooks[hookName]; !ok {
snapInfo.Hooks[hookName] = &HookInfo{Snap: snapInfo, Name: hookName}
}
}
snapd-2.37.4~14.04.1/snap/channel.go 0000664 0000000 0000000 00000010161 13435556260 013440 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"fmt"
"strings"
"github.com/snapcore/snapd/arch"
"github.com/snapcore/snapd/strutil"
)
var channelRisks = []string{"stable", "candidate", "beta", "edge"}
// Channel identifies and describes completely a store channel.
type Channel struct {
Architecture string `json:"architecture"`
Name string `json:"name"`
Track string `json:"track"`
Risk string `json:"risk"`
Branch string `json:"branch,omitempty"`
}
// ParseChannel parses a string representing a store channel and includes the given architecture, if architecture is "" the system architecture is included.
func ParseChannel(s string, architecture string) (Channel, error) {
if s == "" {
return Channel{}, fmt.Errorf("channel name cannot be empty")
}
p := strings.Split(s, "/")
var risk, track, branch string
switch len(p) {
default:
return Channel{}, fmt.Errorf("channel name has too many components: %s", s)
case 3:
track, risk, branch = p[0], p[1], p[2]
case 2:
if strutil.ListContains(channelRisks, p[0]) {
risk, branch = p[0], p[1]
} else {
track, risk = p[0], p[1]
}
case 1:
if strutil.ListContains(channelRisks, p[0]) {
risk = p[0]
} else {
track = p[0]
risk = "stable"
}
}
if !strutil.ListContains(channelRisks, risk) {
return Channel{}, fmt.Errorf("invalid risk in channel name: %s", s)
}
if architecture == "" {
architecture = arch.UbuntuArchitecture()
}
return Channel{
Architecture: architecture,
Track: track,
Risk: risk,
Branch: branch,
}.Clean(), nil
}
// Clean returns a Channel with a normalized track and name.
func (c Channel) Clean() Channel {
track := c.Track
if track == "latest" {
track = ""
}
// normalized name
name := c.Risk
if track != "" {
name = track + "/" + name
}
if c.Branch != "" {
name = name + "/" + c.Branch
}
return Channel{
Architecture: c.Architecture,
Name: name,
Track: track,
Risk: c.Risk,
Branch: c.Branch,
}
}
func (c Channel) String() string {
return c.Name
}
// Full returns the full name of the channel, inclusive the default track "latest".
func (c *Channel) Full() string {
if c.Track == "" {
return "latest/" + c.Name
}
return c.String()
}
func riskLevel(risk string) int {
for i, r := range channelRisks {
if r == risk {
return i
}
}
return -1
}
// ChannelMatch represents on which fields two channels are matching.
type ChannelMatch struct {
Architecture bool
Track bool
Risk bool
}
// String returns the string represantion of the match, results can be:
// "architecture:track:risk"
// "architecture:track"
// "architecture:risk"
// "track:risk"
// "architecture"
// "track"
// "risk"
// ""
func (cm ChannelMatch) String() string {
matching := []string{}
if cm.Architecture {
matching = append(matching, "architecture")
}
if cm.Track {
matching = append(matching, "track")
}
if cm.Risk {
matching = append(matching, "risk")
}
return strings.Join(matching, ":")
}
// Match returns a ChannelMatch of which fields among architecture,track,risk match between c and c1 store channels, risk is matched taking channel inheritance into account and considering c the requested channel.
func (c *Channel) Match(c1 *Channel) ChannelMatch {
requestedRiskLevel := riskLevel(c.Risk)
rl1 := riskLevel(c1.Risk)
return ChannelMatch{
Architecture: c.Architecture == c1.Architecture,
Track: c.Track == c1.Track,
Risk: requestedRiskLevel >= rl1,
}
}
snapd-2.37.4~14.04.1/snap/epoch_test.go 0000664 0000000 0000000 00000036622 13435556260 014177 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
"encoding/json"
"github.com/snapcore/snapd/snap"
"gopkg.in/check.v1"
"gopkg.in/yaml.v2"
)
type epochSuite struct{}
var _ = check.Suite(&epochSuite{})
var (
// some duplication here maybe
epochZeroStar = `0\* is an invalid epoch`
hugeEpochNumber = `epoch numbers must be less than 2³², but got .*`
badEpochNumber = `epoch numbers must be base 10 with no zero padding, but got .*`
badEpochList = "epoch read/write attributes must be lists of epoch numbers"
emptyEpochList = "epoch list cannot be explicitly empty"
epochListNotIncreasing = "epoch list must be a strictly increasing sequence"
epochListJustRidiculouslyLong = "epoch list must not have more than 10 entries"
noEpochIntersection = "epoch read and write lists must have a non-empty intersection"
)
func (s epochSuite) TestBadEpochs(c *check.C) {
type Tt struct {
s string
e string
y int
}
tests := []Tt{
{s: `"rubbish"`, e: badEpochNumber}, // SISO
{s: `0xA`, e: badEpochNumber, y: 1}, // no hex
{s: `"0xA"`, e: badEpochNumber}, //
{s: `001`, e: badEpochNumber, y: 1}, // no octal, in fact no zero prefixes at all
{s: `"001"`, e: badEpochNumber}, //
{s: `{"read": 5}`, e: badEpochList}, // when split, must be list
{s: `{"write": 5}`, e: badEpochList}, //
{s: `{"read": "5"}`, e: badEpochList}, //
{s: `{"write": "5"}`, e: badEpochList}, //
{s: `{"read": "1*"}`, e: badEpochList}, // what
{s: `{"read": [-1]}`, e: badEpochNumber}, // negative not allowed
{s: `{"write": [-1]}`, e: badEpochNumber}, //
{s: `{"read": ["-1"]}`, e: badEpochNumber}, //
{s: `{"write": ["-1"]}`, e: badEpochNumber}, //
{s: `{"read": ["yes"]}`, e: badEpochNumber}, // must be numbers
{s: `{"write": ["yes"]}`, e: badEpochNumber}, //
{s: `{"read": ["Ⅰ","Ⅱ"]}`, e: badEpochNumber}, // not roman numerals you idiot
{s: `{"read": [0xA]}`, e: badEpochNumber, y: 1}, //
{s: `{"read": [010]}`, e: badEpochNumber, y: 1}, //
{s: `{"read": [9999999999]}`, e: hugeEpochNumber}, // you done yet?
{s: `"0*"`, e: epochZeroStar}, // 0* means nothing
{s: `"42**"`, e: badEpochNumber}, // N** is dead
{s: `{"read": []}`, e: emptyEpochList}, // explicitly empty is bad
{s: `{"write": []}`, e: emptyEpochList}, //
{s: `{"read": [1,2,4,3]}`, e: epochListNotIncreasing}, // must be ordered
{s: `{"read": [1,2,2,3]}`, e: epochListNotIncreasing}, // must be strictly increasing
{s: `{"write": [4,3,2,1]}`, e: epochListNotIncreasing}, // ...*increasing*
{s: `{"read": [0], "write": [1]}`, e: noEpochIntersection}, // must have at least one in common
{s: `{"read": [0,1,2,3,4,5,6,7,8,9,10],
"write": [0,1,2,3,4,5,6,7,8,9,10]}`, e: epochListJustRidiculouslyLong}, // must have <10 elements
}
for _, test := range tests {
var v snap.Epoch
err := yaml.Unmarshal([]byte(test.s), &v)
c.Check(err, check.ErrorMatches, test.e, check.Commentf("YAML: %#q", test.s))
if test.y == 1 {
continue
}
err = json.Unmarshal([]byte(test.s), &v)
c.Check(err, check.ErrorMatches, test.e, check.Commentf("JSON: %#q", test.s))
}
}
func (s epochSuite) TestGoodEpochs(c *check.C) {
type Tt struct {
s string
e snap.Epoch
y int
}
tests := []Tt{
{s: `0`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}, y: 1},
{s: `""`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `"0"`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `{}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `"2*"`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}},
{s: `{"read": [2]}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}},
{s: `{"read": [1, 2]}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}},
{s: `{"write": [2]}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}},
{s: `{"write": [1, 2]}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{1, 2}}},
{s: `{"read": [2,4,8], "write": [2,3,5]}`, e: snap.Epoch{Read: []uint32{2, 4, 8}, Write: []uint32{2, 3, 5}}},
}
for _, test := range tests {
var v snap.Epoch
err := yaml.Unmarshal([]byte(test.s), &v)
c.Check(err, check.IsNil, check.Commentf("YAML: %s", test.s))
c.Check(v, check.DeepEquals, test.e)
if test.y > 0 {
continue
}
err = json.Unmarshal([]byte(test.s), &v)
c.Check(err, check.IsNil, check.Commentf("JSON: %s", test.s))
c.Check(v, check.DeepEquals, test.e)
}
}
func (s epochSuite) TestGoodEpochsInSnapYAML(c *check.C) {
defer snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})()
type Tt struct {
s string
e snap.Epoch
}
tests := []Tt{
{s: ``, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `epoch: null`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `epoch: 0`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `epoch: "0"`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `epoch: {}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `epoch: "2*"`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}},
{s: `epoch: {"read": [2]}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}},
{s: `epoch: {"read": [1, 2]}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}},
{s: `epoch: {"write": [2]}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}},
{s: `epoch: {"write": [1, 2]}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{1, 2}}},
{s: `epoch: {"read": [2,4,8], "write": [2,3,5]}`, e: snap.Epoch{Read: []uint32{2, 4, 8}, Write: []uint32{2, 3, 5}}},
}
for _, test := range tests {
info, err := snap.InfoFromSnapYaml([]byte(test.s))
c.Check(err, check.IsNil, check.Commentf("YAML: %s", test.s))
c.Check(info.Epoch, check.DeepEquals, test.e)
}
}
func (s epochSuite) TestGoodEpochsInJSON(c *check.C) {
type Tt struct {
s string
e snap.Epoch
}
type Tinfo struct {
Epoch snap.Epoch `json:"epoch"`
}
tests := []Tt{
// {} should give snap.Epoch{Read: []uint32{0}, Write: []uint32{0}} but needs an UnmarshalJSON on the parent
{s: `{"epoch": null}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `{"epoch": "0"}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `{"epoch": {}}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `{"epoch": "2*"}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}},
{s: `{"epoch": {"read": [0]}}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `{"epoch": {"write": [0]}}`, e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: `{"epoch": {"read": [2]}}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}},
{s: `{"epoch": {"read": [1, 2]}}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{2}}},
{s: `{"epoch": {"write": [2]}}`, e: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}}},
{s: `{"epoch": {"write": [1, 2]}}`, e: snap.Epoch{Read: []uint32{1, 2}, Write: []uint32{1, 2}}},
{s: `{"epoch": {"read": [2,4,8], "write": [2,3,5]}}`, e: snap.Epoch{Read: []uint32{2, 4, 8}, Write: []uint32{2, 3, 5}}},
}
for _, test := range tests {
var info Tinfo
err := json.Unmarshal([]byte(test.s), &info)
c.Check(err, check.IsNil, check.Commentf("JSON: %s", test.s))
c.Check(info.Epoch, check.DeepEquals, test.e, check.Commentf("JSON: %s", test.s))
}
}
func (s *epochSuite) TestEpochValidate(c *check.C) {
validEpochs := []snap.Epoch{
{},
{Read: []uint32{0}, Write: []uint32{0}},
{Read: []uint32{0, 1}, Write: []uint32{1}},
{Read: []uint32{1}, Write: []uint32{1}},
{Read: []uint32{399, 400}, Write: []uint32{400}},
{Read: []uint32{1, 2, 3}, Write: []uint32{1, 2, 3}},
}
for _, epoch := range validEpochs {
err := epoch.Validate()
c.Check(err, check.IsNil, check.Commentf("%s", epoch))
}
invalidEpochs := []struct {
epoch snap.Epoch
err string
}{
{epoch: snap.Epoch{Read: []uint32{}}, err: emptyEpochList},
{epoch: snap.Epoch{Write: []uint32{}}, err: emptyEpochList},
{epoch: snap.Epoch{Read: []uint32{}, Write: []uint32{}}, err: emptyEpochList},
{epoch: snap.Epoch{Read: []uint32{1}, Write: []uint32{2}}, err: noEpochIntersection},
{epoch: snap.Epoch{Read: []uint32{1, 3, 5}, Write: []uint32{2, 4, 6}}, err: noEpochIntersection},
{epoch: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{3, 2, 1}}, err: epochListNotIncreasing},
{epoch: snap.Epoch{Read: []uint32{3, 2, 1}, Write: []uint32{1, 2, 3}}, err: epochListNotIncreasing},
{epoch: snap.Epoch{Read: []uint32{3, 2, 1}, Write: []uint32{3, 2, 1}}, err: epochListNotIncreasing},
{epoch: snap.Epoch{Read: []uint32{0, 0, 0}, Write: []uint32{0}}, err: epochListNotIncreasing},
{epoch: snap.Epoch{Read: []uint32{0}, Write: []uint32{0, 0, 0}}, err: epochListNotIncreasing},
{epoch: snap.Epoch{Read: []uint32{0, 0, 0}, Write: []uint32{0, 0, 0}}, err: epochListNotIncreasing},
{epoch: snap.Epoch{
Read: []uint32{0},
Write: []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
}, err: epochListJustRidiculouslyLong},
{epoch: snap.Epoch{
Read: []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
Write: []uint32{0},
}, err: epochListJustRidiculouslyLong},
{epoch: snap.Epoch{
Read: []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
Write: []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
}, err: epochListJustRidiculouslyLong},
}
for _, test := range invalidEpochs {
err := test.epoch.Validate()
c.Check(err, check.ErrorMatches, test.err, check.Commentf("%s", test.epoch))
}
}
func (s *epochSuite) TestEpochString(c *check.C) {
tests := []struct {
e snap.Epoch
s string
}{
{e: snap.Epoch{}, s: "0"},
{e: snap.Epoch{Read: []uint32{0}}, s: "0"},
{e: snap.Epoch{Write: []uint32{0}}, s: "0"},
{e: snap.Epoch{Read: []uint32{0}, Write: []uint32{}}, s: "0"},
{e: snap.Epoch{Read: []uint32{}, Write: []uint32{0}}, s: "0"},
{e: snap.Epoch{Read: []uint32{}, Write: []uint32{}}, s: "0"},
{e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}, s: "0"},
{e: snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, s: "1*"},
{e: snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, s: "1"},
{e: snap.Epoch{Read: []uint32{399, 400}, Write: []uint32{400}}, s: "400*"},
{e: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{1, 2, 3}}, s: `{"read":[1,2,3],"write":[1,2,3]}`},
}
for _, test := range tests {
c.Check(test.e.String(), check.Equals, test.s, check.Commentf(test.s))
}
}
func (s *epochSuite) TestEpochMarshal(c *check.C) {
tests := []struct {
e snap.Epoch
s string
}{
{e: snap.Epoch{}, s: `{"read":[0],"write":[0]}`},
{e: snap.Epoch{Read: []uint32{0}}, s: `{"read":[0],"write":[0]}`},
{e: snap.Epoch{Write: []uint32{0}}, s: `{"read":[0],"write":[0]}`},
{e: snap.Epoch{Read: []uint32{0}, Write: []uint32{}}, s: `{"read":[0],"write":[0]}`},
{e: snap.Epoch{Read: []uint32{}, Write: []uint32{0}}, s: `{"read":[0],"write":[0]}`},
{e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}, s: `{"read":[0],"write":[0]}`},
{e: snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, s: `{"read":[0,1],"write":[1]}`},
{e: snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, s: `{"read":[1],"write":[1]}`},
{e: snap.Epoch{Read: []uint32{399, 400}, Write: []uint32{400}}, s: `{"read":[399,400],"write":[400]}`},
{e: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{1, 2, 3}}, s: `{"read":[1,2,3],"write":[1,2,3]}`},
}
for _, test := range tests {
bs, err := test.e.MarshalJSON()
c.Assert(err, check.IsNil)
c.Check(string(bs), check.Equals, test.s, check.Commentf(test.s))
bs, err = json.Marshal(test.e)
c.Assert(err, check.IsNil)
c.Check(string(bs), check.Equals, test.s, check.Commentf(test.s))
}
}
func (s *epochSuite) TestE(c *check.C) {
tests := []struct {
e snap.Epoch
s string
}{
{s: "0", e: snap.Epoch{Read: []uint32{0}, Write: []uint32{0}}},
{s: "1", e: snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}},
{s: "1*", e: snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}},
{s: "400*", e: snap.Epoch{Read: []uint32{399, 400}, Write: []uint32{400}}},
}
for _, test := range tests {
c.Check(snap.E(test.s), check.DeepEquals, test.e, check.Commentf(test.s))
c.Check(test.e.String(), check.Equals, test.s, check.Commentf(test.s))
}
}
func (s *epochSuite) TestIsZero(c *check.C) {
for _, e := range []*snap.Epoch{
nil,
{},
{Read: []uint32{0}},
{Write: []uint32{0}},
{Read: []uint32{0}, Write: []uint32{}},
{Read: []uint32{}, Write: []uint32{0}},
{Read: []uint32{0}, Write: []uint32{0}},
} {
c.Check(e.IsZero(), check.Equals, true, check.Commentf("%#v", e))
}
for _, e := range []*snap.Epoch{
{Read: []uint32{0, 1}, Write: []uint32{0}},
{Read: []uint32{1}, Write: []uint32{1, 2}},
} {
c.Check(e.IsZero(), check.Equals, false, check.Commentf("%#v", e))
}
}
func (s *epochSuite) TestCanRead(c *check.C) {
tests := []struct {
a, b snap.Epoch
ab, ba bool
}{
{ab: true, ba: true}, // test for empty epoch
{a: snap.E("0"), ab: true, ba: true}, // hybrid empty / zero
{a: snap.E("0"), b: snap.E("1"), ab: false, ba: false},
{a: snap.E("0"), b: snap.E("1*"), ab: false, ba: true},
{a: snap.E("0"), b: snap.E("2*"), ab: false, ba: false},
{
a: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{2}},
b: snap.Epoch{Read: []uint32{1, 3, 4}, Write: []uint32{4}},
ab: false,
ba: false,
},
{
a: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{3}},
b: snap.Epoch{Read: []uint32{1, 2, 3}, Write: []uint32{2}},
ab: true,
ba: true,
},
}
for i, test := range tests {
c.Assert(test.a.CanRead(test.b), check.Equals, test.ab, check.Commentf("ab/%d", i))
c.Assert(test.b.CanRead(test.a), check.Equals, test.ba, check.Commentf("ba/%d", i))
}
}
func (s *epochSuite) TestEqual(c *check.C) {
tests := []struct {
a, b *snap.Epoch
eq bool
}{
{a: &snap.Epoch{}, b: nil, eq: true},
{a: &snap.Epoch{Read: []uint32{}, Write: []uint32{}}, b: nil, eq: true},
{a: &snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, b: &snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, eq: true},
{a: &snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, b: &snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, eq: true},
{a: &snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, b: &snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, eq: false},
{a: &snap.Epoch{Read: []uint32{1, 2, 3, 4}, Write: []uint32{7}}, b: &snap.Epoch{Read: []uint32{1, 2, 3, 7}, Write: []uint32{7}}, eq: false},
}
for i, test := range tests {
c.Check(test.a.Equal(test.b), check.Equals, test.eq, check.Commentf("ab/%d", i))
c.Check(test.b.Equal(test.a), check.Equals, test.eq, check.Commentf("ab/%d", i))
}
}
snapd-2.37.4~14.04.1/snap/info.go 0000664 0000000 0000000 00000111024 13435556260 012763 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil/sys"
"github.com/snapcore/snapd/strutil"
"github.com/snapcore/snapd/timeout"
)
// PlaceInfo offers all the information about where a snap and its data are located and exposed in the filesystem.
type PlaceInfo interface {
// InstanceName returns the name of the snap decorated with instance
// key, if any.
InstanceName() string
// SnapName returns the name of the snap.
SnapName() string
// MountDir returns the base directory of the snap.
MountDir() string
// MountFile returns the path where the snap file that is mounted is installed.
MountFile() string
// HooksDir returns the directory containing the snap's hooks.
HooksDir() string
// DataDir returns the data directory of the snap.
DataDir() string
// UserDataDir returns the per user data directory of the snap.
UserDataDir(home string) string
// CommonDataDir returns the data directory common across revisions of the snap.
CommonDataDir() string
// UserCommonDataDir returns the per user data directory common across revisions of the snap.
UserCommonDataDir(home string) string
// UserXdgRuntimeDir returns the per user XDG_RUNTIME_DIR directory
UserXdgRuntimeDir(userID sys.UserID) string
// DataHomeDir returns the a glob that matches all per user data directories of a snap.
DataHomeDir() string
// CommonDataHomeDir returns a glob that matches all per user data directories common across revisions of the snap.
CommonDataHomeDir() string
// XdgRuntimeDirs returns a glob that matches all XDG_RUNTIME_DIR directories for all users of the snap.
XdgRuntimeDirs() string
}
// MinimalPlaceInfo returns a PlaceInfo with just the location information for a snap of the given name and revision.
func MinimalPlaceInfo(name string, revision Revision) PlaceInfo {
storeName, instanceKey := SplitInstanceName(name)
return &Info{SideInfo: SideInfo{RealName: storeName, Revision: revision}, InstanceKey: instanceKey}
}
// BaseDir returns the system level directory of given snap.
func BaseDir(name string) string {
return filepath.Join(dirs.SnapMountDir, name)
}
// MountDir returns the base directory where it gets mounted of the snap with the given name and revision.
func MountDir(name string, revision Revision) string {
return filepath.Join(BaseDir(name), revision.String())
}
// MountFile returns the path where the snap file that is mounted is installed.
func MountFile(name string, revision Revision) string {
return filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%s.snap", name, revision))
}
// ScopedSecurityTag returns the snap-specific, scope specific, security tag.
func ScopedSecurityTag(snapName, scopeName, suffix string) string {
return fmt.Sprintf("snap.%s.%s.%s", snapName, scopeName, suffix)
}
// SecurityTag returns the snap-specific security tag.
func SecurityTag(snapName string) string {
return fmt.Sprintf("snap.%s", snapName)
}
// AppSecurityTag returns the application-specific security tag.
func AppSecurityTag(snapName, appName string) string {
return fmt.Sprintf("%s.%s", SecurityTag(snapName), appName)
}
// HookSecurityTag returns the hook-specific security tag.
func HookSecurityTag(snapName, hookName string) string {
return ScopedSecurityTag(snapName, "hook", hookName)
}
// NoneSecurityTag returns the security tag for interfaces that
// are not associated to an app or hook in the snap.
func NoneSecurityTag(snapName, uniqueName string) string {
return ScopedSecurityTag(snapName, "none", uniqueName)
}
// BaseDataDir returns the base directory for snap data locations.
func BaseDataDir(name string) string {
return filepath.Join(dirs.SnapDataDir, name)
}
// DataDir returns the data directory for given snap name and revision. The name can be
// either a snap name or snap instance name.
func DataDir(name string, revision Revision) string {
return filepath.Join(BaseDataDir(name), revision.String())
}
// CommonDataDir returns the common data directory for given snap name. The name
// can be either a snap name or snap instance name.
func CommonDataDir(name string) string {
return filepath.Join(dirs.SnapDataDir, name, "common")
}
// HooksDir returns the directory containing the snap's hooks for given snap
// name. The name can be either a snap name or snap instance name.
func HooksDir(name string, revision Revision) string {
return filepath.Join(MountDir(name, revision), "meta", "hooks")
}
// UserDataDir returns the user-specific data directory for given snap name. The
// name can be either a snap name or snap instance name.
func UserDataDir(home string, name string, revision Revision) string {
return filepath.Join(home, dirs.UserHomeSnapDir, name, revision.String())
}
// UserCommonDataDir returns the user-specific common data directory for given
// snap name. The name can be either a snap name or snap instance name.
func UserCommonDataDir(home string, name string) string {
return filepath.Join(home, dirs.UserHomeSnapDir, name, "common")
}
// UserSnapDir returns the user-specific directory for given
// snap name. The name can be either a snap name or snap instance name.
func UserSnapDir(home string, name string) string {
return filepath.Join(home, dirs.UserHomeSnapDir, name)
}
// UserXdgRuntimeDir returns the user-specific XDG_RUNTIME_DIR directory for
// given snap name. The name can be either a snap name or snap instance name.
func UserXdgRuntimeDir(euid sys.UserID, name string) string {
return filepath.Join(dirs.XdgRuntimeDirBase, fmt.Sprintf("%d/snap.%s", euid, name))
}
// SideInfo holds snap metadata that is crucial for the tracking of
// snaps and for the working of the system offline and which is not
// included in snap.yaml or for which the store is the canonical
// source overriding snap.yaml content.
//
// It can be marshalled and will be stored in the system state for
// each currently installed snap revision so it needs to be evolved
// carefully.
//
// Information that can be taken directly from snap.yaml or that comes
// from the store but is not required for working offline should not
// end up in SideInfo.
type SideInfo struct {
RealName string `yaml:"name,omitempty" json:"name,omitempty"`
SnapID string `yaml:"snap-id" json:"snap-id"`
Revision Revision `yaml:"revision" json:"revision"`
Channel string `yaml:"channel,omitempty" json:"channel,omitempty"`
Contact string `yaml:"contact,omitempty" json:"contact,omitempty"`
EditedTitle string `yaml:"title,omitempty" json:"title,omitempty"`
EditedSummary string `yaml:"summary,omitempty" json:"summary,omitempty"`
EditedDescription string `yaml:"description,omitempty" json:"description,omitempty"`
Private bool `yaml:"private,omitempty" json:"private,omitempty"`
Paid bool `yaml:"paid,omitempty" json:"paid,omitempty"`
}
// Info provides information about snaps.
type Info struct {
SuggestedName string
InstanceKey string
Version string
Type Type
Architectures []string
Assumes []string
OriginalTitle string
OriginalSummary string
OriginalDescription string
Environment strutil.OrderedMap
LicenseAgreement string
LicenseVersion string
License string
Epoch Epoch
Base string
Confinement ConfinementType
Apps map[string]*AppInfo
LegacyAliases map[string]*AppInfo // FIXME: eventually drop this
Hooks map[string]*HookInfo
Plugs map[string]*PlugInfo
Slots map[string]*SlotInfo
toplevelPlugs []*PlugInfo
toplevelSlots []*SlotInfo
// Plugs or slots with issues (they are not included in Plugs or Slots)
BadInterfaces map[string]string // slot or plug => message
// The information in all the remaining fields is not sourced from the snap blob itself.
SideInfo
// Broken marks whether the snap is broken and the reason.
Broken string
// The information in these fields is ephemeral, available only from the store.
DownloadInfo
Prices map[string]float64
MustBuy bool
Publisher StoreAccount
Media MediaInfos
// The flattended channel map with $track/$risk
Channels map[string]*ChannelSnapInfo
// The ordered list of tracks that contain channels
Tracks []string
Layout map[string]*Layout
// The list of common-ids from all apps of the snap
CommonIDs []string
}
// StoreAccount holds information about a store account, for example
// of snap publisher.
type StoreAccount struct {
ID string `json:"id"`
Username string `json:"username"`
DisplayName string `json:"display-name"`
Validation string `json:"validation,omitempty"`
}
// Layout describes a single element of the layout section.
type Layout struct {
Snap *Info
Path string `json:"path"`
Bind string `json:"bind,omitempty"`
BindFile string `json:"bind-file,omitempty"`
Type string `json:"type,omitempty"`
User string `json:"user,omitempty"`
Group string `json:"group,omitempty"`
Mode os.FileMode `json:"mode,omitempty"`
Symlink string `json:"symlink,omitempty"`
}
// String returns a simple textual representation of a layout.
func (l *Layout) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "%s: ", l.Path)
switch {
case l.Bind != "":
fmt.Fprintf(&buf, "bind %s", l.Bind)
case l.BindFile != "":
fmt.Fprintf(&buf, "bind-file %s", l.BindFile)
case l.Symlink != "":
fmt.Fprintf(&buf, "symlink %s", l.Symlink)
case l.Type != "":
fmt.Fprintf(&buf, "type %s", l.Type)
default:
fmt.Fprintf(&buf, "???")
}
if l.User != "root" && l.User != "" {
fmt.Fprintf(&buf, ", user: %s", l.User)
}
if l.Group != "root" && l.Group != "" {
fmt.Fprintf(&buf, ", group: %s", l.Group)
}
if l.Mode != 0755 {
fmt.Fprintf(&buf, ", mode: %#o", l.Mode)
}
return buf.String()
}
// ChannelSnapInfo is the minimum information that can be used to clearly
// distinguish different revisions of the same snap.
type ChannelSnapInfo struct {
Revision Revision `json:"revision"`
Confinement ConfinementType `json:"confinement"`
Version string `json:"version"`
Channel string `json:"channel"`
Epoch Epoch `json:"epoch"`
Size int64 `json:"size"`
ReleasedAt time.Time `json:"released-at"`
}
// InstanceName returns the blessed name of the snap decorated with instance
// key, if any.
func (s *Info) InstanceName() string {
return InstanceName(s.SnapName(), s.InstanceKey)
}
// SnapName returns the global blessed name of the snap.
func (s *Info) SnapName() string {
if s.RealName != "" {
return s.RealName
}
return s.SuggestedName
}
// Title returns the blessed title for the snap.
func (s *Info) Title() string {
if s.EditedTitle != "" {
return s.EditedTitle
}
return s.OriginalTitle
}
// Summary returns the blessed summary for the snap.
func (s *Info) Summary() string {
if s.EditedSummary != "" {
return s.EditedSummary
}
return s.OriginalSummary
}
// Description returns the blessed description for the snap.
func (s *Info) Description() string {
if s.EditedDescription != "" {
return s.EditedDescription
}
return s.OriginalDescription
}
// MountDir returns the base directory of the snap where it gets mounted.
func (s *Info) MountDir() string {
return MountDir(s.InstanceName(), s.Revision)
}
// MountFile returns the path where the snap file that is mounted is installed.
func (s *Info) MountFile() string {
return MountFile(s.InstanceName(), s.Revision)
}
// HooksDir returns the directory containing the snap's hooks.
func (s *Info) HooksDir() string {
return HooksDir(s.InstanceName(), s.Revision)
}
// DataDir returns the data directory of the snap.
func (s *Info) DataDir() string {
return DataDir(s.InstanceName(), s.Revision)
}
// UserDataDir returns the user-specific data directory of the snap.
func (s *Info) UserDataDir(home string) string {
return UserDataDir(home, s.InstanceName(), s.Revision)
}
// UserCommonDataDir returns the user-specific data directory common across revision of the snap.
func (s *Info) UserCommonDataDir(home string) string {
return UserCommonDataDir(home, s.InstanceName())
}
// CommonDataDir returns the data directory common across revisions of the snap.
func (s *Info) CommonDataDir() string {
return CommonDataDir(s.InstanceName())
}
// DataHomeDir returns the per user data directory of the snap.
func (s *Info) DataHomeDir() string {
return filepath.Join(dirs.SnapDataHomeGlob, s.InstanceName(), s.Revision.String())
}
// CommonDataHomeDir returns the per user data directory common across revisions of the snap.
func (s *Info) CommonDataHomeDir() string {
return filepath.Join(dirs.SnapDataHomeGlob, s.InstanceName(), "common")
}
// UserXdgRuntimeDir returns the XDG_RUNTIME_DIR directory of the snap for a particular user.
func (s *Info) UserXdgRuntimeDir(euid sys.UserID) string {
return UserXdgRuntimeDir(euid, s.InstanceName())
}
// XdgRuntimeDirs returns the XDG_RUNTIME_DIR directories for all users of the snap.
func (s *Info) XdgRuntimeDirs() string {
return filepath.Join(dirs.XdgRuntimeDirGlob, fmt.Sprintf("snap.%s", s.InstanceName()))
}
// NeedsDevMode returns whether the snap needs devmode.
func (s *Info) NeedsDevMode() bool {
return s.Confinement == DevModeConfinement
}
// NeedsClassic returns whether the snap needs classic confinement consent.
func (s *Info) NeedsClassic() bool {
return s.Confinement == ClassicConfinement
}
// Services returns a list of the apps that have "daemon" set.
func (s *Info) Services() []*AppInfo {
svcs := make([]*AppInfo, 0, len(s.Apps))
for _, app := range s.Apps {
if !app.IsService() {
continue
}
svcs = append(svcs, app)
}
return svcs
}
// ExpandSnapVariables resolves $SNAP, $SNAP_DATA and $SNAP_COMMON inside the
// snap's mount namespace.
func (s *Info) ExpandSnapVariables(path string) string {
return os.Expand(path, func(v string) string {
switch v {
case "SNAP":
// NOTE: We use dirs.CoreSnapMountDir here as the path used will be always
// inside the mount namespace snap-confine creates and there we will
// always have a /snap directory available regardless if the system
// we're running on supports this or not.
return filepath.Join(dirs.CoreSnapMountDir, s.SnapName(), s.Revision.String())
case "SNAP_DATA":
return DataDir(s.SnapName(), s.Revision)
case "SNAP_COMMON":
return CommonDataDir(s.SnapName())
}
return ""
})
}
// InstallDate returns the "install date" of the snap.
//
// If the snap is not active, it'll return a zero time; otherwise
// it'll return the modtime of the "current" symlink. Sneaky.
func (s *Info) InstallDate() time.Time {
dir, rev := filepath.Split(s.MountDir())
cur := filepath.Join(dir, "current")
tag, err := os.Readlink(cur)
if err == nil && tag == rev {
if st, err := os.Lstat(cur); err == nil {
return st.ModTime()
}
}
return time.Time{}
}
// IsActive returns whether this snap revision is active.
func (s *Info) IsActive() bool {
dir, rev := filepath.Split(s.MountDir())
cur := filepath.Join(dir, "current")
tag, err := os.Readlink(cur)
return err == nil && tag == rev
}
// BadInterfacesSummary returns a summary of the problems of bad plugs
// and slots in the snap.
func BadInterfacesSummary(snapInfo *Info) string {
inverted := make(map[string][]string)
for name, reason := range snapInfo.BadInterfaces {
inverted[reason] = append(inverted[reason], name)
}
var buf bytes.Buffer
fmt.Fprintf(&buf, "snap %q has bad plugs or slots: ", snapInfo.InstanceName())
reasons := make([]string, 0, len(inverted))
for reason := range inverted {
reasons = append(reasons, reason)
}
sort.Strings(reasons)
for _, reason := range reasons {
names := inverted[reason]
sort.Strings(names)
for i, name := range names {
if i > 0 {
buf.WriteString(", ")
}
buf.WriteString(name)
}
fmt.Fprintf(&buf, " (%s); ", reason)
}
return strings.TrimSuffix(buf.String(), "; ")
}
// DownloadInfo contains the information to download a snap.
// It can be marshalled.
type DownloadInfo struct {
AnonDownloadURL string `json:"anon-download-url,omitempty"`
DownloadURL string `json:"download-url,omitempty"`
Size int64 `json:"size,omitempty"`
Sha3_384 string `json:"sha3-384,omitempty"`
// The server can include information about available deltas for a given
// snap at a specific revision during refresh. Currently during refresh the
// server will provide single matching deltas only, from the clients
// revision to the target revision when available, per requested format.
Deltas []DeltaInfo `json:"deltas,omitempty"`
}
// DeltaInfo contains the information to download a delta
// from one revision to another.
type DeltaInfo struct {
FromRevision int `json:"from-revision,omitempty"`
ToRevision int `json:"to-revision,omitempty"`
Format string `json:"format,omitempty"`
AnonDownloadURL string `json:"anon-download-url,omitempty"`
DownloadURL string `json:"download-url,omitempty"`
Size int64 `json:"size,omitempty"`
Sha3_384 string `json:"sha3-384,omitempty"`
}
// sanity check that Info is a PlaceInfo
var _ PlaceInfo = (*Info)(nil)
// PlugInfo provides information about a plug.
type PlugInfo struct {
Snap *Info
Name string
Interface string
Attrs map[string]interface{}
Label string
Apps map[string]*AppInfo
Hooks map[string]*HookInfo
}
func lookupAttr(attrs map[string]interface{}, path string) (interface{}, bool) {
var v interface{}
comps := strings.FieldsFunc(path, func(r rune) bool { return r == '.' })
if len(comps) == 0 {
return nil, false
}
v = attrs
for _, comp := range comps {
m, ok := v.(map[string]interface{})
if !ok {
return nil, false
}
v, ok = m[comp]
if !ok {
return nil, false
}
}
return v, true
}
func getAttribute(snapName string, ifaceName string, attrs map[string]interface{}, key string, val interface{}) error {
v, ok := lookupAttr(attrs, key)
if !ok {
return fmt.Errorf("snap %q does not have attribute %q for interface %q", snapName, key, ifaceName)
}
rt := reflect.TypeOf(val)
if rt.Kind() != reflect.Ptr || val == nil {
return fmt.Errorf("internal error: cannot get %q attribute of interface %q with non-pointer value", key, ifaceName)
}
if reflect.TypeOf(v) != rt.Elem() {
return fmt.Errorf("snap %q has interface %q with invalid value type for %q attribute", snapName, ifaceName, key)
}
rv := reflect.ValueOf(val)
rv.Elem().Set(reflect.ValueOf(v))
return nil
}
func (plug *PlugInfo) Attr(key string, val interface{}) error {
return getAttribute(plug.Snap.InstanceName(), plug.Interface, plug.Attrs, key, val)
}
func (plug *PlugInfo) Lookup(key string) (interface{}, bool) {
return lookupAttr(plug.Attrs, key)
}
// SecurityTags returns security tags associated with a given plug.
func (plug *PlugInfo) SecurityTags() []string {
tags := make([]string, 0, len(plug.Apps)+len(plug.Hooks))
for _, app := range plug.Apps {
tags = append(tags, app.SecurityTag())
}
for _, hook := range plug.Hooks {
tags = append(tags, hook.SecurityTag())
}
sort.Strings(tags)
return tags
}
// String returns the representation of the plug as snap:plug string.
func (plug *PlugInfo) String() string {
return fmt.Sprintf("%s:%s", plug.Snap.InstanceName(), plug.Name)
}
func (slot *SlotInfo) Attr(key string, val interface{}) error {
return getAttribute(slot.Snap.InstanceName(), slot.Interface, slot.Attrs, key, val)
}
func (slot *SlotInfo) Lookup(key string) (interface{}, bool) {
return lookupAttr(slot.Attrs, key)
}
// SecurityTags returns security tags associated with a given slot.
func (slot *SlotInfo) SecurityTags() []string {
tags := make([]string, 0, len(slot.Apps))
for _, app := range slot.Apps {
tags = append(tags, app.SecurityTag())
}
for _, hook := range slot.Hooks {
tags = append(tags, hook.SecurityTag())
}
sort.Strings(tags)
return tags
}
// String returns the representation of the slot as snap:slot string.
func (slot *SlotInfo) String() string {
return fmt.Sprintf("%s:%s", slot.Snap.InstanceName(), slot.Name)
}
// SlotInfo provides information about a slot.
type SlotInfo struct {
Snap *Info
Name string
Interface string
Attrs map[string]interface{}
Label string
Apps map[string]*AppInfo
Hooks map[string]*HookInfo
// HotplugKey is a unique key built by the slot's interface
// using properties of a hotplugged device so that the same
// slot may be made available if the device is reinserted.
// It's empty for regular slots.
HotplugKey string
}
// SocketInfo provides information on application sockets.
type SocketInfo struct {
App *AppInfo
Name string
ListenStream string
SocketMode os.FileMode
}
// TimerInfo provides information on application timer.
type TimerInfo struct {
App *AppInfo
Timer string
}
// StopModeType is the type for the "stop-mode:" of a snap app
type StopModeType string
// KillAll returns if the stop-mode means all processes should be killed
// when the service is stopped or just the main process.
func (st StopModeType) KillAll() bool {
return string(st) == "" || strings.HasSuffix(string(st), "-all")
}
// KillSignal returns the signal that should be used to kill the process
// (or an empty string if no signal is needed)
func (st StopModeType) KillSignal() string {
if st.Validate() != nil || st == "" {
return ""
}
return strings.ToUpper(strings.TrimSuffix(string(st), "-all"))
}
func (st StopModeType) Validate() error {
switch st {
case "", "sigterm", "sigterm-all", "sighup", "sighup-all", "sigusr1", "sigusr1-all", "sigusr2", "sigusr2-all":
// valid
return nil
}
return fmt.Errorf(`"stop-mode" field contains invalid value %q`, st)
}
// AppInfo provides information about an app.
type AppInfo struct {
Snap *Info
Name string
LegacyAliases []string // FIXME: eventually drop this
Command string
CommandChain []string
CommonID string
Daemon string
StopTimeout timeout.Timeout
WatchdogTimeout timeout.Timeout
StopCommand string
ReloadCommand string
PostStopCommand string
RestartCond RestartCondition
RestartDelay timeout.Timeout
Completer string
RefreshMode string
StopMode StopModeType
// TODO: this should go away once we have more plumbing and can change
// things vs refactor
// https://github.com/snapcore/snapd/pull/794#discussion_r58688496
BusName string
Plugs map[string]*PlugInfo
Slots map[string]*SlotInfo
Sockets map[string]*SocketInfo
Environment strutil.OrderedMap
// list of other service names that this service will start after or
// before
After []string
Before []string
Timer *TimerInfo
Autostart string
}
// ScreenshotInfo provides information about a screenshot.
type ScreenshotInfo struct {
URL string `json:"url"`
Width int64 `json:"width,omitempty"`
Height int64 `json:"height,omitempty"`
Note string `json:"note,omitempty"`
}
type MediaInfo struct {
Type string `json:"type"`
URL string `json:"url"`
Width int64 `json:"width,omitempty"`
Height int64 `json:"height,omitempty"`
}
type MediaInfos []MediaInfo
const ScreenshotsDeprecationNotice = `'screenshots' is deprecated; use 'media' instead. More info at https://forum.snapcraft.io/t/8086`
func (mis MediaInfos) Screenshots() []ScreenshotInfo {
shots := make([]ScreenshotInfo, 0, len(mis))
for _, mi := range mis {
if mi.Type != "screenshot" {
continue
}
shots = append(shots, ScreenshotInfo{
URL: mi.URL,
Width: mi.Width,
Height: mi.Height,
Note: ScreenshotsDeprecationNotice,
})
}
return shots
}
func (mis MediaInfos) IconURL() string {
for _, mi := range mis {
if mi.Type == "icon" {
return mi.URL
}
}
return ""
}
// HookInfo provides information about a hook.
type HookInfo struct {
Snap *Info
Name string
Plugs map[string]*PlugInfo
Slots map[string]*SlotInfo
Environment strutil.OrderedMap
CommandChain []string
Explicit bool
}
// File returns the path to the *.socket file
func (socket *SocketInfo) File() string {
return filepath.Join(dirs.SnapServicesDir, socket.App.SecurityTag()+"."+socket.Name+".socket")
}
// File returns the path to the *.timer file
func (timer *TimerInfo) File() string {
return filepath.Join(dirs.SnapServicesDir, timer.App.SecurityTag()+".timer")
}
func (app *AppInfo) String() string {
return JoinSnapApp(app.Snap.InstanceName(), app.Name)
}
// SecurityTag returns application-specific security tag.
//
// Security tags are used by various security subsystems as "profile names" and
// sometimes also as a part of the file name.
func (app *AppInfo) SecurityTag() string {
return AppSecurityTag(app.Snap.InstanceName(), app.Name)
}
// DesktopFile returns the path to the installed optional desktop file for the application.
func (app *AppInfo) DesktopFile() string {
return filepath.Join(dirs.SnapDesktopFilesDir, fmt.Sprintf("%s_%s.desktop", app.Snap.InstanceName(), app.Name))
}
// WrapperPath returns the path to wrapper invoking the app binary.
func (app *AppInfo) WrapperPath() string {
return filepath.Join(dirs.SnapBinariesDir, JoinSnapApp(app.Snap.InstanceName(), app.Name))
}
// CompleterPath returns the path to the completer snippet for the app binary.
func (app *AppInfo) CompleterPath() string {
return filepath.Join(dirs.CompletersDir, JoinSnapApp(app.Snap.InstanceName(), app.Name))
}
func (app *AppInfo) launcherCommand(command string) string {
if command != "" {
command = " " + command
}
if app.Name == app.Snap.SnapName() {
return fmt.Sprintf("/usr/bin/snap run%s %s", command, app.Snap.InstanceName())
}
return fmt.Sprintf("/usr/bin/snap run%s %s.%s", command, app.Snap.InstanceName(), app.Name)
}
// LauncherCommand returns the launcher command line to use when invoking the app binary.
func (app *AppInfo) LauncherCommand() string {
if app.Timer != nil {
return app.launcherCommand(fmt.Sprintf("--timer=%q", app.Timer.Timer))
}
return app.launcherCommand("")
}
// LauncherStopCommand returns the launcher command line to use when invoking the app stop command binary.
func (app *AppInfo) LauncherStopCommand() string {
return app.launcherCommand("--command=stop")
}
// LauncherReloadCommand returns the launcher command line to use when invoking the app stop command binary.
func (app *AppInfo) LauncherReloadCommand() string {
return app.launcherCommand("--command=reload")
}
// LauncherPostStopCommand returns the launcher command line to use when invoking the app post-stop command binary.
func (app *AppInfo) LauncherPostStopCommand() string {
return app.launcherCommand("--command=post-stop")
}
// ServiceName returns the systemd service name for the daemon app.
func (app *AppInfo) ServiceName() string {
return app.SecurityTag() + ".service"
}
// ServiceFile returns the systemd service file path for the daemon app.
func (app *AppInfo) ServiceFile() string {
return filepath.Join(dirs.SnapServicesDir, app.ServiceName())
}
// Env returns the app specific environment overrides
func (app *AppInfo) Env() []string {
appEnv := app.Snap.Environment.Copy()
for _, k := range app.Environment.Keys() {
appEnv.Set(k, app.Environment.Get(k))
}
return envFromMap(appEnv)
}
// IsService returns whether app represents a daemon/service.
func (app *AppInfo) IsService() bool {
return app.Daemon != ""
}
// SecurityTag returns the hook-specific security tag.
//
// Security tags are used by various security subsystems as "profile names" and
// sometimes also as a part of the file name.
func (hook *HookInfo) SecurityTag() string {
return HookSecurityTag(hook.Snap.InstanceName(), hook.Name)
}
// Env returns the hook-specific environment overrides
func (hook *HookInfo) Env() []string {
hookEnv := hook.Snap.Environment.Copy()
for _, k := range hook.Environment.Keys() {
hookEnv.Set(k, hook.Environment.Get(k))
}
return envFromMap(hookEnv)
}
func envFromMap(envMap *strutil.OrderedMap) []string {
env := []string{}
for _, k := range envMap.Keys() {
env = append(env, fmt.Sprintf("%s=%s", k, envMap.Get(k)))
}
return env
}
func infoFromSnapYamlWithSideInfo(meta []byte, si *SideInfo) (*Info, error) {
info, err := InfoFromSnapYaml(meta)
if err != nil {
return nil, err
}
if si != nil {
info.SideInfo = *si
}
return info, nil
}
// BrokenSnapError describes an error that refers to a snap that warrants the "broken" note.
type BrokenSnapError interface {
error
Broken() string
}
type NotFoundError struct {
Snap string
Revision Revision
// Path encodes the path that triggered the not-found error.
// It may refer to a file inside the snap or to the snap file itself.
Path string
}
func (e NotFoundError) Error() string {
if e.Path != "" {
return fmt.Sprintf("cannot find installed snap %q at revision %s: missing file %s", e.Snap, e.Revision, e.Path)
}
return fmt.Sprintf("cannot find installed snap %q at revision %s", e.Snap, e.Revision)
}
func (e NotFoundError) Broken() string {
return e.Error()
}
type invalidMetaError struct {
Snap string
Revision Revision
Msg string
}
func (e invalidMetaError) Error() string {
return fmt.Sprintf("cannot use installed snap %q at revision %s: %s", e.Snap, e.Revision, e.Msg)
}
func (e invalidMetaError) Broken() string {
return e.Error()
}
func MockSanitizePlugsSlots(f func(snapInfo *Info)) (restore func()) {
old := SanitizePlugsSlots
SanitizePlugsSlots = f
return func() { SanitizePlugsSlots = old }
}
var SanitizePlugsSlots = func(snapInfo *Info) {
panic("SanitizePlugsSlots function not set")
}
// ReadInfo reads the snap information for the installed snap with the given name and given side-info.
func ReadInfo(name string, si *SideInfo) (*Info, error) {
snapYamlFn := filepath.Join(MountDir(name, si.Revision), "meta", "snap.yaml")
meta, err := ioutil.ReadFile(snapYamlFn)
if os.IsNotExist(err) {
return nil, &NotFoundError{Snap: name, Revision: si.Revision, Path: snapYamlFn}
}
if err != nil {
return nil, err
}
info, err := infoFromSnapYamlWithSideInfo(meta, si)
if err != nil {
return nil, &invalidMetaError{Snap: name, Revision: si.Revision, Msg: err.Error()}
}
_, instanceKey := SplitInstanceName(name)
info.InstanceKey = instanceKey
err = addImplicitHooks(info)
if err != nil {
return nil, &invalidMetaError{Snap: name, Revision: si.Revision, Msg: err.Error()}
}
bindImplicitHooks(info)
mountFile := MountFile(name, si.Revision)
st, err := os.Lstat(mountFile)
if os.IsNotExist(err) {
// This can happen when "snap try" mode snap is moved around. The mount
// is still in place (it's a bind mount, it doesn't care about the
// source moving) but the symlink in /var/lib/snapd/snaps is now
// dangling.
return nil, &NotFoundError{Snap: name, Revision: si.Revision, Path: mountFile}
}
if err != nil {
return nil, err
}
// If the file is a regular file than it must be a squashfs file that is
// used as the backing store for the snap. The size of that file is the
// size of the snap.
if st.Mode().IsRegular() {
info.Size = st.Size()
}
return info, nil
}
// ReadCurrentInfo reads the snap information from the installed snap in 'current' revision
func ReadCurrentInfo(snapName string) (*Info, error) {
curFn := filepath.Join(dirs.SnapMountDir, snapName, "current")
realFn, err := os.Readlink(curFn)
if err != nil {
return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err)
}
rev := filepath.Base(realFn)
revision, err := ParseRevision(rev)
if err != nil {
return nil, fmt.Errorf("cannot read revision %s: %s", rev, err)
}
return ReadInfo(snapName, &SideInfo{Revision: revision})
}
// ReadInfoFromSnapFile reads the snap information from the given Container
// and completes it with the given side-info if this is not nil.
func ReadInfoFromSnapFile(snapf Container, si *SideInfo) (*Info, error) {
meta, err := snapf.ReadFile("meta/snap.yaml")
if err != nil {
return nil, err
}
info, err := infoFromSnapYamlWithSideInfo(meta, si)
if err != nil {
return nil, err
}
info.Size, err = snapf.Size()
if err != nil {
return nil, err
}
err = addImplicitHooksFromContainer(info, snapf)
if err != nil {
return nil, err
}
bindImplicitHooks(info)
err = Validate(info)
if err != nil {
return nil, err
}
return info, nil
}
// InstallDate returns the "install date" of the snap.
//
// If the snap is not active, it'll return a zero time; otherwise
// it'll return the modtime of the "current" symlink.
func InstallDate(name string) time.Time {
cur := filepath.Join(dirs.SnapMountDir, name, "current")
if st, err := os.Lstat(cur); err == nil {
return st.ModTime()
}
return time.Time{}
}
// SplitSnapApp will split a string of the form `snap.app` into
// the `snap` and the `app` part. It also deals with the special
// case of snapName == appName.
func SplitSnapApp(snapApp string) (snap, app string) {
l := strings.SplitN(snapApp, ".", 2)
if len(l) < 2 {
return l[0], InstanceSnap(l[0])
}
return l[0], l[1]
}
// JoinSnapApp produces a full application wrapper name from the
// `snap` and the `app` part. It also deals with the special
// case of snapName == appName.
func JoinSnapApp(snap, app string) string {
storeName, instanceKey := SplitInstanceName(snap)
if storeName == app {
return InstanceName(app, instanceKey)
}
return fmt.Sprintf("%s.%s", snap, app)
}
// InstanceSnap splits the instance name and returns the name of the snap.
func InstanceSnap(instanceName string) string {
snapName, _ := SplitInstanceName(instanceName)
return snapName
}
// SplitInstanceName splits the instance name and returns the snap name and the
// instance key.
func SplitInstanceName(instanceName string) (snapName, instanceKey string) {
split := strings.SplitN(instanceName, "_", 2)
snapName = split[0]
if len(split) > 1 {
instanceKey = split[1]
}
return snapName, instanceKey
}
// InstanceName takes the snap name and the instance key and returns an instance
// name of the snap.
func InstanceName(snapName, instanceKey string) string {
if instanceKey != "" {
return fmt.Sprintf("%s_%s", snapName, instanceKey)
}
return snapName
}
// ByType supports sorting the given slice of snap info by types. The most
// important types will come first.
type ByType []*Info
func (r ByType) Len() int { return len(r) }
func (r ByType) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r ByType) Less(i, j int) bool {
return r[i].Type.SortsBefore(r[j].Type)
}
func SortServices(apps []*AppInfo) (sorted []*AppInfo, err error) {
nameToApp := make(map[string]*AppInfo, len(apps))
for _, app := range apps {
nameToApp[app.Name] = app
}
// list of successors of given app
successors := make(map[string][]*AppInfo, len(apps))
// count of predecessors (i.e. incoming edges) of given app
predecessors := make(map[string]int, len(apps))
for _, app := range apps {
for _, other := range app.After {
predecessors[app.Name]++
successors[other] = append(successors[other], app)
}
for _, other := range app.Before {
predecessors[other]++
successors[app.Name] = append(successors[app.Name], nameToApp[other])
}
}
// list of apps without predecessors (no incoming edges)
queue := make([]*AppInfo, 0, len(apps))
for _, app := range apps {
if predecessors[app.Name] == 0 {
queue = append(queue, app)
}
}
// Kahn:
// see https://dl.acm.org/citation.cfm?doid=368996.369025
// https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
//
// Apps without predecessors are 'top' nodes. On each iteration, take
// the next 'top' node, and decrease the predecessor count of each
// successor app. Once that successor app has no more predecessors, take
// it out of the predecessors set and add it to the queue of 'top'
// nodes.
for len(queue) > 0 {
app := queue[0]
queue = queue[1:]
for _, successor := range successors[app.Name] {
predecessors[successor.Name]--
if predecessors[successor.Name] == 0 {
delete(predecessors, successor.Name)
queue = append(queue, successor)
}
}
sorted = append(sorted, app)
}
if len(predecessors) != 0 {
// apps with predecessors unaccounted for are a part of
// dependency cycle
unsatisifed := bytes.Buffer{}
for name := range predecessors {
if unsatisifed.Len() > 0 {
unsatisifed.WriteString(", ")
}
unsatisifed.WriteString(name)
}
return nil, fmt.Errorf("applications are part of a before/after cycle: %s", unsatisifed.String())
}
return sorted, nil
}
snapd-2.37.4~14.04.1/snap/container.go 0000664 0000000 0000000 00000020127 13435556260 014015 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap/snapdir"
"github.com/snapcore/snapd/snap/squashfs"
)
// Container is the interface to interact with the low-level snap files.
type Container interface {
// Size returns the size of the snap in bytes.
Size() (int64, error)
// ReadFile returns the content of a single file from the snap.
ReadFile(relative string) ([]byte, error)
// Walk is like filepath.Walk, without the ordering guarantee.
Walk(relative string, walkFn filepath.WalkFunc) error
// ListDir returns the content of a single directory inside the snap.
ListDir(path string) ([]string, error)
// Install copies the snap file to targetPath (and possibly unpacks it to mountDir)
Install(targetPath, mountDir string) error
// Unpack unpacks the src parts to the dst directory
Unpack(src, dst string) error
}
// backend implements a specific snap format
type snapFormat struct {
magic []byte
open func(fn string) (Container, error)
}
// formatHandlers is the registry of known formats, squashfs is the only one atm.
var formatHandlers = []snapFormat{
{squashfs.Magic, func(p string) (Container, error) {
return squashfs.New(p), nil
}},
}
// Open opens a given snap file with the right backend.
func Open(path string) (Container, error) {
if osutil.IsDirectory(path) {
if osutil.FileExists(filepath.Join(path, "meta", "snap.yaml")) {
return snapdir.New(path), nil
}
return nil, NotSnapError{Path: path}
}
// open the file and check magic
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("cannot open snap: %v", err)
}
defer f.Close()
header := make([]byte, 20)
if _, err := f.ReadAt(header, 0); err != nil {
return nil, fmt.Errorf("cannot read snap: %v", err)
}
for _, h := range formatHandlers {
if bytes.HasPrefix(header, h.magic) {
return h.open(path)
}
}
return nil, fmt.Errorf("cannot open snap: unknown header: %q", header)
}
var (
// ErrBadModes is returned by ValidateContainer when the container has files with the wrong file modes for their role
ErrBadModes = errors.New("snap is unusable due to bad permissions")
// ErrMissingPaths is returned by ValidateContainer when the container is missing required files or directories
ErrMissingPaths = errors.New("snap is unusable due to missing files")
)
// ValidateContainer does a minimal sanity check on the container.
func ValidateContainer(c Container, s *Info, logf func(format string, v ...interface{})) error {
// needsrx keeps track of things that need to have at least 0555 perms
needsrx := map[string]bool{
".": true,
"meta": true,
}
// needsx keeps track of things that need to have at least 0111 perms
needsx := map[string]bool{}
// needsr keeps track of things that need to have at least 0444 perms
needsr := map[string]bool{
"meta/snap.yaml": true,
}
// needsf keeps track of things that need to be regular files (or symlinks to regular files)
needsf := map[string]bool{}
// noskipd tracks directories we want to descend into despite not being in needs*
noskipd := map[string]bool{}
for _, app := range s.Apps {
// for non-services, paths go into the needsrx bag because users
// need rx perms to execute it
bag := needsrx
paths := []string{app.Command}
if app.IsService() {
// services' paths just need to not be skipped by the validator
bag = noskipd
// additional paths to check for services:
// XXX maybe have a method on app to keep this in sync
paths = append(paths, app.StopCommand, app.ReloadCommand, app.PostStopCommand)
}
for _, path := range paths {
path = normPath(path)
if path == "" {
continue
}
needsf[path] = true
if app.IsService() {
needsx[path] = true
}
for ; path != "."; path = filepath.Dir(path) {
bag[path] = true
}
}
// completer is special :-/
if path := normPath(app.Completer); path != "" {
needsr[path] = true
for path = filepath.Dir(path); path != "."; path = filepath.Dir(path) {
needsrx[path] = true
}
}
}
// note all needsr so far need to be regular files (or symlinks)
for k := range needsr {
needsf[k] = true
}
// thing can get jumbled up
for path := range needsrx {
delete(needsx, path)
delete(needsr, path)
}
for path := range needsx {
if needsr[path] {
delete(needsx, path)
delete(needsr, path)
needsrx[path] = true
}
}
seen := make(map[string]bool, len(needsx)+len(needsrx)+len(needsr))
// bad modes are logged instead of being returned because the end user
// can do nothing with the info (and the developer can read the logs)
hasBadModes := false
err := c.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
mode := info.Mode()
if needsrx[path] || needsx[path] || needsr[path] {
seen[path] = true
}
if !needsrx[path] && !needsx[path] && !needsr[path] && !strings.HasPrefix(path, "meta/") {
if mode.IsDir() {
if noskipd[path] {
return nil
}
return filepath.SkipDir
}
return nil
}
if needsrx[path] || mode.IsDir() {
if mode.Perm()&0555 != 0555 {
logf("in snap %q: %q should be world-readable and executable, and isn't: %s", s.InstanceName(), path, mode)
hasBadModes = true
}
} else {
if needsf[path] {
// this assumes that if it's a symlink it's OK. Arguably we
// should instead follow the symlink. We'd have to expose
// Lstat(), and guard against loops, and ... huge can of
// worms, and as this validator is meant as a developer aid
// more than anything else, not worth it IMHO (as I can't
// imagine this happening by accident).
if mode&(os.ModeDir|os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 {
logf("in snap %q: %q should be a regular file (or a symlink) and isn't", s.InstanceName(), path)
hasBadModes = true
}
}
if needsx[path] || strings.HasPrefix(path, "meta/hooks/") {
if mode.Perm()&0111 == 0 {
logf("in snap %q: %q should be executable, and isn't: %s", s.InstanceName(), path, mode)
hasBadModes = true
}
} else {
// in needsr, or under meta but not a hook
if mode.Perm()&0444 != 0444 {
logf("in snap %q: %q should be world-readable, and isn't: %s", s.InstanceName(), path, mode)
hasBadModes = true
}
}
}
return nil
})
if err != nil {
return err
}
if len(seen) != len(needsx)+len(needsrx)+len(needsr) {
for _, needs := range []map[string]bool{needsx, needsrx, needsr} {
for path := range needs {
if !seen[path] {
logf("in snap %q: path %q does not exist", s.InstanceName(), path)
}
}
}
return ErrMissingPaths
}
if hasBadModes {
return ErrBadModes
}
return nil
}
// normPath is a helper for validateContainer. It takes a relative path (e.g. an
// app's RestartCommand, which might be empty to mean there is no such thing),
// and cleans it.
//
// * empty paths are returned as is
// * if the path is not relative, it's initial / is dropped
// * if the path goes "outside" (ie starts with ../), the empty string is
// returned (i.e. "ignore")
// * if there's a space in the command, ignore the rest of the string
// (see also cmd/snap-exec/main.go's comment about strings.Split)
func normPath(path string) string {
if path == "" {
return ""
}
path = strings.TrimPrefix(filepath.Clean(path), "/")
if strings.HasPrefix(path, "../") {
// not something inside the snap
return ""
}
if idx := strings.IndexByte(path, ' '); idx > -1 {
return path[:idx]
}
return path
}
snapd-2.37.4~14.04.1/snap/snapenv/ 0000775 0000000 0000000 00000000000 13435556260 013154 5 ustar snapd-2.37.4~14.04.1/snap/snapenv/snapenv_test.go 0000664 0000000 0000000 00000020013 13435556260 016210 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snapenv
import (
"fmt"
"os"
"os/user"
"strings"
"testing"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/arch"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil/sys"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/testutil"
)
func Test(t *testing.T) { TestingT(t) }
type HTestSuite struct {
testutil.BaseTest
}
var _ = Suite(&HTestSuite{})
var mockYaml = []byte(`name: snapname
version: 1.0
apps:
app:
command: run-app
hooks:
configure:
`)
var mockSnapInfo = &snap.Info{
SuggestedName: "foo",
Version: "1.0",
SideInfo: snap.SideInfo{
Revision: snap.R(17),
},
}
var mockClassicSnapInfo = &snap.Info{
SuggestedName: "foo",
Version: "1.0",
SideInfo: snap.SideInfo{
Revision: snap.R(17),
},
Confinement: snap.ClassicConfinement,
}
func (s *HTestSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
}
func (s *HTestSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
}
func (ts *HTestSuite) TestBasic(c *C) {
env := basicEnv(mockSnapInfo)
c.Assert(env, DeepEquals, map[string]string{
"SNAP": fmt.Sprintf("%s/foo/17", dirs.CoreSnapMountDir),
"SNAP_ARCH": arch.UbuntuArchitecture(),
"SNAP_COMMON": "/var/snap/foo/common",
"SNAP_DATA": "/var/snap/foo/17",
"SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void",
"SNAP_NAME": "foo",
"SNAP_INSTANCE_NAME": "foo",
"SNAP_INSTANCE_KEY": "",
"SNAP_REEXEC": "",
"SNAP_REVISION": "17",
"SNAP_VERSION": "1.0",
})
}
func (ts *HTestSuite) TestUser(c *C) {
env := userEnv(mockSnapInfo, "/root")
c.Assert(env, DeepEquals, map[string]string{
"HOME": "/root/snap/foo/17",
"SNAP_USER_COMMON": "/root/snap/foo/common",
"SNAP_USER_DATA": "/root/snap/foo/17",
"XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.foo", sys.Geteuid()),
})
}
func (ts *HTestSuite) TestUserForClassicConfinement(c *C) {
env := userEnv(mockClassicSnapInfo, "/root")
c.Assert(env, DeepEquals, map[string]string{
// NOTE HOME Is absent! we no longer override it
"SNAP_USER_COMMON": "/root/snap/foo/common",
"SNAP_USER_DATA": "/root/snap/foo/17",
"XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.foo", sys.Geteuid()),
})
}
func (s *HTestSuite) TestSnapRunSnapExecEnv(c *C) {
info, err := snap.InfoFromSnapYaml(mockYaml)
c.Assert(err, IsNil)
info.SideInfo.Revision = snap.R(42)
usr, err := user.Current()
c.Assert(err, IsNil)
homeEnv := os.Getenv("HOME")
defer os.Setenv("HOME", homeEnv)
for _, withHomeEnv := range []bool{true, false} {
if !withHomeEnv {
os.Setenv("HOME", "")
}
env := snapEnv(info)
c.Check(env, DeepEquals, map[string]string{
"SNAP_ARCH": arch.UbuntuArchitecture(),
"SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void",
"SNAP_NAME": "snapname",
"SNAP_INSTANCE_NAME": "snapname",
"SNAP_INSTANCE_KEY": "",
"SNAP_REEXEC": "",
"SNAP_REVISION": "42",
"SNAP_VERSION": "1.0",
"SNAP": fmt.Sprintf("%s/snapname/42", dirs.CoreSnapMountDir),
"SNAP_COMMON": "/var/snap/snapname/common",
"SNAP_DATA": "/var/snap/snapname/42",
"SNAP_USER_COMMON": fmt.Sprintf("%s/snap/snapname/common", usr.HomeDir),
"SNAP_USER_DATA": fmt.Sprintf("%s/snap/snapname/42", usr.HomeDir),
"XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.snapname", sys.Geteuid()),
"HOME": fmt.Sprintf("%s/snap/snapname/42", usr.HomeDir),
})
}
}
func (s *HTestSuite) TestParallelInstallSnapRunSnapExecEnv(c *C) {
info, err := snap.InfoFromSnapYaml(mockYaml)
c.Assert(err, IsNil)
info.SideInfo.Revision = snap.R(42)
usr, err := user.Current()
c.Assert(err, IsNil)
homeEnv := os.Getenv("HOME")
defer os.Setenv("HOME", homeEnv)
// pretend it's snapname_foo
info.InstanceKey = "foo"
for _, withHomeEnv := range []bool{true, false} {
if !withHomeEnv {
os.Setenv("HOME", "")
}
env := snapEnv(info)
c.Check(env, DeepEquals, map[string]string{
"SNAP_ARCH": arch.UbuntuArchitecture(),
"SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void",
"SNAP_NAME": "snapname",
"SNAP_INSTANCE_NAME": "snapname_foo",
"SNAP_INSTANCE_KEY": "foo",
"SNAP_REEXEC": "",
"SNAP_REVISION": "42",
"SNAP_VERSION": "1.0",
// Those are mapped to snap-specific directories by
// mount namespace setup
"SNAP": fmt.Sprintf("%s/snapname/42", dirs.CoreSnapMountDir),
"SNAP_COMMON": "/var/snap/snapname/common",
"SNAP_DATA": "/var/snap/snapname/42",
// User's data directories are not mapped to
// snap-specific ones
"SNAP_USER_COMMON": fmt.Sprintf("%s/snap/snapname_foo/common", usr.HomeDir),
"SNAP_USER_DATA": fmt.Sprintf("%s/snap/snapname_foo/42", usr.HomeDir),
"XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.snapname_foo", sys.Geteuid()),
"HOME": fmt.Sprintf("%s/snap/snapname_foo/42", usr.HomeDir),
})
}
}
func (ts *HTestSuite) TestParallelInstallUser(c *C) {
info := *mockSnapInfo
info.InstanceKey = "bar"
env := userEnv(&info, "/root")
c.Assert(env, DeepEquals, map[string]string{
"HOME": "/root/snap/foo_bar/17",
"SNAP_USER_COMMON": "/root/snap/foo_bar/common",
"SNAP_USER_DATA": "/root/snap/foo_bar/17",
"XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.foo_bar", sys.Geteuid()),
})
}
func (ts *HTestSuite) TestParallelInstallUserForClassicConfinement(c *C) {
info := *mockClassicSnapInfo
info.InstanceKey = "bar"
env := userEnv(&info, "/root")
c.Assert(env, DeepEquals, map[string]string{
// NOTE HOME Is absent! we no longer override it
"SNAP_USER_COMMON": "/root/snap/foo_bar/common",
"SNAP_USER_DATA": "/root/snap/foo_bar/17",
"XDG_RUNTIME_DIR": fmt.Sprintf("/run/user/%d/snap.foo_bar", sys.Geteuid()),
})
}
func envValue(env []string, key string) (bool, string) {
for _, item := range env {
if strings.HasPrefix(item, key+"=") {
return true, strings.SplitN(item, "=", 2)[1]
}
}
return false, ""
}
func (s *HTestSuite) TestExtraEnvForExecEnv(c *C) {
info, err := snap.InfoFromSnapYaml(mockYaml)
c.Assert(err, IsNil)
info.SideInfo.Revision = snap.R(42)
env := ExecEnv(info, map[string]string{"FOO": "BAR"})
found, val := envValue(env, "FOO")
c.Assert(found, Equals, true)
c.Assert(val, Equals, "BAR")
}
func setenvWithReset(s *HTestSuite, key string, val string) {
tmpdirEnv, tmpdirFound := os.LookupEnv("TMPDIR")
os.Setenv("TMPDIR", "/var/tmp")
if tmpdirFound {
s.AddCleanup(func() { os.Setenv("TMPDIR", tmpdirEnv) })
} else {
s.AddCleanup(func() { os.Unsetenv("TMPDIR") })
}
}
func (s *HTestSuite) TestExecEnvNoRenameTMPDIRForNonClassic(c *C) {
setenvWithReset(s, "TMPDIR", "/var/tmp")
env := ExecEnv(mockSnapInfo, map[string]string{})
found, val := envValue(env, "TMPDIR")
c.Assert(found, Equals, true)
c.Assert(val, Equals, "/var/tmp")
found, _ = envValue(env, PreservedUnsafePrefix+"TMPDIR")
c.Assert(found, Equals, false)
}
func (s *HTestSuite) TestExecEnvRenameTMPDIRForClassic(c *C) {
setenvWithReset(s, "TMPDIR", "/var/tmp")
env := ExecEnv(mockClassicSnapInfo, map[string]string{})
found, _ := envValue(env, "TMPDIR")
c.Assert(found, Equals, false)
found, val := envValue(env, PreservedUnsafePrefix+"TMPDIR")
c.Assert(found, Equals, true)
c.Assert(val, Equals, "/var/tmp")
}
snapd-2.37.4~14.04.1/snap/snapenv/snapenv.go 0000664 0000000 0000000 00000015720 13435556260 015162 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snapenv
import (
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"github.com/snapcore/snapd/arch"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil/sys"
"github.com/snapcore/snapd/snap"
)
type preserveUnsafeEnvFlag int8
const (
discardUnsafeFlag preserveUnsafeEnvFlag = iota
preserveUnsafeFlag
)
// ExecEnv returns the full environment that is required for
// snap-{confine,exec}(like SNAP_{NAME,REVISION} etc are all set).
//
// It merges it with the existing os.Environ() and ensures the SNAP_*
// overrides the any pre-existing environment variables. For a classic
// snap, environment variables that are usually stripped out by ld.so
// when starting a setuid process are renamed by prepending
// PreservedUnsafePrefix -- which snap-exec will remove, restoring the
// variables to their original names.
//
// With the extra parameter additional environment variables can be
// supplied which will be set in the execution environment.
func ExecEnv(info *snap.Info, extra map[string]string) []string {
// merge environment and the snap environment, note that the
// snap environment overrides pre-existing env entries
preserve := discardUnsafeFlag
if info.NeedsClassic() {
preserve = preserveUnsafeFlag
}
env := envMap(os.Environ(), preserve)
snapEnv := snapEnv(info)
for k, v := range snapEnv {
env[k] = v
}
for k, v := range extra {
env[k] = v
}
return envFromMap(env)
}
// snapEnv returns the extra environment that is required for
// snap-{confine,exec} to work.
func snapEnv(info *snap.Info) map[string]string {
var home string
usr, err := user.Current()
if err == nil {
home = usr.HomeDir
}
env := basicEnv(info)
if home != "" {
for k, v := range userEnv(info, home) {
env[k] = v
}
}
return env
}
// basicEnv returns the app-level environment variables for a snap.
// Despite this being a bit snap-specific, this is in helpers.go because it's
// used by so many other modules, we run into circular dependencies if it's
// somewhere more reasonable like the snappy module.
func basicEnv(info *snap.Info) map[string]string {
return map[string]string{
// This uses CoreSnapMountDir because the computed environment
// variables are conveyed to the started application process which
// shall *either* execute with the new mount namespace where snaps are
// always mounted on /snap OR it is a classically confined snap where
// /snap is a part of the distribution package.
//
// For parallel-installs the mount namespace setup is making the
// environment of each snap instance appear as if it's the only
// snap, i.e. SNAP paths point to the same locations within the
// mount namespace
"SNAP": filepath.Join(dirs.CoreSnapMountDir, info.SnapName(), info.Revision.String()),
"SNAP_COMMON": snap.CommonDataDir(info.SnapName()),
"SNAP_DATA": snap.DataDir(info.SnapName(), info.Revision),
"SNAP_NAME": info.SnapName(),
"SNAP_INSTANCE_NAME": info.InstanceName(),
"SNAP_INSTANCE_KEY": info.InstanceKey,
"SNAP_VERSION": info.Version,
"SNAP_REVISION": info.Revision.String(),
"SNAP_ARCH": arch.UbuntuArchitecture(),
// see https://github.com/snapcore/snapd/pull/2732#pullrequestreview-18827193
"SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void",
"SNAP_REEXEC": os.Getenv("SNAP_REEXEC"),
}
}
// userEnv returns the user-level environment variables for a snap.
// Despite this being a bit snap-specific, this is in helpers.go because it's
// used by so many other modules, we run into circular dependencies if it's
// somewhere more reasonable like the snappy module.
func userEnv(info *snap.Info, home string) map[string]string {
// To keep things simple the user variables always point to the
// instance-specific directories.
result := map[string]string{
"SNAP_USER_COMMON": info.UserCommonDataDir(home),
"SNAP_USER_DATA": info.UserDataDir(home),
"XDG_RUNTIME_DIR": info.UserXdgRuntimeDir(sys.Geteuid()),
}
// For non-classic snaps, we set HOME but on classic allow snaps to see real HOME
if !info.NeedsClassic() {
result["HOME"] = info.UserDataDir(home)
}
return result
}
// Environment variables glibc strips out when running a setuid binary.
// Taken from https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=sysdeps/generic/unsecvars.h;hb=HEAD
// TODO: use go generate to obtain this list at build time.
var unsafeEnv = map[string]bool{
"GCONV_PATH": true,
"GETCONF_DIR": true,
"GLIBC_TUNABLES": true,
"HOSTALIASES": true,
"LD_AUDIT": true,
"LD_DEBUG": true,
"LD_DEBUG_OUTPUT": true,
"LD_DYNAMIC_WEAK": true,
"LD_HWCAP_MASK": true,
"LD_LIBRARY_PATH": true,
"LD_ORIGIN_PATH": true,
"LD_PRELOAD": true,
"LD_PROFILE": true,
"LD_SHOW_AUXV": true,
"LD_USE_LOAD_BIAS": true,
"LOCALDOMAIN": true,
"LOCPATH": true,
"MALLOC_TRACE": true,
"NIS_PATH": true,
"NLSPATH": true,
"RESOLV_HOST_CONF": true,
"RES_OPTIONS": true,
"TMPDIR": true,
"TZDIR": true,
}
const PreservedUnsafePrefix = "SNAP_SAVED_"
// envMap creates a map from the given environment string list,
// e.g. the list returned from os.Environ(). If preserveUnsafeVars
// rename variables that will be stripped out by the dynamic linker
// executing the setuid snap-confine by prepending their names with
// PreservedUnsafePrefix.
func envMap(env []string, preserveUnsafeEnv preserveUnsafeEnvFlag) map[string]string {
envMap := map[string]string{}
for _, kv := range env {
// snap-exec unconditionally renames variables
// starting with PreservedUnsafePrefix so skip any
// that are already present in the environment to
// avoid confusion.
if strings.HasPrefix(kv, PreservedUnsafePrefix) {
continue
}
l := strings.SplitN(kv, "=", 2)
if len(l) < 2 {
continue // strange
}
k, v := l[0], l[1]
if preserveUnsafeEnv == preserveUnsafeFlag && unsafeEnv[k] {
k = PreservedUnsafePrefix + k
}
envMap[k] = v
}
return envMap
}
// envFromMap creates a list of strings of the form k=v from a dict. This is
// useful in combination with envMap to create an environment suitable to
// pass to e.g. syscall.Exec()
func envFromMap(em map[string]string) []string {
var out []string
for k, v := range em {
out = append(out, fmt.Sprintf("%s=%s", k, v))
}
return out
}
snapd-2.37.4~14.04.1/snap/types_test.go 0000664 0000000 0000000 00000014430 13435556260 014236 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"encoding/json"
"fmt"
"gopkg.in/yaml.v2"
. "gopkg.in/check.v1"
)
type typeSuite struct{}
var _ = Suite(&typeSuite{})
func (s *typeSuite) TestJSONerr(c *C) {
var t Type
err := json.Unmarshal([]byte("false"), &t)
c.Assert(err, NotNil)
}
func (s *typeSuite) TestJsonMarshalTypes(c *C) {
out, err := json.Marshal(TypeApp)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "\"app\"")
out, err = json.Marshal(TypeGadget)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "\"gadget\"")
out, err = json.Marshal(TypeOS)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "\"os\"")
out, err = json.Marshal(TypeKernel)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "\"kernel\"")
out, err = json.Marshal(TypeBase)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "\"base\"")
out, err = json.Marshal(TypeSnapd)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "\"snapd\"")
}
func (s *typeSuite) TestJsonUnmarshalTypes(c *C) {
var st Type
err := json.Unmarshal([]byte("\"application\""), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeApp)
err = json.Unmarshal([]byte("\"app\""), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeApp)
err = json.Unmarshal([]byte("\"gadget\""), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeGadget)
err = json.Unmarshal([]byte("\"os\""), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeOS)
err = json.Unmarshal([]byte("\"kernel\""), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeKernel)
err = json.Unmarshal([]byte("\"base\""), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeBase)
err = json.Unmarshal([]byte("\"snapd\""), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeSnapd)
}
func (s *typeSuite) TestJsonUnmarshalInvalidTypes(c *C) {
invalidTypes := []string{"foo", "-app", "gadget_"}
var st Type
for _, invalidType := range invalidTypes {
err := json.Unmarshal([]byte(fmt.Sprintf("%q", invalidType)), &st)
c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid type", invalidType))
}
}
func (s *typeSuite) TestYamlMarshalTypes(c *C) {
out, err := yaml.Marshal(TypeApp)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "app\n")
out, err = yaml.Marshal(TypeGadget)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "gadget\n")
out, err = yaml.Marshal(TypeOS)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "os\n")
out, err = yaml.Marshal(TypeKernel)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "kernel\n")
out, err = yaml.Marshal(TypeBase)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "base\n")
}
func (s *typeSuite) TestYamlUnmarshalTypes(c *C) {
var st Type
err := yaml.Unmarshal([]byte("application"), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeApp)
err = yaml.Unmarshal([]byte("app"), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeApp)
err = yaml.Unmarshal([]byte("gadget"), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeGadget)
err = yaml.Unmarshal([]byte("os"), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeOS)
err = yaml.Unmarshal([]byte("kernel"), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeKernel)
err = yaml.Unmarshal([]byte("base"), &st)
c.Assert(err, IsNil)
c.Check(st, Equals, TypeBase)
}
func (s *typeSuite) TestYamlUnmarshalInvalidTypes(c *C) {
invalidTypes := []string{"foo", "-app", "gadget_"}
var st Type
for _, invalidType := range invalidTypes {
err := yaml.Unmarshal([]byte(invalidType), &st)
c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid type", invalidType))
}
}
func (s *typeSuite) TestYamlMarshalConfinementTypes(c *C) {
out, err := yaml.Marshal(DevModeConfinement)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "devmode\n")
out, err = yaml.Marshal(StrictConfinement)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "strict\n")
}
func (s *typeSuite) TestYamlUnmarshalConfinementTypes(c *C) {
var confinementType ConfinementType
err := yaml.Unmarshal([]byte("devmode"), &confinementType)
c.Assert(err, IsNil)
c.Check(confinementType, Equals, DevModeConfinement)
err = yaml.Unmarshal([]byte("strict"), &confinementType)
c.Assert(err, IsNil)
c.Check(confinementType, Equals, StrictConfinement)
}
func (s *typeSuite) TestYamlUnmarshalInvalidConfinementTypes(c *C) {
var invalidConfinementTypes = []string{
"foo", "strict-", "_devmode",
}
var confinementType ConfinementType
for _, thisConfinementType := range invalidConfinementTypes {
err := yaml.Unmarshal([]byte(thisConfinementType), &confinementType)
c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid confinement type", thisConfinementType))
}
}
func (s *typeSuite) TestJsonMarshalConfinementTypes(c *C) {
out, err := json.Marshal(DevModeConfinement)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "\"devmode\"")
out, err = json.Marshal(StrictConfinement)
c.Assert(err, IsNil)
c.Check(string(out), Equals, "\"strict\"")
}
func (s *typeSuite) TestJsonUnmarshalConfinementTypes(c *C) {
var confinementType ConfinementType
err := json.Unmarshal([]byte("\"devmode\""), &confinementType)
c.Assert(err, IsNil)
c.Check(confinementType, Equals, DevModeConfinement)
err = json.Unmarshal([]byte("\"strict\""), &confinementType)
c.Assert(err, IsNil)
c.Check(confinementType, Equals, StrictConfinement)
}
func (s *typeSuite) TestJsonUnmarshalInvalidConfinementTypes(c *C) {
var invalidConfinementTypes = []string{
"foo", "strict-", "_devmode",
}
var confinementType ConfinementType
for _, thisConfinementType := range invalidConfinementTypes {
err := json.Unmarshal([]byte(fmt.Sprintf("%q", thisConfinementType)), &confinementType)
c.Assert(err, NotNil, Commentf("Expected '%s' to be an invalid confinement type", thisConfinementType))
}
}
snapd-2.37.4~14.04.1/snap/types.go 0000664 0000000 0000000 00000007214 13435556260 013201 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"encoding/json"
"fmt"
)
// Type represents the kind of snap (app, core, gadget, os, kernel)
type Type string
// The various types of snap parts we support
const (
TypeApp Type = "app"
TypeGadget Type = "gadget"
TypeKernel Type = "kernel"
TypeBase Type = "base"
TypeSnapd Type = "snapd"
// FIXME: this really should be TypeCore
TypeOS Type = "os"
)
// This is the sort order from least important to most important for
// types. On e.g. firstboot this will be used to order the snaps this
// way.
var typeOrder = map[Type]int{
TypeApp: 50,
TypeGadget: 40,
TypeBase: 30,
TypeKernel: 20,
TypeOS: 10,
TypeSnapd: 0,
}
func (m Type) SortsBefore(other Type) bool {
return typeOrder[m] < typeOrder[other]
}
// UnmarshalJSON sets *m to a copy of data.
func (m *Type) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
return m.fromString(str)
}
// UnmarshalYAML so Type implements yaml's Unmarshaler interface
func (m *Type) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err != nil {
return err
}
return m.fromString(str)
}
// fromString converts str to Type and sets *m to it if validations pass
func (m *Type) fromString(str string) error {
t := Type(str)
// this is a workaround as the store sends "application" but snappy uses
// "app" for TypeApp
if str == "application" {
t = TypeApp
}
if t != TypeApp && t != TypeGadget && t != TypeOS && t != TypeKernel && t != TypeBase && t != TypeSnapd {
return fmt.Errorf("invalid snap type: %q", str)
}
*m = t
return nil
}
// ConfinementType represents the kind of confinement supported by the snap
// (devmode only, or strict confinement)
type ConfinementType string
// The various confinement types we support
const (
DevModeConfinement ConfinementType = "devmode"
ClassicConfinement ConfinementType = "classic"
StrictConfinement ConfinementType = "strict"
)
// UnmarshalJSON sets *confinementType to a copy of data, assuming validation passes
func (confinementType *ConfinementType) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
return confinementType.fromString(s)
}
// UnmarshalYAML so ConfinementType implements yaml's Unmarshaler interface
func (confinementType *ConfinementType) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err
}
return confinementType.fromString(s)
}
func (confinementType *ConfinementType) fromString(str string) error {
c := ConfinementType(str)
if c != DevModeConfinement && c != ClassicConfinement && c != StrictConfinement {
return fmt.Errorf("invalid confinement type: %q", str)
}
*confinementType = c
return nil
}
type ServiceStopReason string
const (
StopReasonRefresh ServiceStopReason = "refresh"
StopReasonRemove ServiceStopReason = "remove"
StopReasonDisable ServiceStopReason = "disable"
)
snapd-2.37.4~14.04.1/snap/seed_yaml_test.go 0000664 0000000 0000000 00000003710 13435556260 015033 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
"io/ioutil"
"path/filepath"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/snap"
)
type seedYamlTestSuite struct{}
var _ = Suite(&seedYamlTestSuite{})
var mockSeedYaml = []byte(`
snaps:
- name: foo
snap-id: snapidsnapidsnapid
channel: stable
devmode: true
file: foo_1.0_all.snap
- name: local
unasserted: true
file: local.snap
`)
func (s *seedYamlTestSuite) TestSimple(c *C) {
fn := filepath.Join(c.MkDir(), "seed.yaml")
err := ioutil.WriteFile(fn, mockSeedYaml, 0644)
c.Assert(err, IsNil)
seed, err := snap.ReadSeedYaml(fn)
c.Assert(err, IsNil)
c.Assert(seed.Snaps, HasLen, 2)
c.Assert(seed.Snaps[0], DeepEquals, &snap.SeedSnap{
File: "foo_1.0_all.snap",
Name: "foo",
SnapID: "snapidsnapidsnapid",
Channel: "stable",
DevMode: true,
})
c.Assert(seed.Snaps[1], DeepEquals, &snap.SeedSnap{
File: "local.snap",
Name: "local",
Unasserted: true,
})
}
var badMockSeedYaml = []byte(`
snaps:
- name: foo
file: foo/bar.snap
`)
func (s *seedYamlTestSuite) TestNoPathAllowed(c *C) {
fn := filepath.Join(c.MkDir(), "seed.yaml")
err := ioutil.WriteFile(fn, badMockSeedYaml, 0644)
c.Assert(err, IsNil)
_, err = snap.ReadSeedYaml(fn)
c.Assert(err, ErrorMatches, `"foo/bar.snap" must be a filename, not a path`)
}
snapd-2.37.4~14.04.1/snap/epoch.go 0000664 0000000 0000000 00000022642 13435556260 013135 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"encoding/json"
"fmt"
"strconv"
"github.com/snapcore/snapd/logger"
)
// An Epoch represents the ability of the snap to read and write its data. Most
// developers need not worry about it, and snaps default to the 0th epoch, and
// users are only offered refreshes to epoch 0 snaps. Once an epoch bump is in
// order, there's a simplified expression they can use which should cover the
// majority of the cases:
//
// epoch: N
//
// means a snap can read/write exactly the Nth epoch's data, and
//
// epoch: N*
//
// means a snap can additionally read (N-1)th epoch's data, which means it's a
// snap that can migrate epochs (so a user on epoch 0 can get offered a refresh
// to a snap on epoch 1*).
//
// If the above is not enough, a developer can explicitly describe what epochs a
// snap can read and write:
//
// epoch:
// read: [1, 2, 3]
// write: [1, 3]
//
// the read attribute defaults to the value of the write attribute, and the
// write attribute defaults to the last item in the read attribute. If both are
// unset, it's the same as not specifying an epoch at all (i.e. epoch: 0). The
// lists must not have more than 10 elements, they must be strictly increasing,
// and there must be a non-empty intersection between them.
//
// Epoch numbers must be written in base 10, with no zero padding.
type Epoch struct {
Read []uint32 `yaml:"read"`
Write []uint32 `yaml:"write"`
}
// E returns the epoch represented by the expression s. It's meant for use in
// testing, as it panics at the first sign of trouble.
func E(s string) Epoch {
var e Epoch
if err := e.fromString(s); err != nil {
panic(fmt.Errorf("%q: %v", s, err))
}
return e
}
func (e *Epoch) fromString(s string) error {
if len(s) == 0 || s == "0" {
e.Read = []uint32{0}
e.Write = []uint32{0}
return nil
}
star := false
if s[len(s)-1] == '*' {
star = true
s = s[:len(s)-1]
}
n, err := parseInt(s)
if err != nil {
return err
}
if star {
if n == 0 {
return &EpochError{Message: epochZeroStar}
}
e.Read = []uint32{n - 1, n}
} else {
e.Read = []uint32{n}
}
e.Write = []uint32{n}
return nil
}
func (e *Epoch) fromStructured(structured structuredEpoch) error {
if structured.Read == nil {
if structured.Write == nil {
structured.Write = []uint32{0}
}
structured.Read = structured.Write
} else if len(structured.Read) == 0 {
// this means they explicitly set it to []. Bad they!
return &EpochError{Message: emptyEpochList}
}
if structured.Write == nil {
structured.Write = structured.Read[len(structured.Read)-1:]
} else if len(structured.Write) == 0 {
return &EpochError{Message: emptyEpochList}
}
p := &Epoch{Read: structured.Read, Write: structured.Write}
if err := p.Validate(); err != nil {
return err
}
*e = *p
return nil
}
func (e *Epoch) UnmarshalJSON(bs []byte) error {
return e.UnmarshalYAML(func(v interface{}) error {
return json.Unmarshal(bs, &v)
})
}
func (e *Epoch) UnmarshalYAML(unmarshal func(interface{}) error) error {
var shortEpoch string
if err := unmarshal(&shortEpoch); err == nil {
return e.fromString(shortEpoch)
}
var structured structuredEpoch
if err := unmarshal(&structured); err != nil {
return err
}
return e.fromStructured(structured)
}
// IsZero checks whether a snap's epoch is not set (or is set to the default
// value of "0"). Also zero are some epochs that would be normalized to "0",
// such as {"read": 0}, as well as some invalid ones like {"read": []}.
func (e *Epoch) IsZero() bool {
if e == nil {
return true
}
rZero := len(e.Read) == 0 || (len(e.Read) == 1 && e.Read[0] == 0)
wZero := len(e.Write) == 0 || (len(e.Write) == 1 && e.Write[0] == 0)
return rZero && wZero
}
func epochListEq(a, b []uint32) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func (e *Epoch) Equal(other *Epoch) bool {
if e.IsZero() {
return other.IsZero()
}
return epochListEq(e.Read, other.Read) && epochListEq(e.Write, other.Write)
}
// Validate checks that the epoch makes sense.
func (e *Epoch) Validate() error {
if (e.Read != nil && len(e.Read) == 0) || (e.Write != nil && len(e.Write) == 0) {
// these are invalid, but if both are true then IsZero will be true.
// In practice this check is redundant because it's caught in deserialise.
// Belts-and-suspenders all the way down.
return &EpochError{Message: emptyEpochList}
}
if e.IsZero() {
return nil
}
if len(e.Read) > 10 || len(e.Write) > 10 {
return &EpochError{Message: epochListJustRidiculouslyLong}
}
if !isIncreasing(e.Read) || !isIncreasing(e.Write) {
return &EpochError{Message: epochListNotIncreasing}
}
if intersect(e.Read, e.Write) {
return nil
}
return &EpochError{Message: noEpochIntersection}
}
func (e *Epoch) simplify() interface{} {
if e.IsZero() {
return "0"
}
if len(e.Write) == 1 && len(e.Read) == 1 && e.Read[0] == e.Write[0] {
return strconv.FormatUint(uint64(e.Read[0]), 10)
}
if len(e.Write) == 1 && len(e.Read) == 2 && e.Read[0]+1 == e.Read[1] && e.Read[1] == e.Write[0] {
return strconv.FormatUint(uint64(e.Read[1]), 10) + "*"
}
return &structuredEpoch{Read: e.Read, Write: e.Write}
}
func (e Epoch) MarshalJSON() ([]byte, error) {
se := &structuredEpoch{Read: e.Read, Write: e.Write}
if len(se.Read) == 0 {
se.Read = uint32slice{0}
}
if len(se.Write) == 0 {
se.Write = uint32slice{0}
}
return json.Marshal(se)
}
func (Epoch) MarshalYAML() (interface{}, error) {
panic("unexpected attempt to marshal an Epoch to YAML")
}
func (e Epoch) String() string {
i := e.simplify()
if s, ok := i.(string); ok {
return s
}
buf, err := json.Marshal(i)
if err != nil {
// can this happen?
logger.Noticef("trying to marshal %#v, simplified to %#v, got %v", e, i, err)
return "-1"
}
return string(buf)
}
// CanRead checks whether this epoch can read the data written by the
// other one.
func (e *Epoch) CanRead(other Epoch) bool {
// the intersection between e.Read and other.Write needs to be non-empty
// normalize (empty epoch should be treated like "0" here)
var rs, ws []uint32
if e != nil {
rs = e.Read
}
ws = other.Write
if len(rs) == 0 {
rs = []uint32{0}
}
if len(ws) == 0 {
ws = []uint32{0}
}
return intersect(rs, ws)
}
func intersect(rs, ws []uint32) bool {
// O(𝑚𝑛) instead of O(𝑚log𝑛) for the binary search we could do, but
// 𝑚 and 𝑛 < 10, so the simple solution is good enough (and if that
// alone makes you nervous, know that it is ~2× faster in the worst
// case; bisect starts being faster at ~50 entries).
for _, r := range rs {
for _, w := range ws {
if r == w {
return true
}
}
}
return false
}
// EpochError tracks the details of a failed epoch parse or validation.
type EpochError struct {
Message string
}
func (e EpochError) Error() string {
return e.Message
}
const (
epochZeroStar = "0* is an invalid epoch"
hugeEpochNumber = "epoch numbers must be less than 2³², but got %q"
badEpochNumber = "epoch numbers must be base 10 with no zero padding, but got %q"
badEpochList = "epoch read/write attributes must be lists of epoch numbers"
emptyEpochList = "epoch list cannot be explicitly empty"
epochListNotIncreasing = "epoch list must be a strictly increasing sequence"
epochListJustRidiculouslyLong = "epoch list must not have more than 10 entries"
noEpochIntersection = "epoch read and write lists must have a non-empty intersection"
)
func parseInt(s string) (uint32, error) {
if !(len(s) > 1 && s[0] == '0') {
u, err := strconv.ParseUint(s, 10, 32)
if err == nil {
return uint32(u), nil
}
if e, ok := err.(*strconv.NumError); ok {
if e.Err == strconv.ErrRange {
return 0, &EpochError{
Message: fmt.Sprintf(hugeEpochNumber, s),
}
}
}
}
return 0, &EpochError{
Message: fmt.Sprintf(badEpochNumber, s),
}
}
type uint32slice []uint32
func (z *uint32slice) UnmarshalYAML(unmarshal func(interface{}) error) error {
var ss []string
if err := unmarshal(&ss); err != nil {
return &EpochError{Message: badEpochList}
}
x := make([]uint32, len(ss))
for i, s := range ss {
n, err := parseInt(s)
if err != nil {
return err
}
x[i] = n
}
*z = x
return nil
}
func (z *uint32slice) UnmarshalJSON(bs []byte) error {
var ss []json.RawMessage
if err := json.Unmarshal(bs, &ss); err != nil {
return &EpochError{Message: badEpochList}
}
x := make([]uint32, len(ss))
for i, s := range ss {
n, err := parseInt(string(s))
if err != nil {
return err
}
x[i] = n
}
*z = x
return nil
}
func isIncreasing(z []uint32) bool {
if len(z) < 2 {
return true
}
for i := range z[1:] {
if z[i] >= z[i+1] {
return false
}
}
return true
}
type structuredEpoch struct {
Read uint32slice `json:"read"`
Write uint32slice `json:"write"`
}
snapd-2.37.4~14.04.1/snap/implicit_test.go 0000664 0000000 0000000 00000001431 13435556260 014701 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016-2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
. "gopkg.in/check.v1"
)
type implicitSuite struct{}
var _ = Suite(&implicitSuite{})
snapd-2.37.4~14.04.1/snap/revision.go 0000664 0000000 0000000 00000005454 13435556260 013677 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"fmt"
"strconv"
)
// Keep this in sync between snap and client packages.
type Revision struct {
N int
}
func (r Revision) String() string {
if r.N == 0 {
return "unset"
}
if r.N < 0 {
return fmt.Sprintf("x%d", -r.N)
}
return strconv.Itoa(int(r.N))
}
func (r Revision) Unset() bool {
return r.N == 0
}
func (r Revision) Local() bool {
return r.N < 0
}
func (r Revision) Store() bool {
return r.N > 0
}
func (r Revision) MarshalJSON() ([]byte, error) {
return []byte(`"` + r.String() + `"`), nil
}
func (r *Revision) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err
}
return r.UnmarshalJSON([]byte(`"` + s + `"`))
}
func (r Revision) MarshalYAML() (interface{}, error) {
return r.String(), nil
}
func (r *Revision) UnmarshalJSON(data []byte) error {
if len(data) > 0 && data[0] == '"' && data[len(data)-1] == '"' {
parsed, err := ParseRevision(string(data[1 : len(data)-1]))
if err == nil {
*r = parsed
return nil
}
} else {
n, err := strconv.ParseInt(string(data), 10, 64)
if err == nil {
r.N = int(n)
return nil
}
}
return fmt.Errorf("invalid snap revision: %q", data)
}
// ParseRevisions returns the representation in r as a revision.
// See R for a function more suitable for hardcoded revisions.
func ParseRevision(s string) (Revision, error) {
if s == "unset" {
return Revision{}, nil
}
if s != "" && s[0] == 'x' {
i, err := strconv.Atoi(s[1:])
if err == nil && i > 0 {
return Revision{-i}, nil
}
}
i, err := strconv.Atoi(s)
if err == nil && i > 0 {
return Revision{i}, nil
}
return Revision{}, fmt.Errorf("invalid snap revision: %#v", s)
}
// R returns a Revision given an int or a string.
// Providing an invalid revision type or value causes a runtime panic.
// See ParseRevision for a polite function that does not panic.
func R(r interface{}) Revision {
switch r := r.(type) {
case string:
revision, err := ParseRevision(r)
if err != nil {
panic(err)
}
return revision
case int:
return Revision{r}
default:
panic(fmt.Errorf("cannot use %v (%T) as a snap revision", r, r))
}
}
snapd-2.37.4~14.04.1/snap/broken_test.go 0000664 0000000 0000000 00000010757 13435556260 014362 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
"io/ioutil"
"os"
"path/filepath"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
)
type brokenSuite struct{}
var _ = Suite(&brokenSuite{})
func (s *brokenSuite) SetUpTest(c *C) {
dirs.SetRootDir(c.MkDir())
}
func (s *brokenSuite) TearDownTest(c *C) {
dirs.SetRootDir("")
}
func touch(c *C, path string) {
err := os.MkdirAll(filepath.Dir(path), 0755)
c.Assert(err, IsNil)
err = ioutil.WriteFile(path, nil, 0644)
c.Assert(err, IsNil)
}
func (s *brokenSuite) TestGuessAppsForBrokenBinaries(c *C) {
touch(c, filepath.Join(dirs.SnapBinariesDir, "foo"))
touch(c, filepath.Join(dirs.SnapBinariesDir, "foo.bar"))
touch(c, filepath.Join(dirs.SnapBinariesDir, "foo_instance"))
touch(c, filepath.Join(dirs.SnapBinariesDir, "foo_instance.baz"))
info := &snap.Info{SuggestedName: "foo"}
apps := snap.GuessAppsForBroken(info)
c.Check(apps, HasLen, 2)
c.Check(apps["foo"], DeepEquals, &snap.AppInfo{Snap: info, Name: "foo"})
c.Check(apps["bar"], DeepEquals, &snap.AppInfo{Snap: info, Name: "bar"})
info = &snap.Info{SuggestedName: "foo", InstanceKey: "instance"}
apps = snap.GuessAppsForBroken(info)
c.Check(apps, HasLen, 2)
c.Check(apps["foo"], DeepEquals, &snap.AppInfo{Snap: info, Name: "foo"})
c.Check(apps["baz"], DeepEquals, &snap.AppInfo{Snap: info, Name: "baz"})
}
func (s *brokenSuite) TestGuessAppsForBrokenServices(c *C) {
touch(c, filepath.Join(dirs.SnapServicesDir, "snap.foo.foo.service"))
touch(c, filepath.Join(dirs.SnapServicesDir, "snap.foo.bar.service"))
touch(c, filepath.Join(dirs.SnapServicesDir, "snap.foo_instance.foo.service"))
touch(c, filepath.Join(dirs.SnapServicesDir, "snap.foo_instance.baz.service"))
info := &snap.Info{SuggestedName: "foo"}
apps := snap.GuessAppsForBroken(info)
c.Check(apps, HasLen, 2)
c.Check(apps["foo"], DeepEquals, &snap.AppInfo{Snap: info, Name: "foo", Daemon: "simple"})
c.Check(apps["bar"], DeepEquals, &snap.AppInfo{Snap: info, Name: "bar", Daemon: "simple"})
info = &snap.Info{SuggestedName: "foo", InstanceKey: "instance"}
apps = snap.GuessAppsForBroken(info)
c.Check(apps, HasLen, 2)
c.Check(apps["foo"], DeepEquals, &snap.AppInfo{Snap: info, Name: "foo", Daemon: "simple"})
c.Check(apps["baz"], DeepEquals, &snap.AppInfo{Snap: info, Name: "baz", Daemon: "simple"})
}
func (s *brokenSuite) TestForceRenamePlug(c *C) {
snapInfo := snaptest.MockInvalidInfo(c, `name: core
version: 0
plugs:
old:
interface: iface
slots:
old:
interface: iface
apps:
app:
hooks:
configure:
`, nil)
c.Assert(snapInfo.Plugs["old"], Not(IsNil))
c.Assert(snapInfo.Plugs["old"].Name, Equals, "old")
c.Assert(snapInfo.Slots["old"], Not(IsNil))
c.Assert(snapInfo.Slots["old"].Name, Equals, "old")
c.Assert(snapInfo.Apps["app"].Plugs["old"], DeepEquals, snapInfo.Plugs["old"])
c.Assert(snapInfo.Apps["app"].Slots["old"], DeepEquals, snapInfo.Slots["old"])
c.Assert(snapInfo.Hooks["configure"].Plugs["old"], DeepEquals, snapInfo.Plugs["old"])
// Rename the plug now.
snapInfo.ForceRenamePlug("old", "new")
// Check that there's no trace of the old plug name.
c.Assert(snapInfo.Plugs["old"], IsNil)
c.Assert(snapInfo.Plugs["new"], Not(IsNil))
c.Assert(snapInfo.Plugs["new"].Name, Equals, "new")
c.Assert(snapInfo.Apps["app"].Plugs["old"], IsNil)
c.Assert(snapInfo.Apps["app"].Plugs["new"], DeepEquals, snapInfo.Plugs["new"])
c.Assert(snapInfo.Hooks["configure"].Plugs["old"], IsNil)
c.Assert(snapInfo.Hooks["configure"].Plugs["new"], DeepEquals, snapInfo.Plugs["new"])
// Check that slots with the old name are unaffected.
c.Assert(snapInfo.Slots["old"], Not(IsNil))
c.Assert(snapInfo.Slots["old"].Name, Equals, "old")
c.Assert(snapInfo.Apps["app"].Slots["old"], DeepEquals, snapInfo.Slots["old"])
// Check that the rename made the snap valid now
c.Assert(snap.Validate(snapInfo), IsNil)
}
snapd-2.37.4~14.04.1/snap/squashfs/ 0000775 0000000 0000000 00000000000 13435556260 013337 5 ustar snapd-2.37.4~14.04.1/snap/squashfs/squashfs.go 0000664 0000000 0000000 00000023442 13435556260 015530 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package squashfs
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"time"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/strutil"
)
// Magic is the magic prefix of squashfs snap files.
var Magic = []byte{'h', 's', 'q', 's'}
// Snap is the squashfs based snap.
type Snap struct {
path string
}
// Path returns the path of the backing file.
func (s *Snap) Path() string {
return s.path
}
// New returns a new Squashfs snap.
func New(snapPath string) *Snap {
return &Snap{path: snapPath}
}
var osLink = os.Link
var osutilCommandFromCore = osutil.CommandFromCore
func (s *Snap) Install(targetPath, mountDir string) error {
// ensure mount-point and blob target dir.
for _, dir := range []string{mountDir, filepath.Dir(targetPath)} {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
}
// This is required so that the tests can simulate a mounted
// snap when we "install" a squashfs snap in the tests.
// We can not mount it for real in the tests, so we just unpack
// it to the location which is good enough for the tests.
if osutil.GetenvBool("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS") {
if err := s.Unpack("*", mountDir); err != nil {
return err
}
}
// nothing to do, happens on e.g. first-boot when we already
// booted with the OS snap but its also in the seed.yaml
if s.path == targetPath || osutil.FilesAreEqual(s.path, targetPath) {
return nil
}
// try to (hard)link the file, but go on to trying to copy it
// if it fails for whatever reason
//
// link(2) returns EPERM on filesystems that don't support
// hard links (like vfat), so checking the error here doesn't
// make sense vs just trying to copy it.
if err := osLink(s.path, targetPath); err == nil {
return nil
}
// if the file is a seed, but the hardlink failed, symlinking it
// saves the copy (which in livecd is expensive) so try that next
if filepath.Dir(s.path) == dirs.SnapSeedDir && os.Symlink(s.path, targetPath) == nil {
return nil
}
return osutil.CopyFile(s.path, targetPath, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync)
}
// unsquashfsStderrWriter is a helper that captures errors from
// unsquashfs on stderr. Because unsquashfs will potentially
// (e.g. on out-of-diskspace) report an error on every single
// file we limit the reported error lines to 4.
//
// unsquashfs does not exit with an exit code for write errors
// (e.g. no space left on device). There is an upstream PR
// to fix this https://github.com/plougher/squashfs-tools/pull/46
//
// However in the meantime we can detect errors by looking
// on stderr for "failed" which is pretty consistently used in
// the unsquashfs.c source in case of errors.
type unsquashfsStderrWriter struct {
strutil.MatchCounter
}
var unsquashfsStderrRegexp = regexp.MustCompile(`(?m).*\b[Ff]ailed\b.*`)
func newUnsquashfsStderrWriter() *unsquashfsStderrWriter {
return &unsquashfsStderrWriter{strutil.MatchCounter{
Regexp: unsquashfsStderrRegexp,
N: 4, // note Err below uses this value
}}
}
func (u *unsquashfsStderrWriter) Err() error {
// here we use that our N is 4.
errors, count := u.Matches()
switch count {
case 0:
return nil
case 1:
return fmt.Errorf("failed: %q", errors[0])
case 2, 3, 4:
return fmt.Errorf("failed: %s, and %q", strutil.Quoted(errors[:len(errors)-1]), errors[len(errors)-1])
default:
// count > len(matches)
extra := count - len(errors)
return fmt.Errorf("failed: %s, and %d more", strutil.Quoted(errors), extra)
}
}
func (s *Snap) Unpack(src, dstDir string) error {
usw := newUnsquashfsStderrWriter()
cmd := exec.Command("unsquashfs", "-n", "-f", "-d", dstDir, s.path, src)
cmd.Stderr = usw
if err := cmd.Run(); err != nil {
return err
}
if usw.Err() != nil {
return fmt.Errorf("cannot extract %q to %q: %v", src, dstDir, usw.Err())
}
return nil
}
// Size returns the size of a squashfs snap.
func (s *Snap) Size() (size int64, err error) {
st, err := os.Stat(s.path)
if err != nil {
return 0, err
}
return st.Size(), nil
}
// ReadFile returns the content of a single file inside a squashfs snap.
func (s *Snap) ReadFile(filePath string) (content []byte, err error) {
tmpdir, err := ioutil.TempDir("", "read-file")
if err != nil {
return nil, err
}
defer os.RemoveAll(tmpdir)
unpackDir := filepath.Join(tmpdir, "unpack")
if err := exec.Command("unsquashfs", "-n", "-i", "-d", unpackDir, s.path, filePath).Run(); err != nil {
return nil, err
}
return ioutil.ReadFile(filepath.Join(unpackDir, filePath))
}
// skipper is used to track directories that should be skipped
//
// Given sk := make(skipper), if you sk.Add("foo/bar"), then
// sk.Has("foo/bar") is true, but also sk.Has("foo/bar/baz")
//
// It could also be a map[string]bool, but because it's only supposed
// to be checked through its Has method as above, the small added
// complexity of it being a map[string]struct{} lose to the associated
// space savings.
type skipper map[string]struct{}
func (sk skipper) Add(path string) {
sk[filepath.Clean(path)] = struct{}{}
}
func (sk skipper) Has(path string) bool {
for p := filepath.Clean(path); p != "." && p != "/"; p = filepath.Dir(p) {
if _, ok := sk[p]; ok {
return true
}
}
return false
}
// Walk (part of snap.Container) is like filepath.Walk, without the ordering guarantee.
func (s *Snap) Walk(relative string, walkFn filepath.WalkFunc) error {
relative = filepath.Clean(relative)
if relative == "" || relative == "/" {
relative = "."
} else if relative[0] == '/' {
// I said relative, darn it :-)
relative = relative[1:]
}
var cmd *exec.Cmd
if relative == "." {
cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path)
} else {
cmd = exec.Command("unsquashfs", "-no-progress", "-dest", ".", "-ll", s.path, relative)
}
cmd.Env = []string{"TZ=UTC"}
stdout, err := cmd.StdoutPipe()
if err != nil {
return walkFn(relative, nil, err)
}
if err := cmd.Start(); err != nil {
return walkFn(relative, nil, err)
}
defer cmd.Process.Kill()
scanner := bufio.NewScanner(stdout)
// skip the header
for scanner.Scan() {
if len(scanner.Bytes()) == 0 {
break
}
}
skipper := make(skipper)
for scanner.Scan() {
st, err := fromRaw(scanner.Bytes())
if err != nil {
err = walkFn(relative, nil, err)
if err != nil {
return err
}
} else {
path := filepath.Join(relative, st.Path())
if skipper.Has(path) {
continue
}
err = walkFn(path, st, nil)
if err != nil {
if err == filepath.SkipDir && st.IsDir() {
skipper.Add(path)
} else {
return err
}
}
}
}
if err := scanner.Err(); err != nil {
return walkFn(relative, nil, err)
}
if err := cmd.Wait(); err != nil {
return walkFn(relative, nil, err)
}
return nil
}
// ListDir returns the content of a single directory inside a squashfs snap.
func (s *Snap) ListDir(dirPath string) ([]string, error) {
output, err := exec.Command(
"unsquashfs", "-no-progress", "-dest", "_", "-l", s.path, dirPath).CombinedOutput()
if err != nil {
return nil, osutil.OutputErr(output, err)
}
prefixPath := path.Join("_", dirPath)
pattern, err := regexp.Compile("(?m)^" + regexp.QuoteMeta(prefixPath) + "/([^/\r\n]+)$")
if err != nil {
return nil, fmt.Errorf("internal error: cannot compile squashfs list dir regexp for %q: %s", dirPath, err)
}
var directoryContents []string
for _, groups := range pattern.FindAllSubmatch(output, -1) {
if len(groups) > 1 {
directoryContents = append(directoryContents, string(groups[1]))
}
}
return directoryContents, nil
}
// Build builds the snap.
func (s *Snap) Build(sourceDir, snapType string, excludeFiles ...string) error {
fullSnapPath, err := filepath.Abs(s.path)
if err != nil {
return err
}
cmd, err := osutilCommandFromCore(dirs.SnapMountDir, "/usr/bin/mksquashfs")
if err != nil {
cmd = exec.Command("mksquashfs")
}
cmd.Args = append(cmd.Args,
".", fullSnapPath,
"-noappend",
"-comp", "xz",
"-no-fragments",
"-no-progress",
)
if len(excludeFiles) > 0 {
cmd.Args = append(cmd.Args, "-wildcards")
for _, excludeFile := range excludeFiles {
cmd.Args = append(cmd.Args, "-ef", excludeFile)
}
}
if snapType != "os" && snapType != "core" && snapType != "base" {
cmd.Args = append(cmd.Args, "-all-root", "-no-xattrs")
}
return osutil.ChDir(sourceDir, func() error {
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("mksquashfs call failed: %s", osutil.OutputErr(output, err))
}
return nil
})
}
// BuildDate returns the "Creation or last append time" as reported by unsquashfs.
func (s *Snap) BuildDate() time.Time {
return BuildDate(s.path)
}
// BuildDate returns the "Creation or last append time" as reported by unsquashfs.
func BuildDate(path string) time.Time {
var t0 time.Time
const prefix = "Creation or last append time "
m := &strutil.MatchCounter{
Regexp: regexp.MustCompile("(?m)^" + prefix + ".*$"),
N: 1,
}
cmd := exec.Command("unsquashfs", "-n", "-s", path)
cmd.Env = []string{"TZ=UTC"}
cmd.Stdout = m
cmd.Stderr = m
if err := cmd.Run(); err != nil {
return t0
}
matches, count := m.Matches()
if count != 1 {
return t0
}
t0, _ = time.Parse(time.ANSIC, matches[0][len(prefix):])
return t0
}
snapd-2.37.4~14.04.1/snap/squashfs/stat_test.go 0000664 0000000 0000000 00000023624 13435556260 015707 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017-2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package squashfs_test
import (
"fmt"
"math"
"os"
"time"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/snap/squashfs"
)
func (s *SquashfsTestSuite) TestStatBadNodes(c *C) {
badlines := map[string][]string{
"node": {
// size, but device
"brwxrwxr-x u/u 53595 2017-12-08 11:19 .",
"crwxrwxr-x u/u 53595 2017-12-08 11:19 .",
// node info is noise
"brwxrwxr-x u/u noise 2017-12-08 11:19 .",
"crwxrwxr-x u/u noise 2017-12-08 11:19 .",
// major is noise
"brwxrwxr-x u/u noise, 1 2017-12-08 11:19 .",
"crwxrwxr-x u/u noise, 1 2017-12-08 11:19 .",
// minor is noise
"brwxrwxr-x u/u 1, noise 2017-12-08 11:19 .",
"crwxrwxr-x u/u 1, noise 2017-12-08 11:19 .",
},
"size": {
// size is noise
"drwxrwxr-x u/g noise 2017-12-08 11:19 .",
"drwxrwxr-x u/g 1noise 2017-12-08 11:19 .",
// size too big
"drwxrwxr-x u/g 36893488147419103232 2017-12-08 11:19 .",
},
"line": {
// shorter than the minimum:
"-rw-r--r-- too/short 20 2017-12-08 11:19 ./",
// truncated:
"drwxrwxr-x",
"drwxrwxr-x ",
"drwxrwxr-x u/u",
"drwxrwxr-x u/g ",
"drwxrwxr-x u/g 53595",
"drwxrwxr-x u/g 53595 ",
"drwxrwxr-x u/g 53595 2017-12-08 11:19",
"drwxrwxr-x u/g 53595 2017-12-08 11:19 ",
// mode keeps on going
"drwxrwxr-xr-x u/g 53595 2017-12-08 11:19 .",
// spurious padding:
"drwxrwxr-x u/u 53595 2017-12-08 11:19 .",
// missing size
"drwxrwxr-x u/u 2017-12-08 11:19 ",
// everything is size
"drwxrwxr-x u/u 111111111111111111111111111111111111",
},
"mode": {
// zombie file type?
"zrwxrwxr-x u/g 53595 2017-12-08 11:19 .",
// strange permissions
"dbwxrwxr-x u/g 53595 2017-12-08 11:19 .",
"drbxrwxr-x u/g 53595 2017-12-08 11:19 .",
"drwbrwxr-x u/g 53595 2017-12-08 11:19 .",
"drwxbwxr-x u/g 53595 2017-12-08 11:19 .",
"drwxrbxr-x u/g 53595 2017-12-08 11:19 .",
"drwxrwbr-x u/g 53595 2017-12-08 11:19 .",
"drwxrwxb-x u/g 53595 2017-12-08 11:19 .",
"drwxrwxrbx u/g 53595 2017-12-08 11:19 .",
"drwxrwxr-b u/g 53595 2017-12-08 11:19 .",
},
"owner": {
"-rw-r--r-- some.user.with.a.much.too.long.name/some.group.with.a.much.too.long.name 20 2017-12-08 11:19 ./foo",
"-rw-r--r-- nogroup/ 20 2017-12-08 11:19 ./foo",
"-rw-r--r-- noslash 20 2017-12-08 11:19 ./foo",
"-rw-r--r-- this.line.finishes.before.finishing.owner",
},
"time": {
// time is bonkers:
"drwxrwxr-x u/u 53595 2017-bonkers-what .",
},
"path": {
// path doesn't start with "."
"drwxrwxr-x u/g 53595 2017-12-08 11:19 foo",
},
}
for kind, lines := range badlines {
for _, line := range lines {
com := Commentf("%q (expected bad %s)", line, kind)
st, err := squashfs.FromRaw([]byte(line))
c.Assert(err, NotNil, com)
c.Check(st, IsNil, com)
c.Check(err, ErrorMatches, fmt.Sprintf("cannot parse %s: .*", kind))
}
}
}
func (s *SquashfsTestSuite) TestStatUserGroup(c *C) {
usergroups := [][2]string{
{"u", "g"},
{"user", "group"},
{"some.user.with.a.veery.long.name", "group"},
{"user", "some.group.with.a.very.long.name"},
{"some.user.with.a.veery.long.name", "some.group.with.a.very.long.name"},
}
for _, ug := range usergroups {
user, group := ug[0], ug[1]
raw := []byte(fmt.Sprintf("-rw-r--r-- %s/%s 20 2017-12-08 11:19 ./foo", user, group))
com := Commentf("%q", raw)
c.Assert(len(user) <= 32, Equals, true, com)
c.Assert(len(group) <= 32, Equals, true, com)
st, err := squashfs.FromRaw(raw)
c.Assert(err, IsNil, com)
c.Check(st.Mode(), Equals, os.FileMode(0644), com)
c.Check(st.Path(), Equals, "/foo", com)
c.Check(st.User(), Equals, user, com)
c.Check(st.Group(), Equals, group, com)
c.Check(st.Size(), Equals, int64(20), com)
c.Check(st.ModTime(), Equals, time.Date(2017, 12, 8, 11, 19, 0, 0, time.UTC), com)
}
}
func (s *SquashfsTestSuite) TestStatPath(c *C) {
paths := [][]byte{
[]byte("hello"),
[]byte(" this is/ a path/(somehow)"),
{239, 191, 190},
{0355, 0240, 0200, 0355, 0260, 0200},
}
for _, path := range paths {
raw := []byte(fmt.Sprintf("-rw-r--r-- user/group 20 2017-12-08 11:19 ./%s", path))
com := Commentf("%q", raw)
st, err := squashfs.FromRaw(raw)
c.Assert(err, IsNil, com)
c.Check(st.Mode(), Equals, os.FileMode(0644), com)
c.Check(st.Path(), Equals, fmt.Sprintf("/%s", path), com)
c.Check(st.User(), Equals, "user", com)
c.Check(st.Group(), Equals, "group", com)
c.Check(st.Size(), Equals, int64(20), com)
c.Check(st.ModTime(), Equals, time.Date(2017, 12, 8, 11, 19, 0, 0, time.UTC), com)
}
}
func (s *SquashfsTestSuite) TestStatBlock(c *C) {
line := "brw-rw---- root/disk 7, 0 2017-12-05 10:29 ./dev/loop0"
st, err := squashfs.FromRaw([]byte(line))
c.Assert(err, IsNil)
c.Check(st.Mode(), Equals, os.FileMode(0660|os.ModeDevice))
c.Check(st.Path(), Equals, "/dev/loop0")
c.Check(st.User(), Equals, "root")
c.Check(st.Group(), Equals, "disk")
c.Check(st.Size(), Equals, int64(0))
c.Check(st.ModTime(), Equals, time.Date(2017, 12, 5, 10, 29, 0, 0, time.UTC))
// note the major and minor numbers are ignored (for now)
}
func (s *SquashfsTestSuite) TestStatCharacter(c *C) {
line := "crw-rw---- root/audio 14, 3 2017-12-05 10:29 ./dev/dsp"
st, err := squashfs.FromRaw([]byte(line))
c.Assert(err, IsNil)
c.Check(st.Mode(), Equals, os.FileMode(0660|os.ModeCharDevice))
c.Check(st.Path(), Equals, "/dev/dsp")
c.Check(st.User(), Equals, "root")
c.Check(st.Group(), Equals, "audio")
c.Check(st.Size(), Equals, int64(0))
c.Check(st.ModTime(), Equals, time.Date(2017, 12, 5, 10, 29, 0, 0, time.UTC))
// note the major and minor numbers are ignored (for now)
}
func (s *SquashfsTestSuite) TestStatSymlink(c *C) {
line := "lrwxrwxrwx root/root 4 2017-12-05 10:29 ./var/run -> /run"
st, err := squashfs.FromRaw([]byte(line))
c.Assert(err, IsNil)
c.Check(st.Mode(), Equals, os.FileMode(0777|os.ModeSymlink))
c.Check(st.Path(), Equals, "/var/run")
c.Check(st.User(), Equals, "root")
c.Check(st.Group(), Equals, "root")
c.Check(st.Size(), Equals, int64(4))
c.Check(st.ModTime(), Equals, time.Date(2017, 12, 5, 10, 29, 0, 0, time.UTC))
}
func (s *SquashfsTestSuite) TestStatNamedPipe(c *C) {
line := "prw-rw-r-- john/john 0 2018-01-09 10:24 ./afifo"
st, err := squashfs.FromRaw([]byte(line))
c.Assert(err, IsNil)
c.Check(st.Mode(), Equals, os.FileMode(0664|os.ModeNamedPipe))
c.Check(st.Path(), Equals, "/afifo")
c.Check(st.User(), Equals, "john")
c.Check(st.Group(), Equals, "john")
c.Check(st.Size(), Equals, int64(0))
c.Check(st.ModTime(), Equals, time.Date(2018, 1, 9, 10, 24, 0, 0, time.UTC))
}
func (s *SquashfsTestSuite) TestStatSocket(c *C) {
line := "srwxrwxr-x john/john 0 2018-01-09 10:24 ./asock"
st, err := squashfs.FromRaw([]byte(line))
c.Assert(err, IsNil)
c.Check(st.Mode(), Equals, os.FileMode(0775|os.ModeSocket))
c.Check(st.Path(), Equals, "/asock")
c.Check(st.User(), Equals, "john")
c.Check(st.Group(), Equals, "john")
c.Check(st.Size(), Equals, int64(0))
c.Check(st.ModTime(), Equals, time.Date(2018, 1, 9, 10, 24, 0, 0, time.UTC))
}
func (s *SquashfsTestSuite) TestStatLength(c *C) {
ns := []int64{
0,
1024,
math.MaxInt32,
math.MaxInt64,
}
for _, n := range ns {
raw := []byte(fmt.Sprintf("-rw-r--r-- user/group %16d 2017-12-08 11:19 ./some filename", n))
com := Commentf("%q", raw)
st, err := squashfs.FromRaw(raw)
c.Assert(err, IsNil, com)
c.Check(st.Mode(), Equals, os.FileMode(0644), com)
c.Check(st.Path(), Equals, "/some filename", com)
c.Check(st.User(), Equals, "user", com)
c.Check(st.Group(), Equals, "group", com)
c.Check(st.Size(), Equals, n, com)
c.Check(st.ModTime(), Equals, time.Date(2017, 12, 8, 11, 19, 0, 0, time.UTC), com)
}
}
func (s *SquashfsTestSuite) TestStatModeBits(c *C) {
for i := os.FileMode(0); i <= 0777; i++ {
raw := []byte(fmt.Sprintf("%s user/group 53595 2017-12-08 11:19 ./yadda", i))
com := Commentf("%q vs %o", raw, i)
st, err := squashfs.FromRaw(raw)
c.Assert(err, IsNil, com)
c.Check(st.Mode(), Equals, i, com)
c.Check(st.Path(), Equals, "/yadda", com)
c.Check(st.User(), Equals, "user", com)
c.Check(st.Group(), Equals, "group", com)
c.Check(st.Size(), Equals, int64(53595), com)
c.Check(st.ModTime(), Equals, time.Date(2017, 12, 8, 11, 19, 0, 0, time.UTC), com)
jRaw := make([]byte, len(raw))
for j := 01000 + i; j <= 07777; j += 01000 {
// this silliness only needed because os.FileMode's String() throws away sticky/setuid/setgid bits
copy(jRaw, raw)
if j&01000 != 0 {
if j&0001 != 0 {
jRaw[9] = 't'
} else {
jRaw[9] = 'T'
}
}
if j&02000 != 0 {
if j&0010 != 0 {
jRaw[6] = 's'
} else {
jRaw[6] = 'S'
}
}
if j&04000 != 0 {
if j&0100 != 0 {
jRaw[3] = 's'
} else {
jRaw[3] = 'S'
}
}
com := Commentf("%q vs %o", jRaw, j)
st, err := squashfs.FromRaw(jRaw)
c.Assert(err, IsNil, com)
c.Check(st.Mode(), Equals, j, com)
}
}
}
snapd-2.37.4~14.04.1/snap/squashfs/squashfs_test.go 0000664 0000000 0000000 00000043763 13435556260 016577 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package squashfs_test
import (
"errors"
"io/ioutil"
"math"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
. "gopkg.in/check.v1"
"gopkg.in/yaml.v2"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap/snapdir"
"github.com/snapcore/snapd/snap/squashfs"
"github.com/snapcore/snapd/testutil"
)
// Hook up check.v1 into the "go test" runner
func Test(t *testing.T) { TestingT(t) }
type SquashfsTestSuite struct {
oldStdout, oldStderr, outf *os.File
}
var _ = Suite(&SquashfsTestSuite{})
func makeSnap(c *C, manifest, data string) *squashfs.Snap {
cur, _ := os.Getwd()
return makeSnapInDir(c, cur, manifest, data)
}
func makeSnapContents(c *C, manifest, data string) string {
tmp := c.MkDir()
err := os.MkdirAll(filepath.Join(tmp, "meta", "hooks", "dir"), 0755)
c.Assert(err, IsNil)
// our regular snap.yaml
err = ioutil.WriteFile(filepath.Join(tmp, "meta", "snap.yaml"), []byte(manifest), 0644)
c.Assert(err, IsNil)
// some hooks
err = ioutil.WriteFile(filepath.Join(tmp, "meta", "hooks", "foo-hook"), nil, 0755)
c.Assert(err, IsNil)
err = ioutil.WriteFile(filepath.Join(tmp, "meta", "hooks", "bar-hook"), nil, 0755)
c.Assert(err, IsNil)
// And a file in another directory in there, just for testing (not a valid
// hook)
err = ioutil.WriteFile(filepath.Join(tmp, "meta", "hooks", "dir", "baz"), nil, 0755)
c.Assert(err, IsNil)
// some empty directories
err = os.MkdirAll(filepath.Join(tmp, "food", "bard", "bazd"), 0755)
c.Assert(err, IsNil)
// some data
err = ioutil.WriteFile(filepath.Join(tmp, "data.bin"), []byte(data), 0644)
c.Assert(err, IsNil)
return tmp
}
func makeSnapInDir(c *C, dir, manifest, data string) *squashfs.Snap {
snapType := "app"
var m struct {
Type string `yaml:"type"`
}
if err := yaml.Unmarshal([]byte(manifest), &m); err == nil && m.Type != "" {
snapType = m.Type
}
tmp := makeSnapContents(c, manifest, data)
// build it
snap := squashfs.New(filepath.Join(dir, "foo.snap"))
err := snap.Build(tmp, snapType)
c.Assert(err, IsNil)
return snap
}
func (s *SquashfsTestSuite) SetUpTest(c *C) {
d := c.MkDir()
dirs.SetRootDir(d)
err := os.Chdir(d)
c.Assert(err, IsNil)
s.outf, err = ioutil.TempFile(c.MkDir(), "")
c.Assert(err, IsNil)
s.oldStdout, s.oldStderr = os.Stdout, os.Stderr
os.Stdout, os.Stderr = s.outf, s.outf
}
func (s *SquashfsTestSuite) TearDownTest(c *C) {
os.Stdout, os.Stderr = s.oldStdout, s.oldStderr
// this ensures things were quiet
_, err := s.outf.Seek(0, 0)
c.Assert(err, IsNil)
outbuf, err := ioutil.ReadAll(s.outf)
c.Assert(err, IsNil)
c.Check(string(outbuf), Equals, "")
}
func (s *SquashfsTestSuite) TestInstallSimpleNoCp(c *C) {
// mock cp but still cp
cmd := testutil.MockCommand(c, "cp", `#!/bin/sh
exec /bin/cp "$@"
`)
defer cmd.Restore()
// mock link but still link
linked := 0
r := squashfs.MockLink(func(a, b string) error {
linked++
return os.Link(a, b)
})
defer r()
snap := makeSnap(c, "name: test", "")
targetPath := filepath.Join(c.MkDir(), "target.snap")
mountDir := c.MkDir()
err := snap.Install(targetPath, mountDir)
c.Assert(err, IsNil)
c.Check(osutil.FileExists(targetPath), Equals, true)
c.Check(linked, Equals, 1)
c.Check(cmd.Calls(), HasLen, 0)
}
func noLink() func() {
return squashfs.MockLink(func(string, string) error { return errors.New("no.") })
}
func (s *SquashfsTestSuite) TestInstallNotCopyTwice(c *C) {
// first, disable os.Link
defer noLink()()
// then, mock cp but still cp
cmd := testutil.MockCommand(c, "cp", `#!/bin/sh
exec /bin/cp "$@"
`)
defer cmd.Restore()
snap := makeSnap(c, "name: test2", "")
targetPath := filepath.Join(c.MkDir(), "target.snap")
mountDir := c.MkDir()
err := snap.Install(targetPath, mountDir)
c.Assert(err, IsNil)
c.Check(cmd.Calls(), HasLen, 1)
err = snap.Install(targetPath, mountDir)
c.Assert(err, IsNil)
c.Check(cmd.Calls(), HasLen, 1) // and not 2 \o/
}
func (s *SquashfsTestSuite) TestInstallSeedNoLink(c *C) {
defer noLink()()
c.Assert(os.MkdirAll(dirs.SnapSeedDir, 0755), IsNil)
snap := makeSnapInDir(c, dirs.SnapSeedDir, "name: test2", "")
targetPath := filepath.Join(c.MkDir(), "target.snap")
_, err := os.Lstat(targetPath)
c.Check(os.IsNotExist(err), Equals, true)
err = snap.Install(targetPath, c.MkDir())
c.Assert(err, IsNil)
c.Check(osutil.IsSymlink(targetPath), Equals, true) // \o/
}
func (s *SquashfsTestSuite) TestPath(c *C) {
p := "/path/to/foo.snap"
snap := squashfs.New("/path/to/foo.snap")
c.Assert(snap.Path(), Equals, p)
}
func (s *SquashfsTestSuite) TestReadFile(c *C) {
snap := makeSnap(c, "name: foo", "")
content, err := snap.ReadFile("meta/snap.yaml")
c.Assert(err, IsNil)
c.Assert(string(content), Equals, "name: foo")
}
func (s *SquashfsTestSuite) TestListDir(c *C) {
snap := makeSnap(c, "name: foo", "")
fileNames, err := snap.ListDir("meta/hooks")
c.Assert(err, IsNil)
c.Assert(len(fileNames), Equals, 3)
c.Check(fileNames[0], Equals, "bar-hook")
c.Check(fileNames[1], Equals, "dir")
c.Check(fileNames[2], Equals, "foo-hook")
}
func (s *SquashfsTestSuite) TestWalk(c *C) {
sub := "."
snap := makeSnap(c, "name: foo", "")
sqw := map[string]os.FileInfo{}
snap.Walk(sub, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == "food" {
return filepath.SkipDir
}
sqw[path] = info
return nil
})
base := c.MkDir()
c.Assert(snap.Unpack("*", base), IsNil)
sdw := map[string]os.FileInfo{}
snapdir.New(base).Walk(sub, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == "food" {
return filepath.SkipDir
}
sdw[path] = info
return nil
})
fpw := map[string]os.FileInfo{}
filepath.Walk(filepath.Join(base, sub), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
path, err = filepath.Rel(base, path)
if err != nil {
return err
}
if path == "food" {
return filepath.SkipDir
}
fpw[path] = info
return nil
})
for k := range fpw {
squashfs.Alike(sqw[k], fpw[k], c, Commentf(k))
squashfs.Alike(sdw[k], fpw[k], c, Commentf(k))
}
for k := range sqw {
squashfs.Alike(fpw[k], sqw[k], c, Commentf(k))
squashfs.Alike(sdw[k], sqw[k], c, Commentf(k))
}
for k := range sdw {
squashfs.Alike(fpw[k], sdw[k], c, Commentf(k))
squashfs.Alike(sqw[k], sdw[k], c, Commentf(k))
}
}
// TestUnpackGlob tests the internal unpack
func (s *SquashfsTestSuite) TestUnpackGlob(c *C) {
data := "some random data"
snap := makeSnap(c, "", data)
outputDir := c.MkDir()
err := snap.Unpack("data*", outputDir)
c.Assert(err, IsNil)
// this is the file we expect
c.Assert(filepath.Join(outputDir, "data.bin"), testutil.FileEquals, data)
// ensure glob was honored
c.Assert(osutil.FileExists(filepath.Join(outputDir, "meta/snap.yaml")), Equals, false)
}
func (s *SquashfsTestSuite) TestUnpackDetectsFailures(c *C) {
mockUnsquashfs := testutil.MockCommand(c, "unsquashfs", `
cat >&2 <&2
exit 1
`)
defer mockUnsquashfs.Restore()
data := "mock kernel snap"
dir := makeSnapContents(c, "", data)
snap := squashfs.New("foo.snap")
c.Check(snap.Build(dir, "kernel"), ErrorMatches, `mksquashfs call failed: Yeah, nah.`)
}
func (s *SquashfsTestSuite) TestUnsquashfsStderrWriter(c *C) {
for _, t := range []struct {
inp []string
expectedErr string
}{
{
inp: []string{"failed to write something\n"},
expectedErr: `failed: "failed to write something"`,
},
{
inp: []string{"fai", "led to write", " something\nunrelated\n"},
expectedErr: `failed: "failed to write something"`,
},
{
inp: []string{"failed to write\nfailed to read\n"},
expectedErr: `failed: "failed to write", and "failed to read"`,
},
{
inp: []string{"failed 1\nfailed 2\n3 failed\n"},
expectedErr: `failed: "failed 1", "failed 2", and "3 failed"`,
},
{
inp: []string{"failed 1\nfailed 2\n3 Failed\n4 Failed\n"},
expectedErr: `failed: "failed 1", "failed 2", "3 Failed", and "4 Failed"`,
},
{
inp: []string{"failed 1\nfailed 2\n3 Failed\n4 Failed\nfailed #5\n"},
expectedErr: `failed: "failed 1", "failed 2", "3 Failed", "4 Failed", and 1 more`,
},
} {
usw := squashfs.NewUnsquashfsStderrWriter()
for _, l := range t.inp {
usw.Write([]byte(l))
}
if t.expectedErr != "" {
c.Check(usw.Err(), ErrorMatches, t.expectedErr, Commentf("inp: %q failed", t.inp))
} else {
c.Check(usw.Err(), IsNil)
}
}
}
func (s *SquashfsTestSuite) TestBuildDate(c *C) {
// This env is used in reproducible builds and will force
// squashfs to use a specific date. We need to unset it
// for this specific test.
if oldEnv := os.Getenv("SOURCE_DATE_EPOCH"); oldEnv != "" {
os.Unsetenv("SOURCE_DATE_EPOCH")
defer func() { os.Setenv("SOURCE_DATE_EPOCH", oldEnv) }()
}
// make a directory
d := c.MkDir()
// set its time waaay back
now := time.Now()
then := now.Add(-10000 * time.Hour)
c.Assert(os.Chtimes(d, then, then), IsNil)
// make a snap using this directory
filename := filepath.Join(c.MkDir(), "foo.snap")
snap := squashfs.New(filename)
c.Assert(snap.Build(d, "app"), IsNil)
// and see it's BuildDate is _now_, not _then_.
c.Check(squashfs.BuildDate(filename), Equals, snap.BuildDate())
c.Check(math.Abs(now.Sub(snap.BuildDate()).Seconds()) <= 61, Equals, true, Commentf("Unexpected build date %s", snap.BuildDate()))
}
snapd-2.37.4~14.04.1/snap/squashfs/stat.go 0000664 0000000 0000000 00000016200 13435556260 014640 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package squashfs
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
type SnapFileOwner struct {
UID uint32
GID uint32
}
type stat struct {
path string
size int64
mode os.FileMode
mtime time.Time
user string
group string
}
func (s stat) Name() string { return filepath.Base(s.path) }
func (s stat) Size() int64 { return s.size }
func (s stat) Mode() os.FileMode { return s.mode }
func (s stat) ModTime() time.Time { return s.mtime }
func (s stat) IsDir() bool { return s.mode.IsDir() }
func (s stat) Sys() interface{} { return nil }
func (s stat) Path() string { return s.path } // not path of os.FileInfo
const minLen = len("drwxrwxr-x u/g 53595 2017-12-08 11:19 .")
func fromRaw(raw []byte) (*stat, error) {
if len(raw) < minLen {
return nil, errBadLine(raw)
}
st := &stat{}
parsers := []func([]byte) (int, error){
// first, the file mode, e.g. "-rwxr-xr-x"
st.parseMode,
// next, user/group info
st.parseOwner,
// next'll come the size or the node type
st.parseSize,
// and then the time
st.parseTimeUTC,
// and finally the path
st.parsePath,
}
p := 0
for _, parser := range parsers {
n, err := parser(raw[p:])
if err != nil {
return nil, err
}
p += n
if p < len(raw) && raw[p] != ' ' {
return nil, errBadLine(raw)
}
p++
}
if st.mode&os.ModeSymlink != 0 {
// the symlink *could* be from a file called "foo -> bar" to
// another called "baz -> quux" in which case the following
// would be wrong, but so be it.
idx := strings.Index(st.path, " -> ")
if idx < 0 {
return nil, errBadPath(raw)
}
st.path = st.path[:idx]
}
return st, nil
}
type statError struct {
part string
raw []byte
}
func (e statError) Error() string {
return fmt.Sprintf("cannot parse %s: %q", e.part, e.raw)
}
func errBadLine(raw []byte) statError {
return statError{
part: "line",
raw: raw,
}
}
func errBadMode(raw []byte) statError {
return statError{
part: "mode",
raw: raw,
}
}
func errBadOwner(raw []byte) statError {
return statError{
part: "owner",
raw: raw,
}
}
func errBadNode(raw []byte) statError {
return statError{
part: "node",
raw: raw,
}
}
func errBadSize(raw []byte) statError {
return statError{
part: "size",
raw: raw,
}
}
func errBadTime(raw []byte) statError {
return statError{
part: "time",
raw: raw,
}
}
func errBadPath(raw []byte) statError {
return statError{
part: "path",
raw: raw,
}
}
func (st *stat) parseTimeUTC(raw []byte) (int, error) {
const timelen = 16
t, err := time.Parse("2006-01-02 15:04", string(raw[:timelen]))
if err != nil {
return 0, errBadTime(raw)
}
st.mtime = t
return timelen, nil
}
func (st *stat) parseMode(raw []byte) (int, error) {
switch raw[0] {
case '-':
// 0
case 'd':
st.mode |= os.ModeDir
case 's':
st.mode |= os.ModeSocket
case 'c':
st.mode |= os.ModeCharDevice
case 'b':
st.mode |= os.ModeDevice
case 'p':
st.mode |= os.ModeNamedPipe
case 'l':
st.mode |= os.ModeSymlink
default:
return 0, errBadMode(raw)
}
for i := 0; i < 3; i++ {
m, err := modeFromTriplet(raw[1+3*i:4+3*i], uint(2-i))
if err != nil {
return 0, err
}
st.mode |= m
}
// always this length (1+3*3==10)
return 10, nil
}
func (st *stat) parseOwner(raw []byte) (int, error) {
var p, ui, uj, gi, gj int
// first check it's sane (at least two non-space chars)
if raw[0] == ' ' || raw[1] == ' ' {
return 0, errBadLine(raw)
}
ui = 0
// from useradd(8): Usernames may only be up to 32 characters long.
// from groupadd(8): Groupnames may only be up to 32 characters long.
// +1 for the separator, +1 for the ending space
maxL := 66
if len(raw) < maxL {
maxL = len(raw)
}
out:
for p = ui; p < maxL; p++ {
switch raw[p] {
case '/':
uj = p
gi = p + 1
case ' ':
gj = p
break out
}
}
if uj == 0 || gj == 0 || gi == gj {
return 0, errBadOwner(raw)
}
st.user, st.group = string(raw[ui:uj]), string(raw[gi:gj])
return p, nil
}
func modeFromTriplet(trip []byte, shift uint) (os.FileMode, error) {
var mode os.FileMode
high := false
if len(trip) != 3 {
panic("bad triplet length")
}
switch trip[0] {
case '-':
// 0
case 'r':
mode |= 4
default:
return 0, errBadMode(trip)
}
switch trip[1] {
case '-':
// 0
case 'w':
mode |= 2
default:
return 0, errBadMode(trip)
}
switch trip[2] {
case '-':
// 0
case 'x':
mode |= 1
case 'S', 'T':
high = true
case 's', 't':
mode |= 1
high = true
default:
return 0, errBadMode(trip)
}
mode <<= 3 * shift
if high {
mode |= (01000 << shift)
}
return mode, nil
}
func (st *stat) parseSize(raw []byte) (int, error) {
// the "size" column, for regular files, is the file size in bytes:
// -rwxr-xr-x user/group 53595 2017-12-08 11:19 ./yadda
// ^^^^^ like this
// for devices, though, it's the major, minor of the node:
// crw-rw---- root/audio 14, 3 2017-12-05 10:29 ./dev/dsp
// ^^^^^^ like so
// (for other things it is a size, although what the size is
// _of_ is left as an exercise for the reader)
isNode := st.mode&(os.ModeDevice|os.ModeCharDevice) != 0
p := 0
maxP := len(raw) - len("2006-01-02 15:04 .")
for raw[p] == ' ' {
if p >= maxP {
return 0, errBadLine(raw)
}
p++
}
ni := p
for raw[p] >= '0' && raw[p] <= '9' {
if p >= maxP {
return 0, errBadLine(raw)
}
p++
}
if p == ni {
if isNode {
return 0, errBadNode(raw)
}
return 0, errBadSize(raw)
}
if isNode {
if raw[p] != ',' {
return 0, errBadNode(raw)
}
p++
// drop the space before the minor mode
for raw[p] == ' ' {
p++
}
// drop the minor mode
for raw[p] >= '0' && raw[p] <= '9' {
p++
}
if raw[p] != ' ' {
return 0, errBadNode(raw)
}
} else {
if raw[p] != ' ' {
return 0, errBadSize(raw)
}
// note that, much as it makes very little sense, the arch-
// dependent st_size is never an unsigned 64 bit quantity.
// It's one of unsigned long, long long, or just off_t.
//
// Also note os.FileInfo's Size needs to return an int64, and
// squashfs's inode->data (where it stores sizes for regular
// files) is a long long.
sz, err := strconv.ParseInt(string(raw[ni:p]), 10, 64)
if err != nil {
return 0, errBadSize(raw)
}
st.size = sz
}
return p, nil
}
func (st *stat) parsePath(raw []byte) (int, error) {
if raw[0] != '.' {
return 0, errBadPath(raw)
}
if len(raw[1:]) == 0 {
st.path = "/"
} else {
st.path = string(raw[1:])
}
return len(raw), nil
}
snapd-2.37.4~14.04.1/snap/squashfs/export_test.go 0000664 0000000 0000000 00000004214 13435556260 016247 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package squashfs
import (
"os"
"os/exec"
"time"
"gopkg.in/check.v1"
)
var (
FromRaw = fromRaw
NewUnsquashfsStderrWriter = newUnsquashfsStderrWriter
)
func (s stat) User() string { return s.user }
func (s stat) Group() string { return s.group }
func MockLink(newLink func(string, string) error) (restore func()) {
oldLink := osLink
osLink = newLink
return func() {
osLink = oldLink
}
}
func MockFromCore(newFromCore func(string, string, ...string) (*exec.Cmd, error)) (restore func()) {
oldFromCore := osutilCommandFromCore
osutilCommandFromCore = newFromCore
return func() {
osutilCommandFromCore = oldFromCore
}
}
// Alike compares to os.FileInfo to determine if they are sufficiently
// alike to say they refer to the same thing.
func Alike(a, b os.FileInfo, c *check.C, comment check.CommentInterface) {
c.Check(a, check.NotNil, comment)
c.Check(b, check.NotNil, comment)
if a == nil || b == nil {
return
}
// the .Name() of the root will be different on non-squashfs things
_, asq := a.(*stat)
_, bsq := b.(*stat)
if !((asq && a.Name() == "/") || (bsq && b.Name() == "/")) {
c.Check(a.Name(), check.Equals, b.Name(), comment)
}
c.Check(a.Mode(), check.Equals, b.Mode(), comment)
if a.Mode().IsRegular() {
c.Check(a.Size(), check.Equals, b.Size(), comment)
}
am := a.ModTime().UTC().Truncate(time.Minute)
bm := b.ModTime().UTC().Truncate(time.Minute)
c.Check(am.Equal(bm), check.Equals, true, check.Commentf("%s != %s (%s)", am, bm, comment))
}
snapd-2.37.4~14.04.1/snap/info_snap_yaml_test.go 0000664 0000000 0000000 00000137211 13435556260 016073 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
"regexp"
"testing"
"time"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/strutil"
"github.com/snapcore/snapd/timeout"
"github.com/snapcore/snapd/testutil"
)
// Hook up check.v1 into the "go test" runner
func Test(t *testing.T) { TestingT(t) }
type InfoSnapYamlTestSuite struct {
testutil.BaseTest
}
var _ = Suite(&InfoSnapYamlTestSuite{})
var mockYaml = []byte(`name: foo
version: 1.0
type: app
`)
func (s *InfoSnapYamlTestSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
}
func (s *InfoSnapYamlTestSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
}
func (s *InfoSnapYamlTestSuite) TestSimple(c *C) {
info, err := snap.InfoFromSnapYaml(mockYaml)
c.Assert(err, IsNil)
c.Assert(info.InstanceName(), Equals, "foo")
c.Assert(info.Version, Equals, "1.0")
c.Assert(info.Type, Equals, snap.TypeApp)
c.Assert(info.Epoch, DeepEquals, snap.E("0"))
}
func (s *InfoSnapYamlTestSuite) TestSnapdTypeAddedByMagic(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(`name: snapd
version: 1.0`))
c.Assert(err, IsNil)
c.Assert(info.InstanceName(), Equals, "snapd")
c.Assert(info.Version, Equals, "1.0")
c.Assert(info.Type, Equals, snap.TypeSnapd)
}
func (s *InfoSnapYamlTestSuite) TestFail(c *C) {
_, err := snap.InfoFromSnapYaml([]byte("random-crap"))
c.Assert(err, ErrorMatches, "(?m)cannot parse snap.yaml:.*")
}
type YamlSuite struct {
restore func()
testutil.BaseTest
}
var _ = Suite(&YamlSuite{})
func (s *YamlSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
hookType := snap.NewHookType(regexp.MustCompile(".*"))
s.restore = snap.MockSupportedHookTypes([]*snap.HookType{hookType})
}
func (s *YamlSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
s.restore()
}
func (s *YamlSuite) TestUnmarshalGarbage(c *C) {
_, err := snap.InfoFromSnapYaml([]byte(`"`))
c.Assert(err, ErrorMatches, ".*: yaml: found unexpected end of stream")
}
func (s *YamlSuite) TestUnmarshalEmpty(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(``))
c.Assert(err, IsNil)
c.Assert(info.Plugs, HasLen, 0)
c.Assert(info.Slots, HasLen, 0)
c.Assert(info.Apps, HasLen, 0)
}
// Tests focusing on plugs
func (s *YamlSuite) TestUnmarshalStandaloneImplicitPlug(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
network-client:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Assert(info.Plugs["network-client"], DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "network-client",
Interface: "network-client",
})
}
func (s *YamlSuite) TestUnmarshalStandaloneAbbreviatedPlug(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
net: network-client
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "net",
Interface: "network-client",
})
}
func (s *YamlSuite) TestUnmarshalStandaloneMinimalisticPlug(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
net:
interface: network-client
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "net",
Interface: "network-client",
})
}
func (s *YamlSuite) TestUnmarshalStandaloneCompletePlug(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
net:
interface: network-client
ipv6-aware: true
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "net",
Interface: "network-client",
Attrs: map[string]interface{}{"ipv6-aware": true},
})
}
func (s *YamlSuite) TestUnmarshalStandalonePlugWithIntAndListAndMap(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
iface:
interface: complex
i: 3
l: [1,2,3]
m:
a: A
b: B
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Assert(info.Plugs["iface"], DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "iface",
Interface: "complex",
Attrs: map[string]interface{}{
"i": int64(3),
"l": []interface{}{int64(1), int64(2), int64(3)},
"m": map[string]interface{}{"a": "A", "b": "B"},
},
})
}
func (s *YamlSuite) TestUnmarshalLastPlugDefinitionWins(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
net:
interface: network-client
attr: 1
net:
interface: network-client
attr: 2
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Assert(info.Plugs["net"], DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "net",
Interface: "network-client",
Attrs: map[string]interface{}{"attr": int64(2)},
})
}
func (s *YamlSuite) TestUnmarshalPlugsExplicitlyDefinedImplicitlyBoundToApps(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
network-client:
apps:
app:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 1)
plug := info.Plugs["network-client"]
app := info.Apps["app"]
c.Assert(plug, DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "network-client",
Interface: "network-client",
Apps: map[string]*snap.AppInfo{app.Name: app},
})
c.Assert(app, DeepEquals, &snap.AppInfo{
Snap: info,
Name: "app",
Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
})
}
func (s *YamlSuite) TestUnmarshalGlobalPlugBoundToOneApp(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
network-client:
apps:
with-plug:
plugs: [network-client]
without-plug:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 2)
plug := info.Plugs["network-client"]
withPlugApp := info.Apps["with-plug"]
withoutPlugApp := info.Apps["without-plug"]
c.Assert(plug, DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "network-client",
Interface: "network-client",
Apps: map[string]*snap.AppInfo{withPlugApp.Name: withPlugApp},
})
c.Assert(withPlugApp, DeepEquals, &snap.AppInfo{
Snap: info,
Name: "with-plug",
Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
})
c.Assert(withoutPlugApp, DeepEquals, &snap.AppInfo{
Snap: info,
Name: "without-plug",
Plugs: map[string]*snap.PlugInfo{},
})
}
func (s *YamlSuite) TestUnmarshalPlugsExplicitlyDefinedExplicitlyBoundToApps(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
net: network-client
apps:
app:
plugs: ["net"]
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 1)
plug := info.Plugs["net"]
app := info.Apps["app"]
c.Assert(plug, DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "net",
Interface: "network-client",
Apps: map[string]*snap.AppInfo{app.Name: app},
})
c.Assert(app, DeepEquals, &snap.AppInfo{
Snap: info,
Name: "app",
Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
})
}
func (s *YamlSuite) TestUnmarshalPlugsImplicitlyDefinedExplicitlyBoundToApps(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
apps:
app:
plugs: ["network-client"]
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 1)
plug := info.Plugs["network-client"]
app := info.Apps["app"]
c.Assert(plug, DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "network-client",
Interface: "network-client",
Apps: map[string]*snap.AppInfo{app.Name: app},
})
c.Assert(app, DeepEquals, &snap.AppInfo{
Snap: info,
Name: "app",
Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
})
}
func (s *YamlSuite) TestUnmarshalPlugWithoutInterfaceName(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
network-client:
ipv6-aware: true
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 0)
c.Assert(info.Plugs["network-client"], DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "network-client",
Interface: "network-client",
Attrs: map[string]interface{}{"ipv6-aware": true},
})
}
func (s *YamlSuite) TestUnmarshalPlugWithLabel(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
bool-file:
label: Disk I/O indicator
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 0)
c.Assert(info.Plugs["bool-file"], DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "bool-file",
Interface: "bool-file",
Label: "Disk I/O indicator",
})
}
func (s *YamlSuite) TestUnmarshalCorruptedPlugWithNonStringInterfaceName(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
net:
interface: 1.0
ipv6-aware: true
`))
c.Assert(err, ErrorMatches, `interface name on plug "net" is not a string \(found float64\)`)
}
func (s *YamlSuite) TestUnmarshalCorruptedPlugWithNonStringLabel(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
bool-file:
label: 1.0
`))
c.Assert(err, ErrorMatches, `label of plug "bool-file" is not a string \(found float64\)`)
}
func (s *YamlSuite) TestUnmarshalCorruptedPlugWithNonStringAttributes(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
net:
1: ok
`))
c.Assert(err, ErrorMatches, `plug "net" has attribute that is not a string \(found int\)`)
}
func (s *YamlSuite) TestUnmarshalCorruptedPlugWithUnexpectedType(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
net: 5
`))
c.Assert(err, ErrorMatches, `plug "net" has malformed definition \(found int\)`)
}
func (s *YamlSuite) TestUnmarshalReservedPlugAttribute(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
serial:
interface: serial-port
$baud-rate: [9600]
`))
c.Assert(err, ErrorMatches, `plug "serial" uses reserved attribute "\$baud-rate"`)
}
func (s *YamlSuite) TestUnmarshalInvalidPlugAttribute(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
serial:
interface: serial-port
foo: null
`))
c.Assert(err, ErrorMatches, `attribute "foo" of plug \"serial\": invalid scalar:.*`)
}
func (s *YamlSuite) TestUnmarshalInvalidAttributeMapKey(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
serial:
interface: serial-port
bar:
baz:
- 1: A
`))
c.Assert(err, ErrorMatches, `attribute "bar" of plug \"serial\": non-string key: 1`)
}
// Tests focusing on slots
func (s *YamlSuite) TestUnmarshalStandaloneImplicitSlot(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
network-client:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Assert(info.Slots["network-client"], DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "network-client",
Interface: "network-client",
})
}
func (s *YamlSuite) TestUnmarshalStandaloneAbbreviatedSlot(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
net: network-client
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "net",
Interface: "network-client",
})
}
func (s *YamlSuite) TestUnmarshalStandaloneMinimalisticSlot(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
net:
interface: network-client
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "net",
Interface: "network-client",
})
}
func (s *YamlSuite) TestUnmarshalStandaloneCompleteSlot(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
net:
interface: network-client
ipv6-aware: true
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "net",
Interface: "network-client",
Attrs: map[string]interface{}{"ipv6-aware": true},
})
}
func (s *YamlSuite) TestUnmarshalStandaloneSlotWithIntAndListAndMap(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
iface:
interface: complex
i: 3
l: [1,2]
m:
a: "A"
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Assert(info.Slots["iface"], DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "iface",
Interface: "complex",
Attrs: map[string]interface{}{
"i": int64(3),
"l": []interface{}{int64(1), int64(2)},
"m": map[string]interface{}{"a": "A"},
},
})
}
func (s *YamlSuite) TestUnmarshalLastSlotDefinitionWins(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
net:
interface: network-client
attr: 1
net:
interface: network-client
attr: 2
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Assert(info.Slots["net"], DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "net",
Interface: "network-client",
Attrs: map[string]interface{}{"attr": int64(2)},
})
}
func (s *YamlSuite) TestUnmarshalSlotsExplicitlyDefinedImplicitlyBoundToApps(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
network-client:
apps:
app:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Check(info.Apps, HasLen, 1)
slot := info.Slots["network-client"]
app := info.Apps["app"]
c.Assert(slot, DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "network-client",
Interface: "network-client",
Apps: map[string]*snap.AppInfo{app.Name: app},
})
c.Assert(app, DeepEquals, &snap.AppInfo{
Snap: info,
Name: "app",
Slots: map[string]*snap.SlotInfo{slot.Name: slot},
})
}
func (s *YamlSuite) TestUnmarshalSlotsExplicitlyDefinedExplicitlyBoundToApps(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
net: network-client
apps:
app:
slots: ["net"]
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Check(info.Apps, HasLen, 1)
slot := info.Slots["net"]
app := info.Apps["app"]
c.Assert(slot, DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "net",
Interface: "network-client",
Apps: map[string]*snap.AppInfo{app.Name: app},
})
c.Assert(app, DeepEquals, &snap.AppInfo{
Snap: info,
Name: "app",
Slots: map[string]*snap.SlotInfo{slot.Name: slot},
})
}
func (s *YamlSuite) TestUnmarshalSlotsImplicitlyDefinedExplicitlyBoundToApps(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
apps:
app:
slots: ["network-client"]
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Check(info.Apps, HasLen, 1)
slot := info.Slots["network-client"]
app := info.Apps["app"]
c.Assert(slot, DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "network-client",
Interface: "network-client",
Apps: map[string]*snap.AppInfo{app.Name: app},
})
c.Assert(app, DeepEquals, &snap.AppInfo{
Snap: info,
Name: "app",
Slots: map[string]*snap.SlotInfo{slot.Name: slot},
})
}
func (s *YamlSuite) TestUnmarshalSlotWithoutInterfaceName(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
network-client:
ipv6-aware: true
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Check(info.Apps, HasLen, 0)
c.Assert(info.Slots["network-client"], DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "network-client",
Interface: "network-client",
Attrs: map[string]interface{}{"ipv6-aware": true},
})
}
func (s *YamlSuite) TestUnmarshalSlotWithLabel(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
led0:
interface: bool-file
label: Front panel LED (red)
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Check(info.Apps, HasLen, 0)
c.Assert(info.Slots["led0"], DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "led0",
Interface: "bool-file",
Label: "Front panel LED (red)",
})
}
func (s *YamlSuite) TestUnmarshalGlobalSlotsBindToHooks(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
test-slot:
hooks:
test-hook:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Check(info.Apps, HasLen, 0)
c.Check(info.Hooks, HasLen, 1)
slot, ok := info.Slots["test-slot"]
c.Assert(ok, Equals, true, Commentf("Expected slots to include 'test-slot'"))
hook, ok := info.Hooks["test-hook"]
c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
c.Check(slot, DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "test-slot",
Interface: "test-slot",
Hooks: map[string]*snap.HookInfo{hook.Name: hook},
})
c.Check(hook, DeepEquals, &snap.HookInfo{
Snap: info,
Name: "test-hook",
Slots: map[string]*snap.SlotInfo{slot.Name: slot},
Explicit: true,
})
}
func (s *YamlSuite) TestUnmarshalHookWithSlot(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
hooks:
test-hook:
slots: [test-slot]
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 1)
c.Check(info.Apps, HasLen, 0)
c.Check(info.Hooks, HasLen, 1)
slot, ok := info.Slots["test-slot"]
c.Assert(ok, Equals, true, Commentf("Expected slots to include 'test-slot'"))
hook, ok := info.Hooks["test-hook"]
c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
c.Check(slot, DeepEquals, &snap.SlotInfo{
Snap: info,
Name: "test-slot",
Interface: "test-slot",
Hooks: map[string]*snap.HookInfo{hook.Name: hook},
})
c.Check(hook, DeepEquals, &snap.HookInfo{
Snap: info,
Name: "test-hook",
Slots: map[string]*snap.SlotInfo{slot.Name: slot},
Explicit: true,
})
}
func (s *YamlSuite) TestUnmarshalCorruptedSlotWithNonStringInterfaceName(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
net:
interface: 1.0
ipv6-aware: true
`))
c.Assert(err, ErrorMatches, `interface name on slot "net" is not a string \(found float64\)`)
}
func (s *YamlSuite) TestUnmarshalCorruptedSlotWithNonStringLabel(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
bool-file:
label: 1.0
`))
c.Assert(err, ErrorMatches, `label of slot "bool-file" is not a string \(found float64\)`)
}
func (s *YamlSuite) TestUnmarshalCorruptedSlotWithNonStringAttributes(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
net:
1: ok
`))
c.Assert(err, ErrorMatches, `slot "net" has attribute that is not a string \(found int\)`)
}
func (s *YamlSuite) TestUnmarshalCorruptedSlotWithUnexpectedType(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
net: 5
`))
c.Assert(err, ErrorMatches, `slot "net" has malformed definition \(found int\)`)
}
func (s *YamlSuite) TestUnmarshalReservedSlotAttribute(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
serial:
interface: serial-port
$baud-rate: [9600]
`))
c.Assert(err, ErrorMatches, `slot "serial" uses reserved attribute "\$baud-rate"`)
}
func (s *YamlSuite) TestUnmarshalInvalidSlotAttribute(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
_, err := snap.InfoFromSnapYaml([]byte(`
name: snap
slots:
serial:
interface: serial-port
foo: null
`))
c.Assert(err, ErrorMatches, `attribute "foo" of slot \"serial\": invalid scalar:.*`)
}
func (s *YamlSuite) TestUnmarshalHook(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
hooks:
test-hook:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 0)
c.Check(info.Hooks, HasLen, 1)
hook, ok := info.Hooks["test-hook"]
c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
c.Check(hook, DeepEquals, &snap.HookInfo{
Snap: info,
Name: "test-hook",
Plugs: nil,
Explicit: true,
})
}
func (s *YamlSuite) TestUnmarshalUnsupportedHook(c *C) {
s.restore()
hookType := snap.NewHookType(regexp.MustCompile("not-test-hook"))
s.restore = snap.MockSupportedHookTypes([]*snap.HookType{hookType})
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
hooks:
test-hook:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 0)
c.Check(info.Hooks, HasLen, 0, Commentf("Expected no hooks to be loaded"))
}
func (s *YamlSuite) TestUnmarshalHookFiltersOutUnsupportedHooks(c *C) {
s.restore()
hookType := snap.NewHookType(regexp.MustCompile("test-.*"))
s.restore = snap.MockSupportedHookTypes([]*snap.HookType{hookType})
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
hooks:
test-hook:
foo-hook:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 0)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 0)
c.Check(info.Hooks, HasLen, 1)
hook, ok := info.Hooks["test-hook"]
c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
c.Check(hook, DeepEquals, &snap.HookInfo{
Snap: info,
Name: "test-hook",
Plugs: nil,
Explicit: true,
})
}
func (s *YamlSuite) TestUnmarshalHookWithPlug(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
hooks:
test-hook:
plugs: [test-plug]
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 0)
c.Check(info.Hooks, HasLen, 1)
plug, ok := info.Plugs["test-plug"]
c.Assert(ok, Equals, true, Commentf("Expected plugs to include 'test-plug'"))
hook, ok := info.Hooks["test-hook"]
c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
c.Check(plug, DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "test-plug",
Interface: "test-plug",
Hooks: map[string]*snap.HookInfo{hook.Name: hook},
})
c.Check(hook, DeepEquals, &snap.HookInfo{
Snap: info,
Name: "test-hook",
Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
Explicit: true,
})
}
func (s *YamlSuite) TestUnmarshalGlobalPlugsBindToHooks(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
test-plug:
hooks:
test-hook:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 0)
c.Check(info.Hooks, HasLen, 1)
plug, ok := info.Plugs["test-plug"]
c.Assert(ok, Equals, true, Commentf("Expected plugs to include 'test-plug'"))
hook, ok := info.Hooks["test-hook"]
c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
c.Check(plug, DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "test-plug",
Interface: "test-plug",
Hooks: map[string]*snap.HookInfo{hook.Name: hook},
})
c.Check(hook, DeepEquals, &snap.HookInfo{
Snap: info,
Name: "test-hook",
Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
Explicit: true,
})
}
func (s *YamlSuite) TestUnmarshalGlobalPlugBoundToOneHook(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
test-plug:
hooks:
with-plug:
plugs: [test-plug]
without-plug:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 0)
c.Check(info.Hooks, HasLen, 2)
plug := info.Plugs["test-plug"]
withPlugHook := info.Hooks["with-plug"]
withoutPlugHook := info.Hooks["without-plug"]
c.Assert(plug, DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "test-plug",
Interface: "test-plug",
Hooks: map[string]*snap.HookInfo{withPlugHook.Name: withPlugHook},
})
c.Assert(withPlugHook, DeepEquals, &snap.HookInfo{
Snap: info,
Name: "with-plug",
Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
Explicit: true,
})
c.Assert(withoutPlugHook, DeepEquals, &snap.HookInfo{
Snap: info,
Name: "without-plug",
Plugs: map[string]*snap.PlugInfo{},
Explicit: true,
})
}
func (s *YamlSuite) TestUnmarshalExplicitGlobalPlugBoundToHook(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
test-plug: test-interface
hooks:
test-hook:
plugs: ["test-plug"]
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 0)
c.Check(info.Hooks, HasLen, 1)
plug, ok := info.Plugs["test-plug"]
c.Assert(ok, Equals, true, Commentf("Expected plugs to include 'test-plug'"))
hook, ok := info.Hooks["test-hook"]
c.Assert(ok, Equals, true, Commentf("Expected hooks to include 'test-hook'"))
c.Check(plug, DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "test-plug",
Interface: "test-interface",
Hooks: map[string]*snap.HookInfo{hook.Name: hook},
})
c.Check(hook, DeepEquals, &snap.HookInfo{
Snap: info,
Name: "test-hook",
Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
Explicit: true,
})
}
func (s *YamlSuite) TestUnmarshalGlobalPlugBoundToHookNotApp(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: snap
plugs:
test-plug:
hooks:
test-hook:
plugs: [test-plug]
apps:
test-app:
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "snap")
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 0)
c.Check(info.Apps, HasLen, 1)
c.Check(info.Hooks, HasLen, 1)
plug := info.Plugs["test-plug"]
hook := info.Hooks["test-hook"]
app := info.Apps["test-app"]
c.Assert(plug, DeepEquals, &snap.PlugInfo{
Snap: info,
Name: "test-plug",
Interface: "test-plug",
Apps: map[string]*snap.AppInfo{},
Hooks: map[string]*snap.HookInfo{hook.Name: hook},
})
c.Assert(hook, DeepEquals, &snap.HookInfo{
Snap: info,
Name: "test-hook",
Plugs: map[string]*snap.PlugInfo{plug.Name: plug},
Explicit: true,
})
c.Assert(app, DeepEquals, &snap.AppInfo{
Snap: info,
Name: "test-app",
Plugs: map[string]*snap.PlugInfo{},
})
}
func (s *YamlSuite) TestUnmarshalComplexExample(c *C) {
// NOTE: yaml content cannot use tabs, indent the section with spaces.
info, err := snap.InfoFromSnapYaml([]byte(`
name: foo
version: 1.2
title: Foo
summary: foo app
type: app
epoch: 1*
confinement: devmode
license: GPL-3.0
description: |
Foo provides useful services
apps:
daemon:
command: foo --daemon
plugs: [network, network-bind]
slots: [foo-socket-slot]
foo:
command: fooctl
plugs: [foo-socket-plug]
hooks:
test-hook:
plugs: [foo-socket-plug]
slots: [foo-socket-slot]
plugs:
foo-socket-plug:
interface: socket
# $protocol: foo
logging:
interface: syslog
slots:
foo-socket-slot:
interface: socket
path: $SNAP_DATA/socket
protocol: foo
tracing:
interface: ptrace
`))
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "foo")
c.Check(info.Version, Equals, "1.2")
c.Check(info.Type, Equals, snap.TypeApp)
c.Check(info.Epoch, DeepEquals, snap.E("1*"))
c.Check(info.Confinement, Equals, snap.DevModeConfinement)
c.Check(info.Title(), Equals, "Foo")
c.Check(info.Summary(), Equals, "foo app")
c.Check(info.Description(), Equals, "Foo provides useful services\n")
c.Check(info.Apps, HasLen, 2)
c.Check(info.Plugs, HasLen, 4)
c.Check(info.Slots, HasLen, 2)
// these don't come from snap.yaml
c.Check(info.Publisher, Equals, snap.StoreAccount{})
c.Check(info.Channel, Equals, "")
c.Check(info.License, Equals, "GPL-3.0")
app1 := info.Apps["daemon"]
app2 := info.Apps["foo"]
hook := info.Hooks["test-hook"]
plug1 := info.Plugs["network"]
plug2 := info.Plugs["network-bind"]
plug3 := info.Plugs["foo-socket-plug"]
plug4 := info.Plugs["logging"]
slot1 := info.Slots["foo-socket-slot"]
slot2 := info.Slots["tracing"]
// app1 ("daemon") has three plugs ("network", "network-bind", "logging")
// and two slots ("foo-socket", "tracing"). The slot "tracing" and plug
// "logging" are global, everything else is app-bound.
c.Assert(app1, Not(IsNil))
c.Check(app1.Snap, Equals, info)
c.Check(app1.Name, Equals, "daemon")
c.Check(app1.Command, Equals, "foo --daemon")
c.Check(app1.Plugs, DeepEquals, map[string]*snap.PlugInfo{
plug1.Name: plug1, plug2.Name: plug2, plug4.Name: plug4})
c.Check(app1.Slots, DeepEquals, map[string]*snap.SlotInfo{
slot1.Name: slot1, slot2.Name: slot2})
// app2 ("foo") has two plugs ("foo-socket", "logging") and one slot
// ("tracing"). The slot "tracing" and plug "logging" are global while
// "foo-socket" is app-bound.
c.Assert(app2, Not(IsNil))
c.Check(app2.Snap, Equals, info)
c.Check(app2.Name, Equals, "foo")
c.Check(app2.Command, Equals, "fooctl")
c.Check(app2.Plugs, DeepEquals, map[string]*snap.PlugInfo{
plug3.Name: plug3, plug4.Name: plug4})
c.Check(app2.Slots, DeepEquals, map[string]*snap.SlotInfo{
slot2.Name: slot2})
// hook1 has two plugs ("foo-socket", "logging") and two slots ("foo-socket", "tracing").
// The plug "logging" and slot "tracing" are global while "foo-socket" is hook-bound.
c.Assert(hook, NotNil)
c.Check(hook.Snap, Equals, info)
c.Check(hook.Name, Equals, "test-hook")
c.Check(hook.Plugs, DeepEquals, map[string]*snap.PlugInfo{
plug3.Name: plug3, plug4.Name: plug4})
c.Check(hook.Slots, DeepEquals, map[string]*snap.SlotInfo{
slot1.Name: slot1, slot2.Name: slot2})
// plug1 ("network") is implicitly defined and app-bound to "daemon"
c.Assert(plug1, Not(IsNil))
c.Check(plug1.Snap, Equals, info)
c.Check(plug1.Name, Equals, "network")
c.Check(plug1.Interface, Equals, "network")
c.Check(plug1.Attrs, HasLen, 0)
c.Check(plug1.Label, Equals, "")
c.Check(plug1.Apps, DeepEquals, map[string]*snap.AppInfo{app1.Name: app1})
// plug2 ("network-bind") is implicitly defined and app-bound to "daemon"
c.Assert(plug2, Not(IsNil))
c.Check(plug2.Snap, Equals, info)
c.Check(plug2.Name, Equals, "network-bind")
c.Check(plug2.Interface, Equals, "network-bind")
c.Check(plug2.Attrs, HasLen, 0)
c.Check(plug2.Label, Equals, "")
c.Check(plug2.Apps, DeepEquals, map[string]*snap.AppInfo{app1.Name: app1})
// plug3 ("foo-socket") is app-bound to "foo"
c.Assert(plug3, Not(IsNil))
c.Check(plug3.Snap, Equals, info)
c.Check(plug3.Name, Equals, "foo-socket-plug")
c.Check(plug3.Interface, Equals, "socket")
c.Check(plug3.Attrs, HasLen, 0)
c.Check(plug3.Label, Equals, "")
c.Check(plug3.Apps, DeepEquals, map[string]*snap.AppInfo{app2.Name: app2})
// plug4 ("logging") is global so it is bound to all apps
c.Assert(plug4, Not(IsNil))
c.Check(plug4.Snap, Equals, info)
c.Check(plug4.Name, Equals, "logging")
c.Check(plug4.Interface, Equals, "syslog")
c.Check(plug4.Attrs, HasLen, 0)
c.Check(plug4.Label, Equals, "")
c.Check(plug4.Apps, DeepEquals, map[string]*snap.AppInfo{
app1.Name: app1, app2.Name: app2})
// slot1 ("foo-socket") is app-bound to "daemon"
c.Assert(slot1, Not(IsNil))
c.Check(slot1.Snap, Equals, info)
c.Check(slot1.Name, Equals, "foo-socket-slot")
c.Check(slot1.Interface, Equals, "socket")
c.Check(slot1.Attrs, DeepEquals, map[string]interface{}{
"protocol": "foo", "path": "$SNAP_DATA/socket"})
c.Check(slot1.Label, Equals, "")
c.Check(slot1.Apps, DeepEquals, map[string]*snap.AppInfo{app1.Name: app1})
// slot2 ("tracing") is global so it is bound to all apps
c.Assert(slot2, Not(IsNil))
c.Check(slot2.Snap, Equals, info)
c.Check(slot2.Name, Equals, "tracing")
c.Check(slot2.Interface, Equals, "ptrace")
c.Check(slot2.Attrs, HasLen, 0)
c.Check(slot2.Label, Equals, "")
c.Check(slot2.Apps, DeepEquals, map[string]*snap.AppInfo{
app1.Name: app1, app2.Name: app2})
}
// type and architectures
func (s *YamlSuite) TestSnapYamlTypeDefault(c *C) {
y := []byte(`name: binary
version: 1.0
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Type, Equals, snap.TypeApp)
}
func (s *YamlSuite) TestSnapYamlEpochDefault(c *C) {
y := []byte(`name: binary
version: 1.0
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Epoch, DeepEquals, snap.E("0"))
}
func (s *YamlSuite) TestSnapYamlConfinementDefault(c *C) {
y := []byte(`name: binary
version: 1.0
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Confinement, Equals, snap.StrictConfinement)
}
func (s *YamlSuite) TestSnapYamlMultipleArchitecturesParsing(c *C) {
y := []byte(`name: binary
version: 1.0
architectures: [i386, armhf]
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Architectures, DeepEquals, []string{"i386", "armhf"})
}
func (s *YamlSuite) TestSnapYamlSingleArchitecturesParsing(c *C) {
y := []byte(`name: binary
version: 1.0
architectures: [i386]
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Architectures, DeepEquals, []string{"i386"})
}
func (s *YamlSuite) TestSnapYamlAssumesParsing(c *C) {
y := []byte(`name: binary
version: 1.0
assumes: [feature2, feature1]
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Assumes, DeepEquals, []string{"feature1", "feature2"})
}
func (s *YamlSuite) TestSnapYamlNoArchitecturesParsing(c *C) {
y := []byte(`name: binary
version: 1.0
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Architectures, DeepEquals, []string{"all"})
}
func (s *YamlSuite) TestSnapYamlBadArchitectureParsing(c *C) {
y := []byte(`name: binary
version: 1.0
architectures:
armhf:
no
`)
_, err := snap.InfoFromSnapYaml(y)
c.Assert(err, NotNil)
}
func (s *YamlSuite) TestSnapYamlLicenseParsing(c *C) {
y := []byte(`
name: foo
version: 1.0
license-agreement: explicit
license-version: 12`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.LicenseAgreement, Equals, "explicit")
c.Assert(info.LicenseVersion, Equals, "12")
}
// apps
func (s *YamlSuite) TestSimpleAppExample(c *C) {
y := []byte(`name: wat
version: 42
apps:
cm:
command: cm0
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Check(info.Apps, DeepEquals, map[string]*snap.AppInfo{
"cm": {
Snap: info,
Name: "cm",
Command: "cm0",
},
})
}
func (s *YamlSuite) TestDaemonEverythingExample(c *C) {
y := []byte(`name: wat
version: 42
apps:
svc:
command: svc1
description: svc one
stop-timeout: 25s
daemon: forking
stop-command: stop-cmd
post-stop-command: post-stop-cmd
restart-condition: on-abnormal
bus-name: busName
sockets:
sock1:
listen-stream: $SNAP_DATA/sock1.socket
socket-mode: 0666
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
app := snap.AppInfo{
Snap: info,
Name: "svc",
Command: "svc1",
Daemon: "forking",
RestartCond: snap.RestartOnAbnormal,
StopTimeout: timeout.Timeout(25 * time.Second),
StopCommand: "stop-cmd",
PostStopCommand: "post-stop-cmd",
BusName: "busName",
Sockets: map[string]*snap.SocketInfo{},
}
app.Sockets["sock1"] = &snap.SocketInfo{
App: &app,
Name: "sock1",
ListenStream: "$SNAP_DATA/sock1.socket",
SocketMode: 0666,
}
c.Check(info.Apps, DeepEquals, map[string]*snap.AppInfo{"svc": &app})
}
func (s *YamlSuite) TestDaemonListenStreamAsInteger(c *C) {
y := []byte(`name: wat
version: 42
apps:
svc:
command: svc
sockets:
sock:
listen-stream: 8080
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
app := snap.AppInfo{
Snap: info,
Name: "svc",
Command: "svc",
Sockets: map[string]*snap.SocketInfo{},
}
app.Sockets["sock"] = &snap.SocketInfo{
App: &app,
Name: "sock",
ListenStream: "8080",
}
c.Check(info.Apps, DeepEquals, map[string]*snap.AppInfo{
"svc": &app,
})
}
func (s *YamlSuite) TestDaemonInvalidSocketMode(c *C) {
y := []byte(`name: wat
version: 42
apps:
svc:
command: svc
sockets:
sock:
listen-stream: 8080
socket-mode: asdfasdf
`)
_, err := snap.InfoFromSnapYaml(y)
c.Check(err.Error(), Equals, "cannot parse snap.yaml: yaml: unmarshal errors:\n"+
" line 9: cannot unmarshal !!str `asdfasdf` into os.FileMode")
}
func (s *YamlSuite) TestSnapYamlGlobalEnvironment(c *C) {
y := []byte(`
name: foo
version: 1.0
environment:
foo: bar
baz: boom
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Environment, DeepEquals, *strutil.NewOrderedMap("foo", "bar", "baz", "boom"))
}
func (s *YamlSuite) TestSnapYamlPerAppEnvironment(c *C) {
y := []byte(`
name: foo
version: 1.0
apps:
foo:
environment:
k1: v1
k2: v2
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Apps["foo"].Environment, DeepEquals, *strutil.NewOrderedMap("k1", "v1", "k2", "v2"))
}
func (s *YamlSuite) TestSnapYamlPerHookEnvironment(c *C) {
y := []byte(`
name: foo
version: 1.0
hooks:
foo:
environment:
k1: v1
k2: v2
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Hooks["foo"].Environment, DeepEquals, *strutil.NewOrderedMap("k1", "v1", "k2", "v2"))
}
// classic confinement
func (s *YamlSuite) TestClassicConfinement(c *C) {
y := []byte(`
name: foo
confinement: classic
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Confinement, Equals, snap.ClassicConfinement)
}
func (s *YamlSuite) TestSnapYamlAliases(c *C) {
y := []byte(`
name: foo
version: 1.0
apps:
foo:
aliases: [foo]
bar:
aliases: [bar, bar1]
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Check(info.Apps["foo"].LegacyAliases, DeepEquals, []string{"foo"})
c.Check(info.Apps["bar"].LegacyAliases, DeepEquals, []string{"bar", "bar1"})
c.Check(info.LegacyAliases, DeepEquals, map[string]*snap.AppInfo{
"foo": info.Apps["foo"],
"bar": info.Apps["bar"],
"bar1": info.Apps["bar"],
})
}
func (s *YamlSuite) TestSnapYamlAliasesConflict(c *C) {
y := []byte(`
name: foo
version: 1.0
apps:
foo:
aliases: [bar]
bar:
aliases: [bar]
`)
_, err := snap.InfoFromSnapYaml(y)
c.Assert(err, ErrorMatches, `cannot set "bar" as alias for both ("foo" and "bar"|"bar" and "foo")`)
}
func (s *YamlSuite) TestSnapYamlAppStartOrder(c *C) {
y := []byte(`name: wat
version: 42
apps:
foo:
after: [bar, zed]
bar:
before: [foo]
baz:
after: [foo]
zed:
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Check(info.Apps, DeepEquals, map[string]*snap.AppInfo{
"foo": {
Snap: info,
Name: "foo",
After: []string{"bar", "zed"},
},
"bar": {
Snap: info,
Name: "bar",
Before: []string{"foo"},
},
"baz": {
Snap: info,
Name: "baz",
After: []string{"foo"},
},
"zed": {
Snap: info,
Name: "zed",
},
})
}
func (s *YamlSuite) TestSnapYamlWatchdog(c *C) {
y := []byte(`
name: foo
version: 1.0
apps:
foo:
watchdog-timeout: 12s
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Check(info.Apps["foo"].WatchdogTimeout, Equals, timeout.Timeout(12*time.Second))
}
func (s *YamlSuite) TestLayout(c *C) {
y := []byte(`
name: foo
version: 1.0
layout:
/usr/share/foo:
bind: $SNAP/usr/share/foo
/usr/share/bar:
symlink: $SNAP/usr/share/bar
/etc/froz:
bind-file: $SNAP/etc/froz
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
c.Assert(info.Layout["/usr/share/foo"], DeepEquals, &snap.Layout{
Snap: info,
Path: "/usr/share/foo",
Bind: "$SNAP/usr/share/foo",
User: "root",
Group: "root",
Mode: 0755,
})
c.Assert(info.Layout["/usr/share/bar"], DeepEquals, &snap.Layout{
Snap: info,
Path: "/usr/share/bar",
Symlink: "$SNAP/usr/share/bar",
User: "root",
Group: "root",
Mode: 0755,
})
c.Assert(info.Layout["/etc/froz"], DeepEquals, &snap.Layout{
Snap: info,
Path: "/etc/froz",
BindFile: "$SNAP/etc/froz",
User: "root",
Group: "root",
Mode: 0755,
})
}
func (s *YamlSuite) TestLayoutsWithTypo(c *C) {
y := []byte(`
name: foo
version: 1.0
layouts:
/usr/share/foo:
bind: $SNAP/usr/share/foo
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, ErrorMatches, `cannot parse snap.yaml: typo detected: use singular "layout" instead of plural "layouts"`)
c.Assert(info, IsNil)
}
func (s *YamlSuite) TestSnapYamlAppTimer(c *C) {
y := []byte(`name: wat
version: 42
apps:
foo:
daemon: oneshot
timer: mon,10:00-12:00
`)
info, err := snap.InfoFromSnapYaml(y)
c.Assert(err, IsNil)
app := info.Apps["foo"]
c.Check(app.Timer, DeepEquals, &snap.TimerInfo{App: app, Timer: "mon,10:00-12:00"})
}
func (s *YamlSuite) TestSnapYamlAppAutostart(c *C) {
yAutostart := []byte(`name: wat
version: 42
apps:
foo:
command: bin/foo
autostart: foo.desktop
`)
info, err := snap.InfoFromSnapYaml(yAutostart)
c.Assert(err, IsNil)
app := info.Apps["foo"]
c.Check(app.Autostart, Equals, "foo.desktop")
yNoAutostart := []byte(`name: wat
version: 42
apps:
foo:
command: bin/foo
`)
info, err = snap.InfoFromSnapYaml(yNoAutostart)
c.Assert(err, IsNil)
app = info.Apps["foo"]
c.Check(app.Autostart, Equals, "")
}
func (s *YamlSuite) TestSnapYamlAppCommonID(c *C) {
yAutostart := []byte(`name: wat
version: 42
apps:
foo:
command: bin/foo
common-id: org.foo
bar:
command: bin/foo
common-id: org.bar
baz:
command: bin/foo
`)
info, err := snap.InfoFromSnapYaml(yAutostart)
c.Assert(err, IsNil)
c.Check(info.Apps["foo"].CommonID, Equals, "org.foo")
c.Check(info.Apps["bar"].CommonID, Equals, "org.bar")
c.Check(info.Apps["baz"].CommonID, Equals, "")
c.Assert(info.CommonIDs, HasLen, 2)
c.Assert((info.CommonIDs[0] == "org.foo" && info.CommonIDs[1] == "org.bar") ||
(info.CommonIDs[1] == "org.foo" && info.CommonIDs[0] == "org.bar"),
Equals,
true)
}
func (s *YamlSuite) TestSnapYamlCommandChain(c *C) {
yAutostart := []byte(`name: wat
version: 42
apps:
foo:
command: bin/foo
command-chain: [chain1, chain2]
hooks:
configure:
command-chain: [hookchain1, hookchain2]
`)
info, err := snap.InfoFromSnapYaml(yAutostart)
c.Assert(err, IsNil)
app := info.Apps["foo"]
c.Check(app.CommandChain, DeepEquals, []string{"chain1", "chain2"})
hook := info.Hooks["configure"]
c.Check(hook.CommandChain, DeepEquals, []string{"hookchain1", "hookchain2"})
}
func (s *YamlSuite) TestSnapYamlRestartDelay(c *C) {
yAutostart := []byte(`name: wat
version: 42
apps:
foo:
command: bin/foo
daemon: simple
restart-delay: 12s
`)
info, err := snap.InfoFromSnapYaml(yAutostart)
c.Assert(err, IsNil)
app := info.Apps["foo"]
c.Assert(app, NotNil)
c.Check(app.RestartDelay, Equals, timeout.Timeout(12*time.Second))
}
snapd-2.37.4~14.04.1/snap/gadget_test.go 0000664 0000000 0000000 00000024475 13435556260 014337 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
"io/ioutil"
"path/filepath"
"strings"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
)
type gadgetYamlTestSuite struct {
}
var _ = Suite(&gadgetYamlTestSuite{})
var mockGadgetSnapYaml = `
name: canonical-pc
type: gadget
`
var mockGadgetYaml = []byte(`
defaults:
system:
something: true
connections:
- plug: snapid1:plg1
slot: snapid2:slot
- plug: snapid3:process-control
- plug: snapid4:pctl4
slot: system:process-control
volumes:
volumename:
schema: mbr
bootloader: u-boot
id: id,guid
structure:
- filesystem-label: system-boot
offset: 12345
offset-write: 777
size: 88888
type: id,guid
id: id,guid
filesystem: vfat
content:
- source: subdir/
target: /
unpack: false
- image: foo.img
offset: 4321
offset-write: 8888
size: 88888
unpack: false
`)
var mockMultiVolumeGadgetYaml = []byte(`
device-tree: frobinator-3000.dtb
device-tree-origin: kernel
volumes:
frobinator-3000-image:
bootloader: u-boot
schema: mbr
structure:
- name: system-boot
type: 0C
filesystem: vfat
filesystem-label: system-boot
size: 128M
role: system-boot
content:
- source: splash.bmp
target: .
- name: writable
type: 83
filesystem: ext4
filesystem-label: writable
size: 380M
role: system-data
u-boot-frobinator-3000:
structure:
- name: u-boot
type: bare
size: 623000
offset: 0
content:
- image: u-boot.imz
`)
var mockClassicGadgetYaml = []byte(`
defaults:
system:
something: true
otheridididididididididididididi:
foo:
bar: baz
`)
func (s *gadgetYamlTestSuite) SetUpTest(c *C) {
dirs.SetRootDir(c.MkDir())
}
func (s *gadgetYamlTestSuite) TearDownTest(c *C) {
dirs.SetRootDir("/")
}
func (s *gadgetYamlTestSuite) TestReadGadgetNotAGadget(c *C) {
info := snaptest.MockInfo(c, `
name: other
version: 0
`, &snap.SideInfo{Revision: snap.R(42)})
_, err := snap.ReadGadgetInfo(info, false)
c.Assert(err, ErrorMatches, "cannot read gadget snap details: not a gadget snap")
}
func (s *gadgetYamlTestSuite) TestReadGadgetYamlMissing(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)})
_, err := snap.ReadGadgetInfo(info, false)
c.Assert(err, ErrorMatches, ".*meta/gadget.yaml: no such file or directory")
}
func (s *gadgetYamlTestSuite) TestReadGadgetYamlOnClassicOptional(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)})
gi, err := snap.ReadGadgetInfo(info, true)
c.Assert(err, IsNil)
c.Check(gi, NotNil)
}
func (s *gadgetYamlTestSuite) TestReadGadgetYamlOnClassicEmptyIsValid(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)})
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), nil, 0644)
c.Assert(err, IsNil)
ginfo, err := snap.ReadGadgetInfo(info, true)
c.Assert(err, IsNil)
c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{})
}
func (s *gadgetYamlTestSuite) TestReadGadgetYamlOnClassicOnylDefaultsIsValid(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)})
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockClassicGadgetYaml, 0644)
c.Assert(err, IsNil)
ginfo, err := snap.ReadGadgetInfo(info, true)
c.Assert(err, IsNil)
c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{
Defaults: map[string]map[string]interface{}{
"system": {"something": true},
"otheridididididididididididididi": {"foo": map[string]interface{}{"bar": "baz"}},
},
})
}
func (s *gadgetYamlTestSuite) TestReadGadgetYamlValid(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)})
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYaml, 0644)
c.Assert(err, IsNil)
ginfo, err := snap.ReadGadgetInfo(info, false)
c.Assert(err, IsNil)
c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{
Defaults: map[string]map[string]interface{}{
"system": {"something": true},
},
Connections: []snap.GadgetConnection{
{Plug: snap.GadgetConnectionPlug{SnapID: "snapid1", Plug: "plg1"}, Slot: snap.GadgetConnectionSlot{SnapID: "snapid2", Slot: "slot"}},
{Plug: snap.GadgetConnectionPlug{SnapID: "snapid3", Plug: "process-control"}, Slot: snap.GadgetConnectionSlot{SnapID: "system", Slot: "process-control"}},
{Plug: snap.GadgetConnectionPlug{SnapID: "snapid4", Plug: "pctl4"}, Slot: snap.GadgetConnectionSlot{SnapID: "system", Slot: "process-control"}},
},
Volumes: map[string]snap.GadgetVolume{
"volumename": {
Schema: "mbr",
Bootloader: "u-boot",
ID: "id,guid",
Structure: []snap.VolumeStructure{
{
Label: "system-boot",
Offset: "12345",
OffsetWrite: "777",
Size: "88888",
Type: "id,guid",
ID: "id,guid",
Filesystem: "vfat",
Content: []snap.VolumeContent{
{
Source: "subdir/",
Target: "/",
Unpack: false,
},
{
Image: "foo.img",
Offset: "4321",
OffsetWrite: "8888",
Size: "88888",
Unpack: false,
},
},
},
},
},
},
})
}
func (s *gadgetYamlTestSuite) TestReadMultiVolumeGadgetYamlValid(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)})
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockMultiVolumeGadgetYaml, 0644)
c.Assert(err, IsNil)
ginfo, err := snap.ReadGadgetInfo(info, false)
c.Assert(err, IsNil)
c.Check(ginfo.Volumes, HasLen, 2)
c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{
Volumes: map[string]snap.GadgetVolume{
"frobinator-3000-image": {
Schema: "mbr",
Bootloader: "u-boot",
Structure: []snap.VolumeStructure{
{
Label: "system-boot",
Size: "128M",
Filesystem: "vfat",
Type: "0C",
Content: []snap.VolumeContent{
{
Source: "splash.bmp",
Target: ".",
},
},
},
{
Label: "writable",
Type: "83",
Filesystem: "ext4",
Size: "380M",
},
},
},
"u-boot-frobinator-3000": {
Structure: []snap.VolumeStructure{
{
Type: "bare",
Size: "623000",
Offset: "0",
Content: []snap.VolumeContent{
{
Image: "u-boot.imz",
},
},
},
},
},
},
})
}
func (s *gadgetYamlTestSuite) TestReadGadgetYamlInvalidBootloader(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)})
mockGadgetYamlBroken := []byte(`
volumes:
name:
bootloader: silo
`)
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYamlBroken, 0644)
c.Assert(err, IsNil)
_, err = snap.ReadGadgetInfo(info, false)
c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader must be one of grub, u-boot or android-boot")
}
func (s *gadgetYamlTestSuite) TestReadGadgetYamlEmptydBootloader(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)})
mockGadgetYamlBroken := []byte(`
volumes:
name:
bootloader:
`)
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYamlBroken, 0644)
c.Assert(err, IsNil)
_, err = snap.ReadGadgetInfo(info, false)
c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader not declared in any volume")
}
func (s *gadgetYamlTestSuite) TestReadGadgetYamlMissingBootloader(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)})
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), nil, 0644)
c.Assert(err, IsNil)
_, err = snap.ReadGadgetInfo(info, false)
c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader not declared in any volume")
}
func (s *gadgetYamlTestSuite) TestReadGadgetYamlInvalidDefaultsKey(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)})
mockGadgetYamlBroken := []byte(`
defaults:
foo:
x: 1
`)
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYamlBroken, 0644)
c.Assert(err, IsNil)
_, err = snap.ReadGadgetInfo(info, false)
c.Assert(err, ErrorMatches, `default stanza not keyed by "system" or snap-id: foo`)
}
func (s *gadgetYamlTestSuite) TestReadGadgetYamlInvalidConnection(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, &snap.SideInfo{Revision: snap.R(42)})
mockGadgetYamlBroken := `
connections:
- @INVALID@
`
tests := []struct {
invalidConn string
expectedErr string
}{
{``, `gadget connection plug cannot be empty`},
{`foo:bar baz:quux`, `(?s).*unmarshal errors:.*`},
{`plug: foo:`, `.*mapping values are not allowed in this context`},
{`plug: ":"`, `.*in gadget connection plug: expected "\(\|system\):name" not ":"`},
{`slot: "foo:"`, `.*in gadget connection slot: expected "\(\|system\):name" not "foo:"`},
{`slot: foo:bar`, `gadget connection plug cannot be empty`},
}
for _, t := range tests {
mockGadgetYamlBroken := strings.Replace(mockGadgetYamlBroken, "@INVALID@", t.invalidConn, 1)
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), []byte(mockGadgetYamlBroken), 0644)
c.Assert(err, IsNil)
_, err = snap.ReadGadgetInfo(info, false)
c.Check(err, ErrorMatches, t.expectedErr)
}
}
snapd-2.37.4~14.04.1/snap/validate.go 0000664 0000000 0000000 00000067753 13435556260 013644 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"unicode/utf8"
"github.com/snapcore/snapd/spdx"
"github.com/snapcore/snapd/strutil"
"github.com/snapcore/snapd/timeutil"
)
// Regular expressions describing correct identifiers.
var validHookName = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$")
// The fixed length of valid snap IDs.
const validSnapIDLength = 32
// almostValidName is part of snap and socket name validation.
// the full regexp we could use, "^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$", is
// O(2ⁿ) on the length of the string in python. An equivalent regexp that
// doesn't have the nested quantifiers that trip up Python's re would be
// "^(?:[a-z0-9]|(?<=[a-z0-9])-)*[a-z](?:[a-z0-9]|-(?=[a-z0-9]))*$", but Go's
// regexp package doesn't support look-aheads nor look-behinds, so in order to
// have a unified implementation in the Go and Python bits of the project
// we're doing it this way instead. Check the length (if applicable), check
// this regexp, then check the dashes.
// This still leaves sc_snap_name_validate (in cmd/snap-confine/snap.c) and
// snap_validate (cmd/snap-update-ns/bootstrap.c) with their own handcrafted
// validators.
var almostValidName = regexp.MustCompile("^[a-z0-9-]*[a-z][a-z0-9-]*$")
// validInstanceKey is a regular expression describing valid snap instance key
var validInstanceKey = regexp.MustCompile("^[a-z0-9]{1,10}$")
// isValidName checks snap and socket socket identifiers.
func isValidName(name string) bool {
if !almostValidName.MatchString(name) {
return false
}
if name[0] == '-' || name[len(name)-1] == '-' || strings.Contains(name, "--") {
return false
}
return true
}
// ValidateInstanceName checks if a string can be used as a snap instance name.
func ValidateInstanceName(instanceName string) error {
// NOTE: This function should be synchronized with the two other
// implementations: sc_instance_name_validate and validate_instance_name .
pos := strings.IndexByte(instanceName, '_')
if pos == -1 {
// just store name
return ValidateName(instanceName)
}
storeName := instanceName[:pos]
instanceKey := instanceName[pos+1:]
if err := ValidateName(storeName); err != nil {
return err
}
if !validInstanceKey.MatchString(instanceKey) {
return fmt.Errorf("invalid instance key: %q", instanceKey)
}
return nil
}
// ValidateName checks if a string can be used as a snap name.
func ValidateName(name string) error {
// NOTE: This function should be synchronized with the two other
// implementations: sc_snap_name_validate and validate_snap_name .
if len(name) < 2 || len(name) > 40 || !isValidName(name) {
return fmt.Errorf("invalid snap name: %q", name)
}
return nil
}
// Regular expression describing correct plug, slot and interface names.
var validPlugSlotIfaceName = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$")
// ValidatePlugName checks if a string can be used as a slot name.
//
// Slot names and plug names within one snap must have unique names.
// This is not enforced by this function but is enforced by snap-level
// validation.
func ValidatePlugName(name string) error {
if !validPlugSlotIfaceName.MatchString(name) {
return fmt.Errorf("invalid plug name: %q", name)
}
return nil
}
// ValidateSlotName checks if a string can be used as a slot name.
//
// Slot names and plug names within one snap must have unique names.
// This is not enforced by this function but is enforced by snap-level
// validation.
func ValidateSlotName(name string) error {
if !validPlugSlotIfaceName.MatchString(name) {
return fmt.Errorf("invalid slot name: %q", name)
}
return nil
}
// ValidateInterfaceName checks if a string can be used as an interface name.
func ValidateInterfaceName(name string) error {
if !validPlugSlotIfaceName.MatchString(name) {
return fmt.Errorf("invalid interface name: %q", name)
}
return nil
}
// NB keep this in sync with snapcraft and the review tools :-)
var isValidVersion = regexp.MustCompile("^[a-zA-Z0-9](?:[a-zA-Z0-9:.+~-]{0,30}[a-zA-Z0-9+~])?$").MatchString
var isNonGraphicalASCII = regexp.MustCompile("[^[:graph:]]").MatchString
var isInvalidFirstVersionChar = regexp.MustCompile("^[^a-zA-Z0-9]").MatchString
var isInvalidLastVersionChar = regexp.MustCompile("[^a-zA-Z0-9+~]$").MatchString
var invalidMiddleVersionChars = regexp.MustCompile("[^a-zA-Z0-9:.+~-]+").FindAllString
// ValidateVersion checks if a string is a valid snap version.
func ValidateVersion(version string) error {
if !isValidVersion(version) {
// maybe it was too short?
if len(version) == 0 {
return errors.New("invalid snap version: cannot be empty")
}
if isNonGraphicalASCII(version) {
// note that while this way of quoting the version can produce ugly
// output in some cases (e.g. if you're trying to set a version to
// "hello😁", seeing “invalid version "hello😁"” could be clearer than
// “invalid snap version "hello\U0001f601"”), in a lot of more
// interesting cases you _need_ to have the thing that's not ASCII
// pointed out: homoglyphs and near-homoglyphs are too hard to spot
// otherwise. Take for example a version of "аерс". Or "v1.0‑x".
return fmt.Errorf("invalid snap version %s: must be printable, non-whitespace ASCII",
strconv.QuoteToASCII(version))
}
// now we know it's a non-empty ASCII string, we can get serious
var reasons []string
// ... too long?
if len(version) > 32 {
reasons = append(reasons, fmt.Sprintf("cannot be longer than 32 characters (got: %d)", len(version)))
}
// started with a symbol?
if isInvalidFirstVersionChar(version) {
// note that we can only say version[0] because we know it's ASCII :-)
reasons = append(reasons, fmt.Sprintf("must start with an ASCII alphanumeric (and not %q)", version[0]))
}
if len(version) > 1 {
if isInvalidLastVersionChar(version) {
tpl := "must end with an ASCII alphanumeric or one of '+' or '~' (and not %q)"
reasons = append(reasons, fmt.Sprintf(tpl, version[len(version)-1]))
}
if len(version) > 2 {
if all := invalidMiddleVersionChars(version[1:len(version)-1], -1); len(all) > 0 {
reasons = append(reasons, fmt.Sprintf("contains invalid characters: %s", strutil.Quoted(all)))
}
}
}
switch len(reasons) {
case 0:
// huh
return fmt.Errorf("invalid snap version %q", version)
case 1:
return fmt.Errorf("invalid snap version %q: %s", version, reasons[0])
default:
reasons, last := reasons[:len(reasons)-1], reasons[len(reasons)-1]
return fmt.Errorf("invalid snap version %q: %s, and %s", version, strings.Join(reasons, ", "), last)
}
}
return nil
}
// ValidateLicense checks if a string is a valid SPDX expression.
func ValidateLicense(license string) error {
if err := spdx.ValidateLicense(license); err != nil {
return fmt.Errorf("cannot validate license %q: %s", license, err)
}
return nil
}
// ValidateHook validates the content of the given HookInfo
func ValidateHook(hook *HookInfo) error {
valid := validHookName.MatchString(hook.Name)
if !valid {
return fmt.Errorf("invalid hook name: %q", hook.Name)
}
// Also validate the command chain
for _, value := range hook.CommandChain {
if !commandChainContentWhitelist.MatchString(value) {
return fmt.Errorf("hook command-chain contains illegal %q (legal: '%s')", value, commandChainContentWhitelist)
}
}
return nil
}
var validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$")
// ValidateAlias checks if a string can be used as an alias name.
func ValidateAlias(alias string) error {
valid := validAlias.MatchString(alias)
if !valid {
return fmt.Errorf("invalid alias name: %q", alias)
}
return nil
}
// validateSocketName checks if a string ca be used as a name for a socket (for
// socket activation).
func validateSocketName(name string) error {
if !isValidName(name) {
return fmt.Errorf("invalid socket name: %q", name)
}
return nil
}
// validateSocketmode checks that the socket mode is a valid file mode.
func validateSocketMode(mode os.FileMode) error {
if mode > 0777 {
return fmt.Errorf("cannot use mode: %04o", mode)
}
return nil
}
// validateSocketAddr checks that the value of socket addresses.
func validateSocketAddr(socket *SocketInfo, fieldName string, address string) error {
if address == "" {
return fmt.Errorf("%q is not defined", fieldName)
}
switch address[0] {
case '/', '$':
return validateSocketAddrPath(socket, fieldName, address)
case '@':
return validateSocketAddrAbstract(socket, fieldName, address)
default:
return validateSocketAddrNet(socket, fieldName, address)
}
}
func validateSocketAddrPath(socket *SocketInfo, fieldName string, path string) error {
if clean := filepath.Clean(path); clean != path {
return fmt.Errorf("invalid %q: %q should be written as %q", fieldName, path, clean)
}
if !(strings.HasPrefix(path, "$SNAP_DATA/") || strings.HasPrefix(path, "$SNAP_COMMON/")) {
return fmt.Errorf(
"invalid %q: must have a prefix of $SNAP_DATA or $SNAP_COMMON", fieldName)
}
return nil
}
func validateSocketAddrAbstract(socket *SocketInfo, fieldName string, path string) error {
// this comes from snap declaration, so the prefix can only be the snap
// name at this point
prefix := fmt.Sprintf("@snap.%s.", socket.App.Snap.SnapName())
if !strings.HasPrefix(path, prefix) {
return fmt.Errorf("path for %q must be prefixed with %q", fieldName, prefix)
}
return nil
}
func validateSocketAddrNet(socket *SocketInfo, fieldName string, address string) error {
lastIndex := strings.LastIndex(address, ":")
if lastIndex >= 0 {
if err := validateSocketAddrNetHost(socket, fieldName, address[:lastIndex]); err != nil {
return err
}
return validateSocketAddrNetPort(socket, fieldName, address[lastIndex+1:])
}
// Address only contains a port
return validateSocketAddrNetPort(socket, fieldName, address)
}
func validateSocketAddrNetHost(socket *SocketInfo, fieldName string, address string) error {
validAddresses := []string{"127.0.0.1", "[::1]", "[::]"}
for _, valid := range validAddresses {
if address == valid {
return nil
}
}
return fmt.Errorf("invalid %q address %q, must be one of: %s", fieldName, address, strings.Join(validAddresses, ", "))
}
func validateSocketAddrNetPort(socket *SocketInfo, fieldName string, port string) error {
var val uint64
var err error
retErr := fmt.Errorf("invalid %q port number %q", fieldName, port)
if val, err = strconv.ParseUint(port, 10, 16); err != nil {
return retErr
}
if val < 1 || val > 65535 {
return retErr
}
return nil
}
func validateDescription(descr string) error {
if count := utf8.RuneCountInString(descr); count > 4096 {
return fmt.Errorf("description can have up to 4096 codepoints, got %d", count)
}
return nil
}
func validateTitle(title string) error {
if count := utf8.RuneCountInString(title); count > 40 {
return fmt.Errorf("title can have up to 40 codepoints, got %d", count)
}
return nil
}
// Validate verifies the content in the info.
func Validate(info *Info) error {
name := info.InstanceName()
if name == "" {
return errors.New("snap name cannot be empty")
}
if err := ValidateName(info.SnapName()); err != nil {
return err
}
if err := ValidateInstanceName(name); err != nil {
return err
}
if err := validateTitle(info.Title()); err != nil {
return err
}
if err := validateDescription(info.Description()); err != nil {
return err
}
if err := ValidateVersion(info.Version); err != nil {
return err
}
if err := info.Epoch.Validate(); err != nil {
return err
}
if license := info.License; license != "" {
if err := ValidateLicense(license); err != nil {
return err
}
}
// validate app entries
for _, app := range info.Apps {
if err := ValidateApp(app); err != nil {
return fmt.Errorf("invalid definition of application %q: %v", app.Name, err)
}
}
// validate apps ordering according to after/before
if err := validateAppOrderCycles(info.Services()); err != nil {
return err
}
// validate aliases
for alias, app := range info.LegacyAliases {
if !validAlias.MatchString(alias) {
return fmt.Errorf("cannot have %q as alias name for app %q - use only letters, digits, dash, underscore and dot characters", alias, app.Name)
}
}
// validate hook entries
for _, hook := range info.Hooks {
if err := ValidateHook(hook); err != nil {
return err
}
}
// Ensure that plugs and slots have appropriate names and interface names.
if err := plugsSlotsInterfacesNames(info); err != nil {
return err
}
// Ensure that plug and slot have unique names.
if err := plugsSlotsUniqueNames(info); err != nil {
return err
}
// validate that bases do not have base fields
if info.Type == TypeOS || info.Type == TypeBase {
if info.Base != "" {
return fmt.Errorf(`cannot have "base" field on %q snap %q`, info.Type, info.InstanceName())
}
}
// ensure that common-id(s) are unique
if err := ValidateCommonIDs(info); err != nil {
return err
}
return ValidateLayoutAll(info)
}
// ValidateLayoutAll validates the consistency of all the layout elements in a snap.
func ValidateLayoutAll(info *Info) error {
paths := make([]string, 0, len(info.Layout))
for _, layout := range info.Layout {
paths = append(paths, layout.Path)
}
sort.Strings(paths)
// Validate that each source path is used consistently as a file or as a directory.
sourceKindMap := make(map[string]string)
for _, path := range paths {
layout := info.Layout[path]
if layout.Bind != "" {
// Layout refers to a directory.
sourcePath := info.ExpandSnapVariables(layout.Bind)
if kind, ok := sourceKindMap[sourcePath]; ok {
if kind != "dir" {
return fmt.Errorf("layout %q refers to directory %q but another layout treats it as file", layout.Path, layout.Bind)
}
}
sourceKindMap[sourcePath] = "dir"
}
if layout.BindFile != "" {
// Layout refers to a file.
sourcePath := info.ExpandSnapVariables(layout.BindFile)
if kind, ok := sourceKindMap[sourcePath]; ok {
if kind != "file" {
return fmt.Errorf("layout %q refers to file %q but another layout treats it as a directory", layout.Path, layout.BindFile)
}
}
sourceKindMap[sourcePath] = "file"
}
}
// Validate each layout item and collect resulting constraints.
constraints := make([]LayoutConstraint, 0, len(info.Layout))
for _, path := range paths {
layout := info.Layout[path]
if err := ValidateLayout(layout, constraints); err != nil {
return err
}
constraints = append(constraints, layout.constraint())
}
return nil
}
func plugsSlotsInterfacesNames(info *Info) error {
for plugName, plug := range info.Plugs {
if err := ValidatePlugName(plugName); err != nil {
return err
}
if err := ValidateInterfaceName(plug.Interface); err != nil {
return fmt.Errorf("invalid interface name %q for plug %q", plug.Interface, plugName)
}
}
for slotName, slot := range info.Slots {
if err := ValidateSlotName(slotName); err != nil {
return err
}
if err := ValidateInterfaceName(slot.Interface); err != nil {
return fmt.Errorf("invalid interface name %q for slot %q", slot.Interface, slotName)
}
}
return nil
}
func plugsSlotsUniqueNames(info *Info) error {
// we could choose the smaller collection if we wanted to optimize this check
for plugName := range info.Plugs {
if info.Slots[plugName] != nil {
return fmt.Errorf("cannot have plug and slot with the same name: %q", plugName)
}
}
return nil
}
func validateField(name, cont string, whitelist *regexp.Regexp) error {
if !whitelist.MatchString(cont) {
return fmt.Errorf("app description field '%s' contains illegal %q (legal: '%s')", name, cont, whitelist)
}
return nil
}
func validateAppSocket(socket *SocketInfo) error {
if err := validateSocketName(socket.Name); err != nil {
return err
}
if err := validateSocketMode(socket.SocketMode); err != nil {
return err
}
return validateSocketAddr(socket, "listen-stream", socket.ListenStream)
}
// validateAppOrderCycles checks for cycles in app ordering dependencies
func validateAppOrderCycles(apps []*AppInfo) error {
if _, err := SortServices(apps); err != nil {
return err
}
return nil
}
func validateAppOrderNames(app *AppInfo, dependencies []string) error {
// we must be a service to request ordering
if len(dependencies) > 0 && !app.IsService() {
return errors.New("must be a service to define before/after ordering")
}
for _, dep := range dependencies {
// dependency is not defined
other, ok := app.Snap.Apps[dep]
if !ok {
return fmt.Errorf("before/after references a missing application %q", dep)
}
if !other.IsService() {
return fmt.Errorf("before/after references a non-service application %q", dep)
}
}
return nil
}
func validateAppWatchdog(app *AppInfo) error {
if app.WatchdogTimeout == 0 {
// no watchdog
return nil
}
if !app.IsService() {
return errors.New("watchdog-timeout is only applicable to services")
}
if app.WatchdogTimeout < 0 {
return errors.New("watchdog-timeout cannot be negative")
}
return nil
}
func validateAppTimer(app *AppInfo) error {
if app.Timer == nil {
return nil
}
if !app.IsService() {
return errors.New("timer is only applicable to services")
}
if _, err := timeutil.ParseSchedule(app.Timer.Timer); err != nil {
return fmt.Errorf("timer has invalid format: %v", err)
}
return nil
}
func validateAppRestart(app *AppInfo) error {
// app.RestartCond value is validated when unmarshalling
if app.RestartDelay == 0 && app.RestartCond == "" {
return nil
}
if app.RestartDelay != 0 {
if !app.IsService() {
return errors.New("restart-delay is only applicable to services")
}
if app.RestartDelay < 0 {
return errors.New("restart-delay cannot be negative")
}
}
if app.RestartCond != "" {
if !app.IsService() {
return errors.New("restart-condition is only applicable to services")
}
}
return nil
}
// appContentWhitelist is the whitelist of legal chars in the "apps"
// section of snap.yaml. Do not allow any of [',",`] here or snap-exec
// will get confused. chainContentWhitelist is the same, but for the
// command-chain, which also doesn't allow whitespace.
var appContentWhitelist = regexp.MustCompile(`^[A-Za-z0-9/. _#:$-]*$`)
var commandChainContentWhitelist = regexp.MustCompile(`^[A-Za-z0-9/._#:$-]*$`)
var validAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$").MatchString
// ValidAppName tells whether a string is a valid application name.
func ValidAppName(n string) bool {
return validAppName(n)
}
// ValidateApp verifies the content in the app info.
func ValidateApp(app *AppInfo) error {
switch app.Daemon {
case "", "simple", "forking", "oneshot", "dbus", "notify":
// valid
default:
return fmt.Errorf(`"daemon" field contains invalid value %q`, app.Daemon)
}
// Validate app name
if !ValidAppName(app.Name) {
return fmt.Errorf("cannot have %q as app name - use letters, digits, and dash as separator", app.Name)
}
// Validate the rest of the app info
checks := map[string]string{
"command": app.Command,
"stop-command": app.StopCommand,
"reload-command": app.ReloadCommand,
"post-stop-command": app.PostStopCommand,
"bus-name": app.BusName,
}
for name, value := range checks {
if err := validateField(name, value, appContentWhitelist); err != nil {
return err
}
}
// Also validate the command chain
for _, value := range app.CommandChain {
if err := validateField("command-chain", value, commandChainContentWhitelist); err != nil {
return err
}
}
// Socket activation requires the "network-bind" plug
if len(app.Sockets) > 0 {
if _, ok := app.Plugs["network-bind"]; !ok {
return fmt.Errorf(`"network-bind" interface plug is required when sockets are used`)
}
}
for _, socket := range app.Sockets {
if err := validateAppSocket(socket); err != nil {
return fmt.Errorf("invalid definition of socket %q: %v", socket.Name, err)
}
}
if err := validateAppRestart(app); err != nil {
return err
}
if err := validateAppOrderNames(app, app.Before); err != nil {
return err
}
if err := validateAppOrderNames(app, app.After); err != nil {
return err
}
if err := validateAppWatchdog(app); err != nil {
return err
}
// validate stop-mode
if err := app.StopMode.Validate(); err != nil {
return err
}
// validate refresh-mode
switch app.RefreshMode {
case "", "endure", "restart":
// valid
default:
return fmt.Errorf(`"refresh-mode" field contains invalid value %q`, app.RefreshMode)
}
if app.StopMode != "" && app.Daemon == "" {
return fmt.Errorf(`"stop-mode" cannot be used for %q, only for services`, app.Name)
}
if app.RefreshMode != "" && app.Daemon == "" {
return fmt.Errorf(`"refresh-mode" cannot be used for %q, only for services`, app.Name)
}
return validateAppTimer(app)
}
// ValidatePathVariables ensures that given path contains only $SNAP, $SNAP_DATA or $SNAP_COMMON.
func ValidatePathVariables(path string) error {
for path != "" {
start := strings.IndexRune(path, '$')
if start < 0 {
break
}
path = path[start+1:]
end := strings.IndexFunc(path, func(c rune) bool {
return (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '_'
})
if end < 0 {
end = len(path)
}
v := path[:end]
if v != "SNAP" && v != "SNAP_DATA" && v != "SNAP_COMMON" {
return fmt.Errorf("reference to unknown variable %q", "$"+v)
}
path = path[end:]
}
return nil
}
func isAbsAndClean(path string) bool {
return (filepath.IsAbs(path) || strings.HasPrefix(path, "$")) && filepath.Clean(path) == path
}
// LayoutConstraint abstracts validation of conflicting layout elements.
type LayoutConstraint interface {
IsOffLimits(path string) bool
}
// mountedTree represents a mounted file-system tree or a bind-mounted directory.
type mountedTree string
// IsOffLimits returns true if the mount point is (perhaps non-proper) prefix of a given path.
func (mountPoint mountedTree) IsOffLimits(path string) bool {
return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint)
}
// mountedFile represents a bind-mounted file.
type mountedFile string
// IsOffLimits returns true if the mount point is (perhaps non-proper) prefix of a given path.
func (mountPoint mountedFile) IsOffLimits(path string) bool {
return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint)
}
// symlinkFile represents a layout using symbolic link.
type symlinkFile string
// IsOffLimits returns true for mounted files if a path is identical to the path of the mount point.
func (mountPoint symlinkFile) IsOffLimits(path string) bool {
return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint)
}
func (layout *Layout) constraint() LayoutConstraint {
path := layout.Snap.ExpandSnapVariables(layout.Path)
if layout.Symlink != "" {
return symlinkFile(path)
} else if layout.BindFile != "" {
return mountedFile(path)
}
return mountedTree(path)
}
// ValidateLayout ensures that the given layout contains only valid subset of constructs.
func ValidateLayout(layout *Layout, constraints []LayoutConstraint) error {
si := layout.Snap
// Rules for validating layouts:
//
// * source of mount --bind must be in on of $SNAP, $SNAP_DATA or $SNAP_COMMON
// * target of symlink must in in one of $SNAP, $SNAP_DATA, or $SNAP_COMMON
// * may not mount on top of an existing layout mountpoint
mountPoint := layout.Path
if mountPoint == "" {
return errors.New("layout cannot use an empty path")
}
if err := ValidatePathVariables(mountPoint); err != nil {
return fmt.Errorf("layout %q uses invalid mount point: %s", layout.Path, err)
}
mountPoint = si.ExpandSnapVariables(mountPoint)
if !isAbsAndClean(mountPoint) {
return fmt.Errorf("layout %q uses invalid mount point: must be absolute and clean", layout.Path)
}
for _, path := range []string{"/proc", "/sys", "/dev", "/run", "/boot", "/lost+found", "/media", "/var/lib/snapd", "/var/snap"} {
// We use the mountedTree constraint as this has the right semantics.
if mountedTree(path).IsOffLimits(mountPoint) {
return fmt.Errorf("layout %q in an off-limits area", layout.Path)
}
}
for _, constraint := range constraints {
if constraint.IsOffLimits(mountPoint) {
return fmt.Errorf("layout %q underneath prior layout item %q", layout.Path, constraint)
}
}
var nused int
if layout.Bind != "" {
nused++
}
if layout.BindFile != "" {
nused++
}
if layout.Type != "" {
nused++
}
if layout.Symlink != "" {
nused++
}
if nused != 1 {
return fmt.Errorf("layout %q must define a bind mount, a filesystem mount or a symlink", layout.Path)
}
if layout.Bind != "" || layout.BindFile != "" {
mountSource := layout.Bind + layout.BindFile
if err := ValidatePathVariables(mountSource); err != nil {
return fmt.Errorf("layout %q uses invalid bind mount source %q: %s", layout.Path, mountSource, err)
}
mountSource = si.ExpandSnapVariables(mountSource)
if !isAbsAndClean(mountSource) {
return fmt.Errorf("layout %q uses invalid bind mount source %q: must be absolute and clean", layout.Path, mountSource)
}
// Bind mounts *must* use $SNAP, $SNAP_DATA or $SNAP_COMMON as bind
// mount source. This is done so that snaps cannot bypass restrictions
// by mounting something outside into their own space.
if !strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP")) &&
!strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP_DATA")) &&
!strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP_COMMON")) {
return fmt.Errorf("layout %q uses invalid bind mount source %q: must start with $SNAP, $SNAP_DATA or $SNAP_COMMON", layout.Path, mountSource)
}
}
switch layout.Type {
case "tmpfs":
case "":
// nothing to do
default:
return fmt.Errorf("layout %q uses invalid filesystem %q", layout.Path, layout.Type)
}
if layout.Symlink != "" {
oldname := layout.Symlink
if err := ValidatePathVariables(oldname); err != nil {
return fmt.Errorf("layout %q uses invalid symlink old name %q: %s", layout.Path, oldname, err)
}
oldname = si.ExpandSnapVariables(oldname)
if !isAbsAndClean(oldname) {
return fmt.Errorf("layout %q uses invalid symlink old name %q: must be absolute and clean", layout.Path, oldname)
}
// Symlinks *must* use $SNAP, $SNAP_DATA or $SNAP_COMMON as oldname.
// This is done so that snaps cannot attempt to bypass restrictions
// by mounting something outside into their own space.
if !strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP")) &&
!strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP_DATA")) &&
!strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP_COMMON")) {
return fmt.Errorf("layout %q uses invalid symlink old name %q: must start with $SNAP, $SNAP_DATA or $SNAP_COMMON", layout.Path, oldname)
}
}
// When new users and groups are supported those must be added to interfaces/mount/spec.go as well.
// For now only "root" is allowed (and default).
switch layout.User {
case "root", "":
// TODO: allow declared snap user and group names.
default:
return fmt.Errorf("layout %q uses invalid user %q", layout.Path, layout.User)
}
switch layout.Group {
case "root", "":
default:
return fmt.Errorf("layout %q uses invalid group %q", layout.Path, layout.Group)
}
if layout.Mode&01777 != layout.Mode {
return fmt.Errorf("layout %q uses invalid mode %#o", layout.Path, layout.Mode)
}
return nil
}
func ValidateCommonIDs(info *Info) error {
seen := make(map[string]string, len(info.Apps))
for _, app := range info.Apps {
if app.CommonID != "" {
if other, was := seen[app.CommonID]; was {
return fmt.Errorf("application %q common-id %q must be unique, already used by application %q",
app.Name, app.CommonID, other)
}
seen[app.CommonID] = app.Name
}
}
return nil
}
snapd-2.37.4~14.04.1/snap/snaptest/ 0000775 0000000 0000000 00000000000 13435556260 013343 5 ustar snapd-2.37.4~14.04.1/snap/snaptest/snaptest_test.go 0000664 0000000 0000000 00000015424 13435556260 016600 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snaptest_test
import (
"os"
"path/filepath"
"testing"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/testutil"
)
func TestSnapTest(t *testing.T) { TestingT(t) }
const sampleYaml = `
name: sample
version: 1
apps:
app:
command: foo
plugs:
network:
interface: network
`
type snapTestSuite struct{}
var _ = Suite(&snapTestSuite{})
func (s *snapTestSuite) SetUpTest(c *C) {
dirs.SetRootDir(c.MkDir())
}
func (s *snapTestSuite) TearDownTest(c *C) {
dirs.SetRootDir("")
}
func (s *snapTestSuite) TestMockSnap(c *C) {
snapInfo := snaptest.MockSnap(c, sampleYaml, &snap.SideInfo{Revision: snap.R(42)})
// Data from YAML is used
c.Check(snapInfo.InstanceName(), Equals, "sample")
// Data from SideInfo is used
c.Check(snapInfo.Revision, Equals, snap.R(42))
// The YAML is placed on disk
c.Check(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml"),
testutil.FileEquals, sampleYaml)
// More
c.Check(snapInfo.Apps["app"].Command, Equals, "foo")
c.Check(snapInfo.Plugs["network"].Interface, Equals, "network")
}
func (s *snapTestSuite) TestMockSnapInstance(c *C) {
snapInfo := snaptest.MockSnapInstance(c, "sample_instance", sampleYaml, &snap.SideInfo{Revision: snap.R(42)})
// Data from YAML and parameters is used
c.Check(snapInfo.InstanceName(), Equals, "sample_instance")
c.Check(snapInfo.SnapName(), Equals, "sample")
c.Check(snapInfo.InstanceKey, Equals, "instance")
// Data from SideInfo is used
c.Check(snapInfo.Revision, Equals, snap.R(42))
// The YAML is placed on disk
c.Check(filepath.Join(dirs.SnapMountDir, "sample_instance", "42", "meta", "snap.yaml"),
testutil.FileEquals, sampleYaml)
// More
c.Check(snapInfo.Apps["app"].Command, Equals, "foo")
c.Check(snapInfo.Plugs["network"].Interface, Equals, "network")
}
func (s *snapTestSuite) TestMockSnapCurrent(c *C) {
snapInfo := snaptest.MockSnapCurrent(c, sampleYaml, &snap.SideInfo{Revision: snap.R(42)})
// Data from YAML is used
c.Check(snapInfo.InstanceName(), Equals, "sample")
// Data from SideInfo is used
c.Check(snapInfo.Revision, Equals, snap.R(42))
// The YAML is placed on disk
c.Check(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml"),
testutil.FileEquals, sampleYaml)
link, err := os.Readlink(filepath.Join(dirs.SnapMountDir, "sample", "current"))
c.Check(err, IsNil)
c.Check(link, Equals, "42")
}
func (s *snapTestSuite) TestMockSnapInstanceCurrent(c *C) {
snapInfo := snaptest.MockSnapInstanceCurrent(c, "sample_instance", sampleYaml, &snap.SideInfo{Revision: snap.R(42)})
// Data from YAML and parameters is used
c.Check(snapInfo.InstanceName(), Equals, "sample_instance")
c.Check(snapInfo.SnapName(), Equals, "sample")
c.Check(snapInfo.InstanceKey, Equals, "instance")
// Data from SideInfo is used
c.Check(snapInfo.Revision, Equals, snap.R(42))
// The YAML is placed on disk
c.Check(filepath.Join(dirs.SnapMountDir, "sample_instance", "42", "meta", "snap.yaml"),
testutil.FileEquals, sampleYaml)
link, err := os.Readlink(filepath.Join(dirs.SnapMountDir, "sample_instance", "current"))
c.Check(err, IsNil)
c.Check(link, Equals, filepath.Join(dirs.SnapMountDir, "sample_instance", "42"))
}
func (s *snapTestSuite) TestMockInfo(c *C) {
snapInfo := snaptest.MockInfo(c, sampleYaml, &snap.SideInfo{Revision: snap.R(42)})
// Data from YAML is used
c.Check(snapInfo.InstanceName(), Equals, "sample")
// Data from SideInfo is used
c.Check(snapInfo.Revision, Equals, snap.R(42))
// The YAML is *not* placed on disk
_, err := os.Stat(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml"))
c.Assert(os.IsNotExist(err), Equals, true)
// More
c.Check(snapInfo.Apps["app"].Command, Equals, "foo")
c.Check(snapInfo.Plugs["network"].Interface, Equals, "network")
}
func (s *snapTestSuite) TestMockInvalidInfo(c *C) {
snapInfo := snaptest.MockInvalidInfo(c, sampleYaml+"\nslots:\n network:\n", &snap.SideInfo{Revision: snap.R(42)})
// Data from YAML is used
c.Check(snapInfo.InstanceName(), Equals, "sample")
// Data from SideInfo is used
c.Check(snapInfo.Revision, Equals, snap.R(42))
// The YAML is *not* placed on disk
_, err := os.Stat(filepath.Join(dirs.SnapMountDir, "sample", "42", "meta", "snap.yaml"))
c.Assert(os.IsNotExist(err), Equals, true)
// More
c.Check(snapInfo.Apps["app"].Command, Equals, "foo")
c.Check(snapInfo.Plugs["network"].Interface, Equals, "network")
c.Check(snapInfo.Slots["network"].Interface, Equals, "network")
// They info object is not valid
c.Check(snap.Validate(snapInfo), ErrorMatches, `cannot have plug and slot with the same name: "network"`)
}
func (s *snapTestSuite) TestRenameSlot(c *C) {
snapInfo := snaptest.MockInfo(c, `name: core
version: 0
slots:
old:
interface: interface
slot:
interface: interface
plugs:
plug:
interface: interface
apps:
app:
hooks:
configure:
`, nil)
// Rename "old" to "plug"
err := snaptest.RenameSlot(snapInfo, "old", "plug")
c.Assert(err, ErrorMatches, `cannot rename slot "old" to "plug": existing plug with that name`)
// Rename "old" to "slot"
err = snaptest.RenameSlot(snapInfo, "old", "slot")
c.Assert(err, ErrorMatches, `cannot rename slot "old" to "slot": existing slot with that name`)
// Rename "old" to "bad name"
err = snaptest.RenameSlot(snapInfo, "old", "bad name")
c.Assert(err, ErrorMatches, `cannot rename slot "old" to "bad name": invalid slot name: "bad name"`)
// Rename "old" to "new"
err = snaptest.RenameSlot(snapInfo, "old", "new")
c.Assert(err, IsNil)
// Check that there's no trace of the old slot name.
c.Assert(snapInfo.Slots["old"], IsNil)
c.Assert(snapInfo.Slots["new"], NotNil)
c.Assert(snapInfo.Slots["new"].Name, Equals, "new")
c.Assert(snapInfo.Apps["app"].Slots["old"], IsNil)
c.Assert(snapInfo.Apps["app"].Slots["new"], DeepEquals, snapInfo.Slots["new"])
c.Assert(snapInfo.Hooks["configure"].Slots["old"], IsNil)
c.Assert(snapInfo.Hooks["configure"].Slots["new"], DeepEquals, snapInfo.Slots["new"])
// Rename "old" to "new" (again)
err = snaptest.RenameSlot(snapInfo, "old", "new")
c.Assert(err, ErrorMatches, `cannot rename slot "old" to "new": no such slot`)
}
snapd-2.37.4~14.04.1/snap/snaptest/snaptest.go 0000664 0000000 0000000 00000021077 13435556260 015542 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
// Package snaptest contains helper functions for mocking snaps.
package snaptest
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"gopkg.in/check.v1"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/pack"
)
func mockSnap(c *check.C, instanceName, yamlText string, sideInfo *snap.SideInfo) *snap.Info {
c.Assert(sideInfo, check.Not(check.IsNil))
restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})
defer restoreSanitize()
// Parse the yaml (we need the Name).
snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText))
c.Assert(err, check.IsNil)
// Set SideInfo so that we can use MountDir below
snapInfo.SideInfo = *sideInfo
if instanceName != "" {
// Set the snap instance name
snapName, instanceKey := snap.SplitInstanceName(instanceName)
snapInfo.InstanceKey = instanceKey
// Make sure snap name/instance name checks out
c.Assert(snapInfo.InstanceName(), check.Equals, instanceName)
c.Assert(snapInfo.SnapName(), check.Equals, snapName)
}
// Put the YAML on disk, in the right spot.
metaDir := filepath.Join(snapInfo.MountDir(), "meta")
err = os.MkdirAll(metaDir, 0755)
c.Assert(err, check.IsNil)
err = ioutil.WriteFile(filepath.Join(metaDir, "snap.yaml"), []byte(yamlText), 0644)
c.Assert(err, check.IsNil)
// Write the .snap to disk
err = os.MkdirAll(filepath.Dir(snapInfo.MountFile()), 0755)
c.Assert(err, check.IsNil)
snapContents := fmt.Sprintf("%s-%s-%s", sideInfo.RealName, sideInfo.SnapID, sideInfo.Revision)
err = ioutil.WriteFile(snapInfo.MountFile(), []byte(snapContents), 0644)
c.Assert(err, check.IsNil)
snapInfo.Size = int64(len(snapContents))
return snapInfo
}
// MockSnap puts a snap.yaml file on disk so to mock an installed snap, based on the provided arguments.
//
// The caller is responsible for mocking root directory with dirs.SetRootDir()
// and for altering the overlord state if required.
func MockSnap(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info {
return mockSnap(c, "", yamlText, sideInfo)
}
// MockSnapInstance puts a snap.yaml file on disk so to mock an installed snap
// instance, based on the provided arguments.
//
// The caller is responsible for mocking root directory with dirs.SetRootDir()
// and for altering the overlord state if required.
func MockSnapInstance(c *check.C, instanceName, yamlText string, sideInfo *snap.SideInfo) *snap.Info {
return mockSnap(c, instanceName, yamlText, sideInfo)
}
// MockSnapCurrent does the same as MockSnap but additionally creates the
// 'current' symlink.
//
// The caller is responsible for mocking root directory with dirs.SetRootDir()
// and for altering the overlord state if required.
func MockSnapCurrent(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info {
si := MockSnap(c, yamlText, sideInfo)
err := os.Symlink(filepath.Base(si.MountDir()), filepath.Join(si.MountDir(), "../current"))
c.Assert(err, check.IsNil)
return si
}
// MockSnapInstanceCurrent does the same as MockSnapInstance but additionally
// creates the 'current' symlink.
//
// The caller is responsible for mocking root directory with dirs.SetRootDir()
// and for altering the overlord state if required.
func MockSnapInstanceCurrent(c *check.C, instanceName, yamlText string, sideInfo *snap.SideInfo) *snap.Info {
si := MockSnapInstance(c, instanceName, yamlText, sideInfo)
err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current"))
c.Assert(err, check.IsNil)
return si
}
// MockInfo parses the given snap.yaml text and returns a validated snap.Info object including the optional SideInfo.
//
// The result is just kept in memory, there is nothing kept on disk. If that is
// desired please use MockSnap instead.
func MockInfo(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info {
if sideInfo == nil {
sideInfo = &snap.SideInfo{}
}
restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})
defer restoreSanitize()
snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText))
c.Assert(err, check.IsNil)
snapInfo.SideInfo = *sideInfo
err = snap.Validate(snapInfo)
c.Assert(err, check.IsNil)
return snapInfo
}
// MockInvalidInfo parses the given snap.yaml text and returns the snap.Info object including the optional SideInfo.
//
// The result is just kept in memory, there is nothing kept on disk. If that is
// desired please use MockSnap instead.
func MockInvalidInfo(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info {
if sideInfo == nil {
sideInfo = &snap.SideInfo{}
}
restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})
defer restoreSanitize()
snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText))
c.Assert(err, check.IsNil)
snapInfo.SideInfo = *sideInfo
err = snap.Validate(snapInfo)
c.Assert(err, check.NotNil)
return snapInfo
}
// PopulateDir populates the directory with files specified as pairs of relative file path and its content. Useful to add extra files to a snap.
func PopulateDir(dir string, files [][]string) {
for _, filenameAndContent := range files {
filename := filenameAndContent[0]
content := filenameAndContent[1]
fpath := filepath.Join(dir, filename)
err := os.MkdirAll(filepath.Dir(fpath), 0755)
if err != nil {
panic(err)
}
err = ioutil.WriteFile(fpath, []byte(content), 0755)
if err != nil {
panic(err)
}
}
}
// MakeTestSnapWithFiles makes a squashfs snap file with the given
// snap.yaml content and optional extras files specified as pairs of
// relative file path and its content.
func MakeTestSnapWithFiles(c *check.C, snapYamlContent string, files [][]string) (snapFilePath string) {
tmpdir := c.MkDir()
snapSource := filepath.Join(tmpdir, "snapsrc")
err := os.MkdirAll(filepath.Join(snapSource, "meta"), 0755)
if err != nil {
panic(err)
}
snapYamlFn := filepath.Join(snapSource, "meta", "snap.yaml")
err = ioutil.WriteFile(snapYamlFn, []byte(snapYamlContent), 0644)
if err != nil {
panic(err)
}
PopulateDir(snapSource, files)
restoreSanitize := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})
defer restoreSanitize()
err = osutil.ChDir(snapSource, func() error {
var err error
snapFilePath, err = pack.Snap(snapSource, "", "")
return err
})
if err != nil {
panic(err)
}
return filepath.Join(snapSource, snapFilePath)
}
// MustParseChannel parses a string representing a store channel and
// includes the given architecture, if architecture is "" the system
// architecture is included. It panics on error.
func MustParseChannel(s string, architecture string) snap.Channel {
c, err := snap.ParseChannel(s, architecture)
if err != nil {
panic(err)
}
return c
}
// RenameSlot renames gives an existing slot a new name.
//
// The new slot name cannot clash with an existing plug or slot and must
// be a valid slot name.
func RenameSlot(snapInfo *snap.Info, oldName, newName string) error {
if snapInfo.Slots[oldName] == nil {
return fmt.Errorf("cannot rename slot %q to %q: no such slot", oldName, newName)
}
if err := snap.ValidateSlotName(newName); err != nil {
return fmt.Errorf("cannot rename slot %q to %q: %s", oldName, newName, err)
}
if oldName == newName {
return nil
}
if snapInfo.Slots[newName] != nil {
return fmt.Errorf("cannot rename slot %q to %q: existing slot with that name", oldName, newName)
}
if snapInfo.Plugs[newName] != nil {
return fmt.Errorf("cannot rename slot %q to %q: existing plug with that name", oldName, newName)
}
// Rename the slot.
slotInfo := snapInfo.Slots[oldName]
snapInfo.Slots[newName] = slotInfo
delete(snapInfo.Slots, oldName)
slotInfo.Name = newName
// Update references to the slot in all applications and hooks.
for _, appInfo := range snapInfo.Apps {
if _, ok := appInfo.Slots[oldName]; ok {
delete(appInfo.Slots, oldName)
appInfo.Slots[newName] = slotInfo
}
}
for _, hookInfo := range snapInfo.Hooks {
if _, ok := hookInfo.Slots[oldName]; ok {
delete(hookInfo.Slots, oldName)
hookInfo.Slots[newName] = slotInfo
}
}
return nil
}
snapd-2.37.4~14.04.1/snap/channel_test.go 0000664 0000000 0000000 00000012541 13435556260 014503 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/arch"
"github.com/snapcore/snapd/snap"
)
type storeChannelSuite struct{}
var _ = Suite(&storeChannelSuite{})
func (s storeChannelSuite) TestParseChannel(c *C) {
ch, err := snap.ParseChannel("stable", "")
c.Assert(err, IsNil)
c.Check(ch, DeepEquals, snap.Channel{
Architecture: arch.UbuntuArchitecture(),
Name: "stable",
Track: "",
Risk: "stable",
Branch: "",
})
ch, err = snap.ParseChannel("latest/stable", "")
c.Assert(err, IsNil)
c.Check(ch, DeepEquals, snap.Channel{
Architecture: arch.UbuntuArchitecture(),
Name: "stable",
Track: "",
Risk: "stable",
Branch: "",
})
ch, err = snap.ParseChannel("1.0/edge", "")
c.Assert(err, IsNil)
c.Check(ch, DeepEquals, snap.Channel{
Architecture: arch.UbuntuArchitecture(),
Name: "1.0/edge",
Track: "1.0",
Risk: "edge",
Branch: "",
})
ch, err = snap.ParseChannel("1.0", "")
c.Assert(err, IsNil)
c.Check(ch, DeepEquals, snap.Channel{
Architecture: arch.UbuntuArchitecture(),
Name: "1.0/stable",
Track: "1.0",
Risk: "stable",
Branch: "",
})
ch, err = snap.ParseChannel("1.0/beta/foo", "")
c.Assert(err, IsNil)
c.Check(ch, DeepEquals, snap.Channel{
Architecture: arch.UbuntuArchitecture(),
Name: "1.0/beta/foo",
Track: "1.0",
Risk: "beta",
Branch: "foo",
})
ch, err = snap.ParseChannel("candidate/foo", "")
c.Assert(err, IsNil)
c.Check(ch, DeepEquals, snap.Channel{
Architecture: arch.UbuntuArchitecture(),
Name: "candidate/foo",
Track: "",
Risk: "candidate",
Branch: "foo",
})
ch, err = snap.ParseChannel("candidate/foo", "other-arch")
c.Assert(err, IsNil)
c.Check(ch, DeepEquals, snap.Channel{
Architecture: "other-arch",
Name: "candidate/foo",
Track: "",
Risk: "candidate",
Branch: "foo",
})
}
func (s storeChannelSuite) TestClean(c *C) {
ch := snap.Channel{
Architecture: "arm64",
Track: "latest",
Name: "latest/stable",
Risk: "stable",
}
cleanedCh := ch.Clean()
c.Check(cleanedCh, Not(DeepEquals), c)
c.Check(cleanedCh, DeepEquals, snap.Channel{
Architecture: "arm64",
Track: "",
Name: "stable",
Risk: "stable",
})
}
func (s storeChannelSuite) TestParseChannelErrors(c *C) {
_, err := snap.ParseChannel("", "")
c.Check(err, ErrorMatches, "channel name cannot be empty")
_, err = snap.ParseChannel("1.0////", "")
c.Check(err, ErrorMatches, "channel name has too many components: 1.0////")
_, err = snap.ParseChannel("1.0/cand", "invalid risk in channel name: 1.0/cand")
c.Check(err, ErrorMatches, "invalid risk in channel name: 1.0/cand")
}
func (s *storeChannelSuite) TestString(c *C) {
tests := []struct {
channel string
str string
}{
{"stable", "stable"},
{"latest/stable", "stable"},
{"1.0/edge", "1.0/edge"},
{"1.0/beta/foo", "1.0/beta/foo"},
{"1.0", "1.0/stable"},
{"candidate/foo", "candidate/foo"},
}
for _, t := range tests {
ch, err := snap.ParseChannel(t.channel, "")
c.Assert(err, IsNil)
c.Check(ch.String(), Equals, t.str)
}
}
func (s *storeChannelSuite) TestFull(c *C) {
tests := []struct {
channel string
str string
}{
{"stable", "latest/stable"},
{"latest/stable", "latest/stable"},
{"1.0/edge", "1.0/edge"},
{"1.0/beta/foo", "1.0/beta/foo"},
{"1.0", "1.0/stable"},
{"candidate/foo", "latest/candidate/foo"},
}
for _, t := range tests {
ch, err := snap.ParseChannel(t.channel, "")
c.Assert(err, IsNil)
c.Check(ch.Full(), Equals, t.str)
}
}
func (s *storeChannelSuite) TestMatch(c *C) {
tests := []struct {
req string
c1 string
sameArch bool
res string
}{
{"stable", "stable", true, "architecture:track:risk"},
{"stable", "beta", true, "architecture:track"},
{"beta", "stable", true, "architecture:track:risk"},
{"stable", "edge", false, "track"},
{"edge", "stable", false, "track:risk"},
{"1.0/stable", "1.0/edge", true, "architecture:track"},
{"1.0/edge", "stable", true, "architecture:risk"},
{"1.0/edge", "stable", false, "risk"},
{"1.0/stable", "stable", false, "risk"},
{"1.0/stable", "beta", false, ""},
{"1.0/stable", "2.0/beta", false, ""},
{"2.0/stable", "2.0/beta", false, "track"},
{"1.0/stable", "2.0/beta", true, "architecture"},
}
for _, t := range tests {
reqArch := "amd64"
c1Arch := "amd64"
if !t.sameArch {
c1Arch = "arm64"
}
req, err := snap.ParseChannel(t.req, reqArch)
c.Assert(err, IsNil)
c1, err := snap.ParseChannel(t.c1, c1Arch)
c.Assert(err, IsNil)
c.Check(req.Match(&c1).String(), Equals, t.res)
}
}
snapd-2.37.4~14.04.1/snap/restartcond.go 0000664 0000000 0000000 00000004135 13435556260 014364 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"errors"
)
// RestartCondition encapsulates the different systemd 'restart' options
type RestartCondition string
// These are the supported restart conditions
const (
RestartNever RestartCondition = "never"
RestartOnSuccess RestartCondition = "on-success"
RestartOnFailure RestartCondition = "on-failure"
RestartOnAbnormal RestartCondition = "on-abnormal"
RestartOnAbort RestartCondition = "on-abort"
RestartOnWatchdog RestartCondition = "on-watchdog"
RestartAlways RestartCondition = "always"
)
var RestartMap = map[string]RestartCondition{
"no": RestartNever,
"never": RestartNever,
"on-success": RestartOnSuccess,
"on-failure": RestartOnFailure,
"on-abnormal": RestartOnAbnormal,
"on-abort": RestartOnAbort,
"on-watchdog": RestartOnWatchdog,
"always": RestartAlways,
}
// ErrUnknownRestartCondition is returned when trying to unmarshal an unknown restart condition
var ErrUnknownRestartCondition = errors.New("invalid restart condition")
func (rc RestartCondition) String() string {
if rc == "never" {
return "no"
}
return string(rc)
}
// UnmarshalYAML so RestartCondition implements yaml's Unmarshaler interface
func (rc *RestartCondition) UnmarshalYAML(unmarshal func(interface{}) error) error {
var v string
if err := unmarshal(&v); err != nil {
return err
}
nrc, ok := RestartMap[v]
if !ok {
return ErrUnknownRestartCondition
}
*rc = nrc
return nil
}
snapd-2.37.4~14.04.1/snap/container_test.go 0000664 0000000 0000000 00000026303 13435556260 015056 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
"io/ioutil"
"os"
"path/filepath"
"syscall"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snapdir"
"github.com/snapcore/snapd/testutil"
)
type FileSuite struct{}
var _ = Suite(&FileSuite{})
func (s *FileSuite) TestFileOpenForSnapDir(c *C) {
sd := c.MkDir()
snapYaml := filepath.Join(sd, "meta", "snap.yaml")
err := os.MkdirAll(filepath.Dir(snapYaml), 0755)
c.Assert(err, IsNil)
err = ioutil.WriteFile(snapYaml, []byte(`name: foo`), 0644)
c.Assert(err, IsNil)
f, err := snap.Open(sd)
c.Assert(err, IsNil)
c.Assert(f, FitsTypeOf, &snapdir.SnapDir{})
}
func (s *FileSuite) TestFileOpenForSnapDirErrors(c *C) {
_, err := snap.Open(c.MkDir())
c.Assert(err, FitsTypeOf, snap.NotSnapError{})
c.Assert(err, ErrorMatches, `"/.*" is not a snap or snapdir`)
}
type validateSuite struct {
testutil.BaseTest
log func(string, ...interface{})
}
var _ = Suite(&validateSuite{})
func discard(string, ...interface{}) {}
func (s *validateSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
}
func (s *validateSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
}
func (s *validateSuite) TestValidateContainerReallyEmptyFails(c *C) {
const yaml = `name: empty-snap
version: 1
`
d := c.MkDir()
// the snap dir is a 0700 directory with nothing in it
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(snapdir.New(d), info, discard)
c.Check(err, Equals, snap.ErrMissingPaths)
}
func (s *validateSuite) TestValidateContainerEmptyButBadPermFails(c *C) {
const yaml = `name: empty-snap
version: 1
`
d := c.MkDir()
stat, err := os.Stat(d)
c.Assert(err, IsNil)
c.Check(stat.Mode().Perm(), Equals, os.FileMode(0700)) // just to be sure
c.Assert(os.Mkdir(filepath.Join(d, "meta"), 0755), IsNil)
c.Assert(ioutil.WriteFile(filepath.Join(d, "meta", "snap.yaml"), nil, 0444), IsNil)
// snapdir has /meta/snap.yaml, but / is 0700
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(snapdir.New(d), info, discard)
c.Check(err, Equals, snap.ErrBadModes)
}
func (s *validateSuite) TestValidateContainerMissingSnapYamlFails(c *C) {
const yaml = `name: empty-snap
version: 1
`
d := c.MkDir()
c.Assert(os.Chmod(d, 0755), IsNil)
c.Assert(os.Mkdir(filepath.Join(d, "meta"), 0755), IsNil)
// snapdir's / and /meta are 0755 (i.e. OK), but no /meta/snap.yaml
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(snapdir.New(d), info, discard)
c.Check(err, Equals, snap.ErrMissingPaths)
}
func (s *validateSuite) TestValidateContainerSnapYamlBadPermsFails(c *C) {
const yaml = `name: empty-snap
version: 1
`
d := c.MkDir()
c.Assert(os.Chmod(d, 0755), IsNil)
c.Assert(os.Mkdir(filepath.Join(d, "meta"), 0755), IsNil)
c.Assert(ioutil.WriteFile(filepath.Join(d, "meta", "snap.yaml"), nil, 0), IsNil)
// snapdir's / and /meta are 0755 (i.e. OK),
// /meta/snap.yaml exists, but isn't readable
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(snapdir.New(d), info, discard)
c.Check(err, Equals, snap.ErrBadModes)
}
func (s *validateSuite) TestValidateContainerSnapYamlNonRegularFails(c *C) {
const yaml = `name: empty-snap
version: 1
`
d := c.MkDir()
c.Assert(os.Chmod(d, 0755), IsNil)
c.Assert(os.Mkdir(filepath.Join(d, "meta"), 0755), IsNil)
c.Assert(syscall.Mkfifo(filepath.Join(d, "meta", "snap.yaml"), 0444), IsNil)
// snapdir's / and /meta are 0755 (i.e. OK),
// /meta/snap.yaml exists, is readable, but isn't a file
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(snapdir.New(d), info, discard)
c.Check(err, Equals, snap.ErrBadModes)
}
// emptyContainer returns a minimal container that passes
// ValidateContainer: / and /meta exist and are 0755, and
// /meta/snap.yaml is a regular world-readable file.
func emptyContainer(c *C) *snapdir.SnapDir {
d := c.MkDir()
c.Assert(os.Chmod(d, 0755), IsNil)
c.Assert(os.Mkdir(filepath.Join(d, "meta"), 0755), IsNil)
c.Assert(ioutil.WriteFile(filepath.Join(d, "meta", "snap.yaml"), nil, 0444), IsNil)
return snapdir.New(d)
}
func (s *validateSuite) TestValidateContainerMinimalOKPermWorks(c *C) {
const yaml = `name: empty-snap
version: 1
`
d := emptyContainer(c)
// snapdir's / and /meta are 0755 (i.e. OK),
// /meta/snap.yaml exists, is readable regular file
// (this could be considered a test of emptyContainer)
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(d, info, discard)
c.Check(err, IsNil)
}
func (s *validateSuite) TestValidateContainerMissingAppsFails(c *C) {
const yaml = `name: empty-snap
version: 1
apps:
foo:
command: foo
`
d := emptyContainer(c)
// snapdir is empty: no apps
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(d, info, discard)
c.Check(err, Equals, snap.ErrMissingPaths)
}
func (s *validateSuite) TestValidateContainerBadAppPermsFails(c *C) {
const yaml = `name: empty-snap
version: 1
apps:
foo:
command: foo
`
d := emptyContainer(c)
c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "foo"), nil, 0444), IsNil)
// snapdir contains the app, but the app is not executable
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(d, info, discard)
c.Check(err, Equals, snap.ErrBadModes)
}
func (s *validateSuite) TestValidateContainerBadAppDirPermsFails(c *C) {
const yaml = `name: empty-snap
version: 1
apps:
foo:
command: apps/foo
`
d := emptyContainer(c)
c.Assert(os.Mkdir(filepath.Join(d.Path(), "apps"), 0700), IsNil)
c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "apps", "foo"), nil, 0555), IsNil)
// snapdir contains executable app, but path to executable isn't rx
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(d, info, discard)
c.Check(err, Equals, snap.ErrBadModes)
}
func (s *validateSuite) TestValidateContainerBadSvcPermsFails(c *C) {
const yaml = `name: empty-snap
version: 1
apps:
bar:
command: svcs/bar
daemon: simple
`
d := emptyContainer(c)
c.Assert(os.Mkdir(filepath.Join(d.Path(), "svcs"), 0755), IsNil)
c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "svcs", "bar"), nil, 0), IsNil)
// snapdir contains service, but it isn't executable
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(d, info, discard)
c.Check(err, Equals, snap.ErrBadModes)
}
func (s *validateSuite) TestValidateContainerCompleterFails(c *C) {
const yaml = `name: empty-snap
version: 1
apps:
foo:
command: cmds/foo
completer: comp/foo.sh
`
d := emptyContainer(c)
c.Assert(os.Mkdir(filepath.Join(d.Path(), "cmds"), 0755), IsNil)
c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "cmds", "foo"), nil, 0555), IsNil)
c.Assert(os.Mkdir(filepath.Join(d.Path(), "comp"), 0755), IsNil)
// snapdir contains executable app, in a rx path, but refers
// to a completer that doesn't exist
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(d, info, discard)
c.Check(err, Equals, snap.ErrMissingPaths)
}
func (s *validateSuite) TestValidateContainerBadAppPathOK(c *C) {
// we actually support this, but don't validate it here
const yaml = `name: empty-snap
version: 1
apps:
foo:
command: ../../../bin/echo
`
d := emptyContainer(c)
// snapdir does not contain the app, but the command is
// "outside" so it might be OK
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(d, info, discard)
c.Check(err, IsNil)
}
func (s *validateSuite) TestValidateContainerSymlinksFails(c *C) {
c.Skip("checking symlink targets not implemented yet")
const yaml = `name: empty-snap
version: 1
apps:
foo:
command: foo
`
d := emptyContainer(c)
fn := filepath.Join(d.Path(), "foo")
c.Assert(ioutil.WriteFile(fn+".real", nil, 0444), IsNil)
c.Assert(os.Symlink(fn+".real", fn), IsNil)
// snapdir contains a command that's a symlink to a file that's not world-rx
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(d, info, discard)
c.Check(err, Equals, snap.ErrBadModes)
}
func (s *validateSuite) TestValidateContainerSymlinksOK(c *C) {
const yaml = `name: empty-snap
version: 1
apps:
foo:
command: foo
`
d := emptyContainer(c)
fn := filepath.Join(d.Path(), "foo")
c.Assert(ioutil.WriteFile(fn+".real", nil, 0555), IsNil)
c.Assert(os.Symlink(fn+".real", fn), IsNil)
// snapdir contains a command that's a symlink to a file that's world-rx
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(d, info, discard)
c.Check(err, IsNil)
}
func (s *validateSuite) TestValidateContainerAppsOK(c *C) {
const yaml = `name: empty-snap
version: 1
apps:
foo:
command: cmds/foo
completer: comp/foo.sh
bar:
command: svcs/bar
daemon: simple
baz:
command: cmds/foo --with=baz
quux:
command: cmds/foo
daemon: simple
meep:
command: comp/foo.sh
daemon: simple
`
d := emptyContainer(c)
c.Assert(os.Mkdir(filepath.Join(d.Path(), "cmds"), 0755), IsNil)
c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "cmds", "foo"), nil, 0555), IsNil)
c.Assert(os.Mkdir(filepath.Join(d.Path(), "comp"), 0755), IsNil)
c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "comp", "foo.sh"), nil, 0555), IsNil)
c.Assert(os.Mkdir(filepath.Join(d.Path(), "svcs"), 0700), IsNil)
c.Assert(ioutil.WriteFile(filepath.Join(d.Path(), "svcs", "bar"), nil, 0500), IsNil)
c.Assert(os.Mkdir(filepath.Join(d.Path(), "garbage"), 0755), IsNil)
c.Assert(os.Mkdir(filepath.Join(d.Path(), "garbage", "zero"), 0), IsNil)
defer os.Chmod(filepath.Join(d.Path(), "garbage", "zero"), 0755)
// snapdir contains:
// * a command that's world-rx, and its directory is
// world-rx, and its completer is world-r in a world-rx
// directory
// * a service that's root-executable, and its directory is
// not readable nor searchable - and that's OK! (NOTE as
// this test should pass as non-rooot, the directory is 0700
// instead of 0000)
// * a command with arguments
// * a service that is also a command
// * a service that is also a completer (WAT)
// * an extra directory only root can look at (this would fail
// if not running the suite as root, and SkipDir didn't
// work)
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
err = snap.ValidateContainer(d, info, discard)
c.Check(err, IsNil)
}
snapd-2.37.4~14.04.1/snap/seed_yaml.go 0000664 0000000 0000000 00000004351 13435556260 013776 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"fmt"
"io/ioutil"
"strings"
"gopkg.in/yaml.v2"
"github.com/snapcore/snapd/osutil"
)
// SeedSnap points to a snap in the seed to install, together with
// assertions (or alone if unasserted is true) it will be used to
// drive the installation and ultimately set SideInfo/SnapState for it.
type SeedSnap struct {
Name string `yaml:"name"`
// cross-reference/audit
SnapID string `yaml:"snap-id,omitempty"`
// bits that are orthongonal/not in assertions
Channel string `yaml:"channel,omitempty"`
DevMode bool `yaml:"devmode,omitempty"`
Classic bool `yaml:"classic,omitempty"`
Private bool `yaml:"private,omitempty"`
Contact string `yaml:"contact,omitempty"`
// no assertions are available in the seed for this snap
Unasserted bool `yaml:"unasserted,omitempty"`
File string `yaml:"file"`
}
type Seed struct {
Snaps []*SeedSnap `yaml:"snaps"`
}
func ReadSeedYaml(fn string) (*Seed, error) {
yamlData, err := ioutil.ReadFile(fn)
if err != nil {
return nil, fmt.Errorf("cannot read seed yaml: %s", fn)
}
var seed Seed
if err := yaml.Unmarshal(yamlData, &seed); err != nil {
return nil, fmt.Errorf("cannot unmarshal %q: %s", yamlData, err)
}
// validate
for _, sn := range seed.Snaps {
if strings.Contains(sn.File, "/") {
return nil, fmt.Errorf("%q must be a filename, not a path", sn.File)
}
}
return &seed, nil
}
func (seed *Seed) Write(seedFn string) error {
data, err := yaml.Marshal(&seed)
if err != nil {
return err
}
if err := osutil.AtomicWriteFile(seedFn, data, 0644, 0); err != nil {
return err
}
return nil
}
snapd-2.37.4~14.04.1/snap/validate_test.go 0000664 0000000 0000000 00000127647 13435556260 014702 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
"fmt"
"regexp"
"strconv"
"strings"
. "gopkg.in/check.v1"
. "github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/testutil"
)
type ValidateSuite struct {
testutil.BaseTest
}
var _ = Suite(&ValidateSuite{})
func createSampleApp() *AppInfo {
socket := &SocketInfo{
Name: "sock",
ListenStream: "$SNAP_COMMON/socket",
}
app := &AppInfo{
Snap: &Info{
SideInfo: SideInfo{
RealName: "mysnap",
Revision: R(20),
},
},
Name: "foo",
Plugs: map[string]*PlugInfo{"network-bind": {}},
Sockets: map[string]*SocketInfo{
"sock": socket,
},
}
socket.App = app
return app
}
func (s *ValidateSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
s.BaseTest.AddCleanup(MockSanitizePlugsSlots(func(snapInfo *Info) {}))
}
func (s *ValidateSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
}
func (s *ValidateSuite) TestValidateName(c *C) {
validNames := []string{
"aa", "aaa", "aaaa",
"a-a", "aa-a", "a-aa", "a-b-c",
"a0", "a-0", "a-0a",
"01game", "1-or-2",
// a regexp stresser
"u-94903713687486543234157734673284536758",
}
for _, name := range validNames {
err := ValidateName(name)
c.Assert(err, IsNil)
}
invalidNames := []string{
// name cannot be empty
"",
// too short (min 2 chars)
"a",
// names cannot be too long
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx",
"1111111111111111111111111111111111111111x",
"x1111111111111111111111111111111111111111",
"x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x",
// a regexp stresser
"u-9490371368748654323415773467328453675-",
// dashes alone are not a name
"-", "--",
// double dashes in a name are not allowed
"a--a",
// name should not end with a dash
"a-",
// name cannot have any spaces in it
"a ", " a", "a a",
// a number alone is not a name
"0", "123",
// identifier must be plain ASCII
"日本語", "한글", "ру́сский язы́к",
}
for _, name := range invalidNames {
err := ValidateName(name)
c.Assert(err, ErrorMatches, `invalid snap name: ".*"`)
}
}
func (s *ValidateSuite) TestValidateInstanceName(c *C) {
validNames := []string{
// plain names are also valid instance names
"aa", "aaa", "aaaa",
"a-a", "aa-a", "a-aa", "a-b-c",
// snap instance
"foo_bar",
"foo_0123456789",
"01game_0123456789",
"foo_1", "foo_1234abcd",
}
for _, name := range validNames {
err := ValidateInstanceName(name)
c.Assert(err, IsNil)
}
invalidNames := []string{
// invalid names are also invalid instance names, just a few
// samples
"",
"a",
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx",
"a--a",
"a-",
"a ", " a", "a a",
"_",
"ру́сский_язы́к",
}
for _, name := range invalidNames {
err := ValidateInstanceName(name)
c.Assert(err, ErrorMatches, `invalid snap name: ".*"`)
}
invalidInstanceKeys := []string{
// the snap names are valid, but instance keys are not
"foo_", "foo_1-23", "foo_01234567890", "foo_123_456",
"foo__bar",
}
for _, name := range invalidInstanceKeys {
err := ValidateInstanceName(name)
c.Assert(err, ErrorMatches, `invalid instance key: ".*"`)
}
}
func (s *ValidateSuite) TestValidateVersion(c *C) {
validVersions := []string{
"0", "v1.0", "0.12+16.04.20160126-0ubuntu1",
"1:6.0.1+r16-3", "1.0~", "1.0+", "README.~1~",
"a+++++++++++++++++++++++++++++++",
"AZaz:.+~-123",
}
for _, version := range validVersions {
err := ValidateVersion(version)
c.Assert(err, IsNil)
}
invalidVersionsTable := [][2]string{
{"~foo", `must start with an ASCII alphanumeric (and not '~')`},
{"+foo", `must start with an ASCII alphanumeric (and not '+')`},
{"foo:", `must end with an ASCII alphanumeric or one of '+' or '~' (and not ':')`},
{"foo.", `must end with an ASCII alphanumeric or one of '+' or '~' (and not '.')`},
{"foo-", `must end with an ASCII alphanumeric or one of '+' or '~' (and not '-')`},
{"horrible_underscores", `contains invalid characters: "_"`},
{"foo($bar^baz$)meep", `contains invalid characters: "($", "^", "$)"`},
{"árbol", `must be printable, non-whitespace ASCII`},
{"日本語", `must be printable, non-whitespace ASCII`},
{"한글", `must be printable, non-whitespace ASCII`},
{"ру́сский язы́к", `must be printable, non-whitespace ASCII`},
{"~foo$bar:", `must start with an ASCII alphanumeric (and not '~'),` +
` must end with an ASCII alphanumeric or one of '+' or '~' (and not ':'),` +
` and contains invalid characters: "$"`},
}
for _, t := range invalidVersionsTable {
version, reason := t[0], t[1]
err := ValidateVersion(version)
c.Assert(err, NotNil)
c.Assert(err.Error(), Equals, fmt.Sprintf("invalid snap version %s: %s", strconv.QuoteToASCII(version), reason))
}
// version cannot be empty
c.Assert(ValidateVersion(""), ErrorMatches, `invalid snap version: cannot be empty`)
// version length cannot be >32
c.Assert(ValidateVersion("this-version-is-a-little-bit-older"), ErrorMatches,
`invalid snap version "this-version-is-a-little-bit-older": cannot be longer than 32 characters \(got: 34\)`)
}
func (s *ValidateSuite) TestValidateLicense(c *C) {
validLicenses := []string{
"GPL-3.0", "(GPL-3.0)", "GPL-3.0+", "GPL-3.0 AND GPL-2.0", "GPL-3.0 OR GPL-2.0", "MIT OR (GPL-3.0 AND GPL-2.0)", "MIT OR(GPL-3.0 AND GPL-2.0)",
}
for _, epoch := range validLicenses {
err := ValidateLicense(epoch)
c.Assert(err, IsNil)
}
invalidLicenses := []string{
"GPL~3.0", "3.0-GPL", "(GPL-3.0", "(GPL-3.0))", "GPL-3.0++", "+GPL-3.0", "GPL-3.0 GPL-2.0",
}
for _, epoch := range invalidLicenses {
err := ValidateLicense(epoch)
c.Assert(err, NotNil)
}
}
func (s *ValidateSuite) TestValidateHook(c *C) {
validHooks := []*HookInfo{
{Name: "a"},
{Name: "aaa"},
{Name: "a-a"},
{Name: "aa-a"},
{Name: "a-aa"},
{Name: "a-b-c"},
{Name: "valid", CommandChain: []string{"valid"}},
}
for _, hook := range validHooks {
err := ValidateHook(hook)
c.Assert(err, IsNil)
}
invalidHooks := []*HookInfo{
{Name: ""},
{Name: "a a"},
{Name: "a--a"},
{Name: "-a"},
{Name: "a-"},
{Name: "0"},
{Name: "123"},
{Name: "123abc"},
{Name: "日本語"},
}
for _, hook := range invalidHooks {
err := ValidateHook(hook)
c.Assert(err, ErrorMatches, `invalid hook name: ".*"`)
}
invalidHooks = []*HookInfo{
{Name: "valid", CommandChain: []string{"in'valid"}},
{Name: "valid", CommandChain: []string{"in valid"}},
}
for _, hook := range invalidHooks {
err := ValidateHook(hook)
c.Assert(err, ErrorMatches, `hook command-chain contains illegal.*`)
}
}
// ValidateApp
func (s *ValidateSuite) TestValidateAppName(c *C) {
validAppNames := []string{
"1", "a", "aa", "aaa", "aaaa", "Aa", "aA", "1a", "a1", "1-a", "a-1",
"a-a", "aa-a", "a-aa", "a-b-c", "0a-a", "a-0a",
}
for _, name := range validAppNames {
c.Check(ValidateApp(&AppInfo{Name: name}), IsNil)
}
invalidAppNames := []string{
"", "-", "--", "a--a", "a-", "a ", " a", "a a", "日本語", "한글",
"ру́сский язы́к", "ໄຂ່ອີສເຕີ້", ":a", "a:", "a:a", "_a", "a_", "a_a",
}
for _, name := range invalidAppNames {
err := ValidateApp(&AppInfo{Name: name})
c.Assert(err, ErrorMatches, `cannot have ".*" as app name.*`)
}
}
func (s *ValidateSuite) TestValidateAppSockets(c *C) {
app := createSampleApp()
app.Sockets["sock"].SocketMode = 0600
c.Check(ValidateApp(app), IsNil)
}
func (s *ValidateSuite) TestValidateAppSocketsEmptyPermsOk(c *C) {
app := createSampleApp()
c.Check(ValidateApp(app), IsNil)
}
func (s *ValidateSuite) TestValidateAppSocketsWrongPerms(c *C) {
app := createSampleApp()
app.Sockets["sock"].SocketMode = 1234
err := ValidateApp(app)
c.Assert(err, ErrorMatches, `invalid definition of socket "sock": cannot use mode: 2322`)
}
func (s *ValidateSuite) TestValidateAppSocketsMissingNetworkBindPlug(c *C) {
app := createSampleApp()
delete(app.Plugs, "network-bind")
err := ValidateApp(app)
c.Assert(
err, ErrorMatches,
`"network-bind" interface plug is required when sockets are used`)
}
func (s *ValidateSuite) TestValidateAppSocketsEmptyListenStream(c *C) {
app := createSampleApp()
app.Sockets["sock"].ListenStream = ""
err := ValidateApp(app)
c.Assert(err, ErrorMatches, `invalid definition of socket "sock": "listen-stream" is not defined`)
}
func (s *ValidateSuite) TestValidateAppSocketsInvalidName(c *C) {
app := createSampleApp()
app.Sockets["sock"].Name = "invalid name"
err := ValidateApp(app)
c.Assert(err, ErrorMatches, `invalid definition of socket "invalid name": invalid socket name: "invalid name"`)
}
func (s *ValidateSuite) TestValidateAppSocketsValidListenStreamAddresses(c *C) {
app := createSampleApp()
validListenAddresses := []string{
// socket paths using variables as prefix
"$SNAP_DATA/my.socket",
"$SNAP_COMMON/my.socket",
// abstract sockets
"@snap.mysnap.my.socket",
// addresses and ports
"1",
"1023",
"1024",
"65535",
"127.0.0.1:8080",
"[::]:8080",
"[::1]:8080",
}
socket := app.Sockets["sock"]
for _, validAddress := range validListenAddresses {
socket.ListenStream = validAddress
err := ValidateApp(app)
c.Check(err, IsNil, Commentf(validAddress))
}
}
func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamPath(c *C) {
app := createSampleApp()
invalidListenAddresses := []string{
// socket paths out of the snap dirs
"/some/path/my.socket",
"/var/snap/mysnap/20/my.socket", // path is correct but has hardcoded prefix
}
socket := app.Sockets["sock"]
for _, invalidAddress := range invalidListenAddresses {
socket.ListenStream = invalidAddress
err := ValidateApp(app)
c.Assert(err, ErrorMatches, `invalid definition of socket "sock": invalid "listen-stream": must have a prefix of .*`)
}
}
func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamPathContainsDots(c *C) {
app := createSampleApp()
app.Sockets["sock"].ListenStream = "$SNAP/../some.path"
err := ValidateApp(app)
c.Assert(
err, ErrorMatches,
`invalid definition of socket "sock": invalid "listen-stream": "\$SNAP/../some.path" should be written as "some.path"`)
}
func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamPathPrefix(c *C) {
app := createSampleApp()
invalidListenAddresses := []string{
"$SNAP/my.socket", // snap dir is not writable
"$SOMEVAR/my.socket",
}
socket := app.Sockets["sock"]
for _, invalidAddress := range invalidListenAddresses {
socket.ListenStream = invalidAddress
err := ValidateApp(app)
c.Assert(
err, ErrorMatches,
`invalid definition of socket "sock": invalid "listen-stream": must have a prefix of \$SNAP_DATA or \$SNAP_COMMON`)
}
}
func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamAbstractSocket(c *C) {
app := createSampleApp()
invalidListenAddresses := []string{
"@snap.mysnap",
"@snap.mysnap\000.foo",
"@snap.notmysnap.my.socket",
"@some.other.name",
"@snap.myappiswrong.foo",
}
socket := app.Sockets["sock"]
for _, invalidAddress := range invalidListenAddresses {
socket.ListenStream = invalidAddress
err := ValidateApp(app)
c.Assert(err, ErrorMatches, `invalid definition of socket "sock": path for "listen-stream" must be prefixed with.*`)
}
}
func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamAddress(c *C) {
app := createSampleApp()
invalidListenAddresses := []string{
"10.0.1.1:8080",
"[fafa::baba]:8080",
"127.0.0.1\000:8080",
"127.0.0.1::8080",
}
socket := app.Sockets["sock"]
for _, invalidAddress := range invalidListenAddresses {
socket.ListenStream = invalidAddress
err := ValidateApp(app)
c.Assert(err, ErrorMatches, `invalid definition of socket "sock": invalid "listen-stream" address ".*", must be one of: 127\.0\.0\.1, \[::1\], \[::\]`)
}
}
func (s *ValidateSuite) TestValidateAppSocketsInvalidListenStreamPort(c *C) {
app := createSampleApp()
invalidPorts := []string{
"0",
"66536",
"-8080",
"12312345345",
"[::]:-123",
"[::1]:3452345234",
"invalid",
"[::]:invalid",
}
socket := app.Sockets["sock"]
for _, invalidPort := range invalidPorts {
socket.ListenStream = invalidPort
err := ValidateApp(app)
c.Assert(err, ErrorMatches, `invalid definition of socket "sock": invalid "listen-stream" port number.*`)
}
}
func (s *ValidateSuite) TestAppWhitelistSimple(c *C) {
c.Check(ValidateApp(&AppInfo{Name: "foo", Command: "foo"}), IsNil)
c.Check(ValidateApp(&AppInfo{Name: "foo", StopCommand: "foo"}), IsNil)
c.Check(ValidateApp(&AppInfo{Name: "foo", PostStopCommand: "foo"}), IsNil)
}
func (s *ValidateSuite) TestAppWhitelistWithVars(c *C) {
c.Check(ValidateApp(&AppInfo{Name: "foo", Command: "foo $SNAP_DATA"}), IsNil)
c.Check(ValidateApp(&AppInfo{Name: "foo", StopCommand: "foo $SNAP_DATA"}), IsNil)
c.Check(ValidateApp(&AppInfo{Name: "foo", PostStopCommand: "foo $SNAP_DATA"}), IsNil)
}
func (s *ValidateSuite) TestAppWhitelistIllegal(c *C) {
c.Check(ValidateApp(&AppInfo{Name: "x\n"}), NotNil)
c.Check(ValidateApp(&AppInfo{Name: "test!me"}), NotNil)
c.Check(ValidateApp(&AppInfo{Name: "test'me"}), NotNil)
c.Check(ValidateApp(&AppInfo{Name: "foo", Command: "foo\n"}), NotNil)
c.Check(ValidateApp(&AppInfo{Name: "foo", StopCommand: "foo\n"}), NotNil)
c.Check(ValidateApp(&AppInfo{Name: "foo", PostStopCommand: "foo\n"}), NotNil)
c.Check(ValidateApp(&AppInfo{Name: "foo", BusName: "foo\n"}), NotNil)
c.Check(ValidateApp(&AppInfo{Name: "foo", CommandChain: []string{"bar'baz"}}), NotNil)
c.Check(ValidateApp(&AppInfo{Name: "foo", CommandChain: []string{"bar baz"}}), NotNil)
}
func (s *ValidateSuite) TestAppDaemonValue(c *C) {
for _, t := range []struct {
daemon string
ok bool
}{
// good
{"", true},
{"simple", true},
{"forking", true},
{"oneshot", true},
{"dbus", true},
{"notify", true},
// bad
{"invalid-thing", false},
} {
if t.ok {
c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: t.daemon}), IsNil)
} else {
c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: t.daemon}), ErrorMatches, fmt.Sprintf(`"daemon" field contains invalid value %q`, t.daemon))
}
}
}
func (s *ValidateSuite) TestAppStopMode(c *C) {
// check services
for _, t := range []struct {
stopMode StopModeType
ok bool
}{
// good
{"", true},
{"sigterm", true},
{"sigterm-all", true},
{"sighup", true},
{"sighup-all", true},
{"sigusr1", true},
{"sigusr1-all", true},
{"sigusr2", true},
{"sigusr2-all", true},
// bad
{"invalid-thing", false},
} {
if t.ok {
c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", StopMode: t.stopMode}), IsNil)
} else {
c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", StopMode: t.stopMode}), ErrorMatches, fmt.Sprintf(`"stop-mode" field contains invalid value %q`, t.stopMode))
}
}
// non-services cannot have a stop-mode
err := ValidateApp(&AppInfo{Name: "foo", Daemon: "", StopMode: "sigterm"})
c.Check(err, ErrorMatches, `"stop-mode" cannot be used for "foo", only for services`)
}
func (s *ValidateSuite) TestAppRefreshMode(c *C) {
// check services
for _, t := range []struct {
refreshMode string
ok bool
}{
// good
{"", true},
{"endure", true},
{"restart", true},
// bad
{"invalid-thing", false},
} {
if t.ok {
c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", RefreshMode: t.refreshMode}), IsNil)
} else {
c.Check(ValidateApp(&AppInfo{Name: "foo", Daemon: "simple", RefreshMode: t.refreshMode}), ErrorMatches, fmt.Sprintf(`"refresh-mode" field contains invalid value %q`, t.refreshMode))
}
}
// non-services cannot have a refresh-mode
err := ValidateApp(&AppInfo{Name: "foo", Daemon: "", RefreshMode: "endure"})
c.Check(err, ErrorMatches, `"refresh-mode" cannot be used for "foo", only for services`)
}
func (s *ValidateSuite) TestAppWhitelistError(c *C) {
err := ValidateApp(&AppInfo{Name: "foo", Command: "x\n"})
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, `app description field 'command' contains illegal "x\n" (legal: '^[A-Za-z0-9/. _#:$-]*$')`)
}
// Validate
func (s *ValidateSuite) TestDetectIllegalYamlBinaries(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: foo
version: 1.0
apps:
tes!me:
command: someething
`))
c.Assert(err, IsNil)
err = Validate(info)
c.Check(err, NotNil)
}
func (s *ValidateSuite) TestDetectIllegalYamlService(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: foo
version: 1.0
apps:
tes!me:
command: something
daemon: forking
`))
c.Assert(err, IsNil)
err = Validate(info)
c.Check(err, NotNil)
}
func (s *ValidateSuite) TestIllegalSnapName(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: foo.something
version: 1.0
`))
c.Assert(err, IsNil)
err = Validate(info)
c.Check(err, ErrorMatches, `invalid snap name: "foo.something"`)
}
func (s *ValidateSuite) TestValidateChecksName(c *C) {
info, err := InfoFromSnapYaml([]byte(`
version: 1.0
`))
c.Assert(err, IsNil)
err = Validate(info)
c.Check(err, ErrorMatches, `snap name cannot be empty`)
}
func (s *ValidateSuite) TestIllegalSnapEpoch(c *C) {
_, err := InfoFromSnapYaml([]byte(`name: foo
version: 1.0
epoch: 0*
`))
c.Assert(err, ErrorMatches, `.*invalid epoch.*`)
}
func (s *ValidateSuite) TestMissingSnapEpochIsOkay(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: foo
version: 1.0
`))
c.Assert(err, IsNil)
c.Assert(Validate(info), IsNil)
}
func (s *ValidateSuite) TestIllegalSnapLicense(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: foo
version: 1.0
license: GPL~3.0
`))
c.Assert(err, IsNil)
err = Validate(info)
c.Check(err, ErrorMatches, `cannot validate license "GPL~3.0": unknown license: GPL~3.0`)
}
func (s *ValidateSuite) TestMissingSnapLicenseIsOkay(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: foo
version: 1.0
`))
c.Assert(err, IsNil)
c.Assert(Validate(info), IsNil)
}
func (s *ValidateSuite) TestIllegalHookName(c *C) {
hookType := NewHookType(regexp.MustCompile(".*"))
restore := MockSupportedHookTypes([]*HookType{hookType})
defer restore()
info, err := InfoFromSnapYaml([]byte(`name: foo
version: 1.0
hooks:
123abc:
`))
c.Assert(err, IsNil)
err = Validate(info)
c.Check(err, ErrorMatches, `invalid hook name: "123abc"`)
}
func (s *ValidateSuite) TestPlugSlotNamesUnique(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: snap
version: 0
plugs:
foo:
slots:
foo:
`))
c.Assert(err, IsNil)
err = Validate(info)
c.Check(err, ErrorMatches, `cannot have plug and slot with the same name: "foo"`)
}
func (s *ValidateSuite) TestIllegalAliasName(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: foo
version: 1.0
apps:
foo:
aliases: [foo$]
`))
c.Assert(err, IsNil)
err = Validate(info)
c.Check(err, ErrorMatches, `cannot have "foo\$" as alias name for app "foo" - use only letters, digits, dash, underscore and dot characters`)
}
func (s *ValidateSuite) TestValidateAlias(c *C) {
validAliases := []string{
"a", "aa", "aaa", "aaaa",
"a-a", "aa-a", "a-aa", "a-b-c",
"a0", "a-0", "a-0a",
"a_a", "aa_a", "a_aa", "a_b_c",
"a0", "a_0", "a_0a",
"01game", "1-or-2",
"0.1game", "1_or_2",
}
for _, alias := range validAliases {
err := ValidateAlias(alias)
c.Assert(err, IsNil)
}
invalidAliases := []string{
"",
"_foo",
"-foo",
".foo",
"foo$",
}
for _, alias := range invalidAliases {
err := ValidateAlias(alias)
c.Assert(err, ErrorMatches, `invalid alias name: ".*"`)
}
}
func (s *ValidateSuite) TestValidatePlugSlotName(c *C) {
const yaml1 = `
name: invalid-plugs
version: 1
plugs:
p--lug: null
`
info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml1), nil)
c.Assert(err, IsNil)
c.Assert(info.Plugs, HasLen, 1)
err = Validate(info)
c.Assert(err, ErrorMatches, `invalid plug name: "p--lug"`)
const yaml2 = `
name: invalid-slots
version: 1
slots:
s--lot: null
`
info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml2), nil)
c.Assert(err, IsNil)
c.Assert(info.Slots, HasLen, 1)
err = Validate(info)
c.Assert(err, ErrorMatches, `invalid slot name: "s--lot"`)
const yaml3 = `
name: invalid-plugs-iface
version: 1
plugs:
plug:
interface: i--face
`
info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml3), nil)
c.Assert(err, IsNil)
c.Assert(info.Plugs, HasLen, 1)
err = Validate(info)
c.Assert(err, ErrorMatches, `invalid interface name "i--face" for plug "plug"`)
const yaml4 = `
name: invalid-slots-iface
version: 1
slots:
slot:
interface: i--face
`
info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml4), nil)
c.Assert(err, IsNil)
c.Assert(info.Slots, HasLen, 1)
err = Validate(info)
c.Assert(err, ErrorMatches, `invalid interface name "i--face" for slot "slot"`)
}
type testConstraint string
func (constraint testConstraint) IsOffLimits(path string) bool {
return true
}
func (s *ValidateSuite) TestValidateLayout(c *C) {
si := &Info{SuggestedName: "foo"}
// Several invalid layouts.
c.Check(ValidateLayout(&Layout{Snap: si}, nil),
ErrorMatches, "layout cannot use an empty path")
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo"}, nil),
ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Bind: "/bar", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Bind: "/bar", BindFile: "/froz"}, nil),
ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Symlink: "/bar", BindFile: "/froz"}, nil),
ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Type: "tmpfs", BindFile: "/froz"}, nil),
ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Bind: "/bar", Symlink: "/froz"}, nil),
ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Type: "tmpfs", Symlink: "/froz"}, nil),
ErrorMatches, `layout "/foo" must define a bind mount, a filesystem mount or a symlink`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Type: "ext4"}, nil),
ErrorMatches, `layout "/foo" uses invalid filesystem "ext4"`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo/bar", Type: "tmpfs", User: "foo"}, nil),
ErrorMatches, `layout "/foo/bar" uses invalid user "foo"`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo/bar", Type: "tmpfs", Group: "foo"}, nil),
ErrorMatches, `layout "/foo/bar" uses invalid group "foo"`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Type: "tmpfs", Mode: 02755}, nil),
ErrorMatches, `layout "/foo" uses invalid mode 02755`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "$FOO", Type: "tmpfs"}, nil),
ErrorMatches, `layout "\$FOO" uses invalid mount point: reference to unknown variable "\$FOO"`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Bind: "$BAR"}, nil),
ErrorMatches, `layout "/foo" uses invalid bind mount source "\$BAR": reference to unknown variable "\$BAR"`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "$SNAP/evil", Bind: "/etc"}, nil),
ErrorMatches, `layout "\$SNAP/evil" uses invalid bind mount source "/etc": must start with \$SNAP, \$SNAP_DATA or \$SNAP_COMMON`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Symlink: "$BAR"}, nil),
ErrorMatches, `layout "/foo" uses invalid symlink old name "\$BAR": reference to unknown variable "\$BAR"`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "$SNAP/evil", Symlink: "/etc"}, nil),
ErrorMatches, `layout "\$SNAP/evil" uses invalid symlink old name "/etc": must start with \$SNAP, \$SNAP_DATA or \$SNAP_COMMON`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo/bar", Bind: "$SNAP/bar/foo"}, []LayoutConstraint{testConstraint("/foo")}),
ErrorMatches, `layout "/foo/bar" underneath prior layout item "/foo"`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/dev", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/dev" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/dev/foo", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/dev/foo" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/proc", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/proc" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/sys", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/sys" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/run", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/run" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/boot", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/boot" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/lost+found", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/lost\+found" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/media", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/media" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var/snap", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/var/snap" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var/lib/snapd", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/var/lib/snapd" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var/lib/snapd/hostfs", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/var/lib/snapd/hostfs" in an off-limits area`)
// Several valid layouts.
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Type: "tmpfs", Mode: 01755}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/tmp", Type: "tmpfs"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/usr", Bind: "$SNAP/usr"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var", Bind: "$SNAP_DATA/var"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var", Bind: "$SNAP_COMMON/var"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/etc/foo.conf", Symlink: "$SNAP_DATA/etc/foo.conf"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/a/b", Type: "tmpfs", User: "root"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/a/b", Type: "tmpfs", Group: "root"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/a/b", Type: "tmpfs", Mode: 0655}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/usr", Symlink: "$SNAP/usr"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var", Symlink: "$SNAP_DATA/var"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var", Symlink: "$SNAP_COMMON/var"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "$SNAP/data", Symlink: "$SNAP_DATA"}, nil), IsNil)
}
func (s *ValidateSuite) TestValidateLayoutAll(c *C) {
// /usr/foo prevents /usr/foo/bar from being valid (tmpfs)
const yaml1 = `
name: broken-layout-1
layout:
/usr/foo:
type: tmpfs
/usr/foo/bar:
type: tmpfs
`
const yaml1rev = `
name: broken-layout-1
layout:
/usr/foo/bar:
type: tmpfs
/usr/foo:
type: tmpfs
`
for _, yaml := range []string{yaml1, yaml1rev} {
info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), &SideInfo{Revision: R(42)})
c.Assert(err, IsNil)
c.Assert(info.Layout, HasLen, 2)
err = ValidateLayoutAll(info)
c.Assert(err, ErrorMatches, `layout "/usr/foo/bar" underneath prior layout item "/usr/foo"`)
}
// Same as above but with bind-mounts instead of filesystem mounts.
const yaml2 = `
name: broken-layout-2
layout:
/usr/foo:
bind: $SNAP
/usr/foo/bar:
bind: $SNAP
`
const yaml2rev = `
name: broken-layout-2
layout:
/usr/foo/bar:
bind: $SNAP
/usr/foo:
bind: $SNAP
`
for _, yaml := range []string{yaml2, yaml2rev} {
info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), &SideInfo{Revision: R(42)})
c.Assert(err, IsNil)
c.Assert(info.Layout, HasLen, 2)
err = ValidateLayoutAll(info)
c.Assert(err, ErrorMatches, `layout "/usr/foo/bar" underneath prior layout item "/usr/foo"`)
}
// /etc/foo (directory) is not clashing with /etc/foo.conf (file)
const yaml3 = `
name: valid-layout-1
layout:
/etc/foo:
bind: $SNAP_DATA/foo
/etc/foo.conf:
symlink: $SNAP_DATA/foo.conf
`
const yaml3rev = `
name: valid-layout-1
layout:
/etc/foo.conf:
symlink: $SNAP_DATA/foo.conf
/etc/foo:
bind: $SNAP_DATA/foo
`
for _, yaml := range []string{yaml3, yaml3rev} {
info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), &SideInfo{Revision: R(42)})
c.Assert(err, IsNil)
c.Assert(info.Layout, HasLen, 2)
err = ValidateLayoutAll(info)
c.Assert(err, IsNil)
}
// /etc/foo file is not clashing with /etc/foobar
const yaml4 = `
name: valid-layout-2
layout:
/etc/foo:
symlink: $SNAP_DATA/foo
/etc/foobar:
symlink: $SNAP_DATA/foobar
`
const yaml4rev = `
name: valid-layout-2
layout:
/etc/foobar:
symlink: $SNAP_DATA/foobar
/etc/foo:
symlink: $SNAP_DATA/foo
`
for _, yaml := range []string{yaml4, yaml4rev} {
info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), &SideInfo{Revision: R(42)})
c.Assert(err, IsNil)
c.Assert(info.Layout, HasLen, 2)
err = ValidateLayoutAll(info)
c.Assert(err, IsNil)
}
// /etc/foo file is also clashing with /etc/foo/bar
const yaml5 = `
name: valid-layout-2
layout:
/usr/foo:
symlink: $SNAP_DATA/foo
/usr/foo/bar:
bind: $SNAP_DATA/foo/bar
`
const yaml5rev = `
name: valid-layout-2
layout:
/usr/foo/bar:
bind: $SNAP_DATA/foo/bar
/usr/foo:
symlink: $SNAP_DATA/foo
`
for _, yaml := range []string{yaml5, yaml5rev} {
info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), &SideInfo{Revision: R(42)})
c.Assert(err, IsNil)
c.Assert(info.Layout, HasLen, 2)
err = ValidateLayoutAll(info)
c.Assert(err, ErrorMatches, `layout "/usr/foo/bar" underneath prior layout item "/usr/foo"`)
}
const yaml6 = `
name: tricky-layout-1
layout:
/etc/norf:
bind: $SNAP/etc/norf
/etc/norf:
bind-file: $SNAP/etc/norf
`
info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml6), &SideInfo{Revision: R(42)})
c.Assert(err, IsNil)
c.Assert(info.Layout, HasLen, 1)
err = ValidateLayoutAll(info)
c.Assert(err, IsNil)
c.Assert(info.Layout["/etc/norf"].Bind, Equals, "")
c.Assert(info.Layout["/etc/norf"].BindFile, Equals, "$SNAP/etc/norf")
// Two layouts refer to the same path as a directory and a file.
const yaml7 = `
name: clashing-source-path-1
layout:
/etc/norf:
bind: $SNAP/etc/norf
/etc/corge:
bind-file: $SNAP/etc/norf
`
info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml7), &SideInfo{Revision: R(42)})
c.Assert(err, IsNil)
c.Assert(info.Layout, HasLen, 2)
err = ValidateLayoutAll(info)
c.Assert(err, ErrorMatches, `layout "/etc/norf" refers to directory "\$SNAP/etc/norf" but another layout treats it as file`)
// Two layouts refer to the same path as a directory and a file (other way around).
const yaml8 = `
name: clashing-source-path-2
layout:
/etc/norf:
bind-file: $SNAP/etc/norf
/etc/corge:
bind: $SNAP/etc/norf
`
info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml8), &SideInfo{Revision: R(42)})
c.Assert(err, IsNil)
c.Assert(info.Layout, HasLen, 2)
err = ValidateLayoutAll(info)
c.Assert(err, ErrorMatches, `layout "/etc/norf" refers to file "\$SNAP/etc/norf" but another layout treats it as a directory`)
// Two layouts refer to the same path, but one uses variable and the other doesn't.
const yaml9 = `
name: clashing-source-path-3
layout:
/etc/norf:
bind-file: $SNAP/etc/norf
/etc/corge:
bind: /snap/clashing-source-path-3/42/etc/norf
`
info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml9), &SideInfo{Revision: R(42)})
c.Assert(err, IsNil)
c.Assert(info.Layout, HasLen, 2)
err = ValidateLayoutAll(info)
c.Assert(err, ErrorMatches, `layout "/etc/norf" refers to file "\$SNAP/etc/norf" but another layout treats it as a directory`)
// Same source path referred from a bind mount and symlink doesn't clash.
const yaml10 = `
name: non-clashing-source-1
layout:
/etc/norf:
bind: $SNAP/etc/norf
/etc/corge:
symlink: $SNAP/etc/norf
`
info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml10), &SideInfo{Revision: R(42)})
c.Assert(err, IsNil)
c.Assert(info.Layout, HasLen, 2)
err = ValidateLayoutAll(info)
c.Assert(err, IsNil)
// Same source path referred from a file bind mount and symlink doesn't clash.
const yaml11 = `
name: non-clashing-source-1
layout:
/etc/norf:
bind-file: $SNAP/etc/norf
/etc/corge:
symlink: $SNAP/etc/norf
`
info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml11), &SideInfo{Revision: R(42)})
c.Assert(err, IsNil)
c.Assert(info.Layout, HasLen, 2)
err = ValidateLayoutAll(info)
c.Assert(err, IsNil)
}
func (s *ValidateSuite) TestValidateSocketName(c *C) {
validNames := []string{
"a", "aa", "aaa", "aaaa",
"a-a", "aa-a", "a-aa", "a-b-c",
"a0", "a-0", "a-0a",
"01game", "1-or-2",
}
for _, name := range validNames {
err := ValidateSocketName(name)
c.Assert(err, IsNil)
}
invalidNames := []string{
// name cannot be empty
"",
// dashes alone are not a name
"-", "--",
// double dashes in a name are not allowed
"a--a",
// name should not end with a dash
"a-",
// name cannot have any spaces in it
"a ", " a", "a a",
// a number alone is not a name
"0", "123",
// identifier must be plain ASCII
"日本語", "한글", "ру́сский язы́к",
// no null chars in the string are allowed
"aa-a\000-b",
}
for _, name := range invalidNames {
err := ValidateSocketName(name)
c.Assert(err, ErrorMatches, `invalid socket name: ".*"`)
}
}
func (s *YamlSuite) TestValidateAppStartupOrder(c *C) {
meta := []byte(`
name: foo
version: 1.0
`)
fooAfterBaz := []byte(`
apps:
foo:
after: [baz]
daemon: simple
bar:
daemon: forking
`)
fooBeforeBaz := []byte(`
apps:
foo:
before: [baz]
daemon: simple
bar:
daemon: forking
`)
fooNotADaemon := []byte(`
apps:
foo:
after: [bar]
bar:
daemon: forking
`)
fooBarNotADaemon := []byte(`
apps:
foo:
after: [bar]
daemon: forking
bar:
`)
fooSelfCycle := []byte(`
apps:
foo:
after: [foo]
daemon: forking
bar:
`)
// cycle between foo and bar
badOrder1 := []byte(`
apps:
foo:
after: [bar]
daemon: forking
bar:
after: [foo]
daemon: forking
`)
// conflicting schedule for baz
badOrder2 := []byte(`
apps:
foo:
before: [bar]
daemon: forking
bar:
after: [foo]
daemon: forking
baz:
before: [foo]
after: [bar]
daemon: forking
`)
// conflicting schedule for baz
badOrder3Cycle := []byte(`
apps:
foo:
before: [bar]
after: [zed]
daemon: forking
bar:
before: [baz]
daemon: forking
baz:
before: [zed]
daemon: forking
zed:
daemon: forking
`)
goodOrder1 := []byte(`
apps:
foo:
after: [bar, zed]
daemon: oneshot
bar:
before: [foo]
daemon: dbus
baz:
after: [foo]
daemon: forking
zed:
daemon: dbus
`)
goodOrder2 := []byte(`
apps:
foo:
after: [baz]
daemon: oneshot
bar:
before: [baz]
daemon: dbus
baz:
daemon: forking
zed:
daemon: dbus
after: [foo, bar, baz]
`)
tcs := []struct {
name string
desc []byte
err string
}{{
name: "foo after baz",
desc: fooAfterBaz,
err: `invalid definition of application "foo": before/after references a missing application "baz"`,
}, {
name: "foo before baz",
desc: fooBeforeBaz,
err: `invalid definition of application "foo": before/after references a missing application "baz"`,
}, {
name: "foo not a daemon",
desc: fooNotADaemon,
err: `invalid definition of application "foo": must be a service to define before/after ordering`,
}, {
name: "foo wants bar, bar not a daemon",
desc: fooBarNotADaemon,
err: `invalid definition of application "foo": before/after references a non-service application "bar"`,
}, {
name: "bad order 1",
desc: badOrder1,
err: `applications are part of a before/after cycle: (foo, bar)|(bar, foo)`,
}, {
name: "bad order 2",
desc: badOrder2,
err: `applications are part of a before/after cycle: ((foo|bar|baz)(, )?){3}`,
}, {
name: "bad order 3 - cycle",
desc: badOrder3Cycle,
err: `applications are part of a before/after cycle: ((foo|bar|baz|zed)(, )?){4}`,
}, {
name: "all good, 3 apps",
desc: goodOrder1,
}, {
name: "all good, 4 apps",
desc: goodOrder2,
}, {
name: "self cycle",
desc: fooSelfCycle,
err: `applications are part of a before/after cycle: foo`},
}
for _, tc := range tcs {
c.Logf("trying %q", tc.name)
info, err := InfoFromSnapYaml(append(meta, tc.desc...))
c.Assert(err, IsNil)
err = Validate(info)
if tc.err != "" {
c.Assert(err, ErrorMatches, tc.err)
} else {
c.Assert(err, IsNil)
}
}
}
func (s *ValidateSuite) TestValidateAppWatchdog(c *C) {
meta := []byte(`
name: foo
version: 1.0
`)
fooAllGood := []byte(`
apps:
foo:
daemon: simple
watchdog-timeout: 12s
`)
fooNotADaemon := []byte(`
apps:
foo:
watchdog-timeout: 12s
`)
fooNegative := []byte(`
apps:
foo:
daemon: simple
watchdog-timeout: -12s
`)
tcs := []struct {
name string
desc []byte
err string
}{{
name: "foo all good",
desc: fooAllGood,
}, {
name: "foo not a service",
desc: fooNotADaemon,
err: `watchdog-timeout is only applicable to services`,
}, {
name: "negative timeout",
desc: fooNegative,
err: `watchdog-timeout cannot be negative`,
}}
for _, tc := range tcs {
c.Logf("trying %q", tc.name)
info, err := InfoFromSnapYaml(append(meta, tc.desc...))
c.Assert(err, IsNil)
c.Assert(info, NotNil)
err = Validate(info)
if tc.err != "" {
c.Assert(err, ErrorMatches, `invalid definition of application "foo": `+tc.err)
} else {
c.Assert(err, IsNil)
}
}
}
func (s *YamlSuite) TestValidateAppTimer(c *C) {
meta := []byte(`
name: foo
version: 1.0
`)
allGood := []byte(`
apps:
foo:
daemon: simple
timer: 10:00-12:00
`)
notAService := []byte(`
apps:
foo:
timer: 10:00-12:00
`)
badTimer := []byte(`
apps:
foo:
daemon: oneshot
timer: mon,10:00-12:00,mon2-wed3
`)
tcs := []struct {
name string
desc []byte
err string
}{{
name: "all correct",
desc: allGood,
}, {
name: "not a service",
desc: notAService,
err: `timer is only applicable to services`,
}, {
name: "invalid timer",
desc: badTimer,
err: `timer has invalid format: cannot parse "mon2-wed3": invalid schedule fragment`,
}}
for _, tc := range tcs {
c.Logf("trying %q", tc.name)
info, err := InfoFromSnapYaml(append(meta, tc.desc...))
c.Assert(err, IsNil)
err = Validate(info)
if tc.err != "" {
c.Assert(err, ErrorMatches, `invalid definition of application "foo": `+tc.err)
} else {
c.Assert(err, IsNil)
}
}
}
func (s *ValidateSuite) TestValidateOsCannotHaveBase(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: foo
version: 1.0
type: os
base: bar
`))
c.Assert(err, IsNil)
err = Validate(info)
c.Check(err, ErrorMatches, `cannot have "base" field on "os" snap "foo"`)
}
func (s *ValidateSuite) TestValidateBaseCannotHaveBase(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: foo
version: 1.0
type: base
base: bar
`))
c.Assert(err, IsNil)
err = Validate(info)
c.Check(err, ErrorMatches, `cannot have "base" field on "base" snap "foo"`)
}
func (s *ValidateSuite) TestValidateCommonIDs(c *C) {
meta := `
name: foo
version: 1.0
`
good := meta + `
apps:
foo:
common-id: org.foo.foo
bar:
common-id: org.foo.bar
baz:
`
bad := meta + `
apps:
foo:
common-id: org.foo.foo
bar:
common-id: org.foo.foo
baz:
`
for i, tc := range []struct {
meta string
err string
}{
{good, ""},
{bad, `application ("bar" common-id "org.foo.foo" must be unique, already used by application "foo"|"foo" common-id "org.foo.foo" must be unique, already used by application "bar")`},
} {
c.Logf("tc #%v", i)
info, err := InfoFromSnapYaml([]byte(tc.meta))
c.Assert(err, IsNil)
err = Validate(info)
if tc.err == "" {
c.Assert(err, IsNil)
} else {
c.Assert(err, NotNil)
c.Check(err, ErrorMatches, tc.err)
}
}
}
func (s *validateSuite) TestValidateDescription(c *C) {
for _, s := range []string{
"xx", // boringest ASCII
"🐧🐧", // len("🐧🐧") == 8
"á", // á (combining)
} {
c.Check(ValidateDescription(s), IsNil)
c.Check(ValidateDescription(strings.Repeat(s, 2049)), ErrorMatches, `description can have up to 4096 codepoints, got 4098`)
c.Check(ValidateDescription(strings.Repeat(s, 2048)), IsNil)
}
}
func (s *validateSuite) TestValidateTitle(c *C) {
for _, s := range []string{
"xx", // boringest ASCII
"🐧🐧", // len("🐧🐧") == 8
"á", // á (combining)
} {
c.Check(ValidateTitle(strings.Repeat(s, 21)), ErrorMatches, `title can have up to 40 codepoints, got 42`)
c.Check(ValidateTitle(strings.Repeat(s, 20)), IsNil)
}
}
func (s *validateSuite) TestValidatePlugSlotName(c *C) {
validNames := []string{
"a", "aa", "aaa", "aaaa",
"a-a", "aa-a", "a-aa", "a-b-c",
"a0", "a-0", "a-0a",
}
for _, name := range validNames {
c.Assert(ValidatePlugName(name), IsNil)
c.Assert(ValidateSlotName(name), IsNil)
c.Assert(ValidateInterfaceName(name), IsNil)
}
invalidNames := []string{
// name cannot be empty
"",
// dashes alone are not a name
"-", "--",
// double dashes in a name are not allowed
"a--a",
// name should not end with a dash
"a-",
// name cannot have any spaces in it
"a ", " a", "a a",
// a number alone is not a name
"0", "123",
// identifier must be plain ASCII
"日本語", "한글", "ру́сский язы́к",
}
for _, name := range invalidNames {
c.Assert(ValidatePlugName(name), ErrorMatches, `invalid plug name: ".*"`)
c.Assert(ValidateSlotName(name), ErrorMatches, `invalid slot name: ".*"`)
c.Assert(ValidateInterfaceName(name), ErrorMatches, `invalid interface name: ".*"`)
}
}
func (s *ValidateSuite) TestValidateSnapInstanceNameBadSnapName(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: foo_bad
version: 1.0
`))
c.Assert(err, IsNil)
err = Validate(info)
c.Check(err, ErrorMatches, `invalid snap name: "foo_bad"`)
}
func (s *ValidateSuite) TestValidateSnapInstanceNameBadInstanceKey(c *C) {
info, err := InfoFromSnapYaml([]byte(`name: foo
version: 1.0
`))
c.Assert(err, IsNil)
for _, s := range []string{"toolonginstance", "ABCD", "_", "inst@nce", "012345678901"} {
info.InstanceKey = s
err = Validate(info)
c.Check(err, ErrorMatches, fmt.Sprintf(`invalid instance key: %q`, s))
}
}
func (s *ValidateSuite) TestValidateAppRestart(c *C) {
meta := []byte(`
name: foo
version: 1.0
`)
fooAllGood := []byte(`
apps:
foo:
daemon: simple
restart-condition: on-abort
restart-delay: 12s
`)
fooAllGoodDefault := []byte(`
apps:
foo:
daemon: simple
`)
fooAllGoodJustDelay := []byte(`
apps:
foo:
daemon: simple
restart-delay: 12s
`)
fooConditionNotADaemon := []byte(`
apps:
foo:
restart-condition: on-abort
`)
fooDelayNotADaemon := []byte(`
apps:
foo:
restart-delay: 12s
`)
fooNegativeDelay := []byte(`
apps:
foo:
daemon: simple
restart-delay: -12s
`)
tcs := []struct {
name string
desc []byte
err string
}{{
name: "foo all good",
desc: fooAllGood,
}, {
name: "foo all good with default values",
desc: fooAllGoodDefault,
}, {
name: "foo all good with restart-delay only",
desc: fooAllGoodJustDelay,
}, {
name: "foo restart-delay but not a service",
desc: fooDelayNotADaemon,
err: `restart-delay is only applicable to services`,
}, {
name: "foo restart-delay but not a service",
desc: fooConditionNotADaemon,
err: `restart-condition is only applicable to services`,
}, {
name: "negative restart-delay",
desc: fooNegativeDelay,
err: `restart-delay cannot be negative`,
}}
for _, tc := range tcs {
c.Logf("trying %q", tc.name)
info, err := InfoFromSnapYaml(append(meta, tc.desc...))
c.Assert(err, IsNil)
c.Assert(info, NotNil)
err = Validate(info)
if tc.err != "" {
c.Assert(err, ErrorMatches, `invalid definition of application "foo": `+tc.err)
} else {
c.Assert(err, IsNil)
}
}
}
snapd-2.37.4~14.04.1/snap/revision_test.go 0000664 0000000 0000000 00000007613 13435556260 014735 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
"encoding/json"
"strconv"
. "gopkg.in/check.v1"
"gopkg.in/yaml.v2"
"github.com/snapcore/snapd/snap"
)
// Keep this in sync between snap and client packages.
type revisionSuite struct{}
var _ = Suite(&revisionSuite{})
func (s revisionSuite) TestString(c *C) {
c.Assert(snap.R(0).String(), Equals, "unset")
c.Assert(snap.R(10).String(), Equals, "10")
c.Assert(snap.R(-9).String(), Equals, "x9")
}
func (s revisionSuite) TestUnset(c *C) {
c.Assert(snap.R(0).Unset(), Equals, true)
c.Assert(snap.R(10).Unset(), Equals, false)
c.Assert(snap.R(-9).Unset(), Equals, false)
}
func (s revisionSuite) TestLocal(c *C) {
c.Assert(snap.R(0).Local(), Equals, false)
c.Assert(snap.R(10).Local(), Equals, false)
c.Assert(snap.R(-9).Local(), Equals, true)
}
func (s revisionSuite) TestStore(c *C) {
c.Assert(snap.R(0).Store(), Equals, false)
c.Assert(snap.R(10).Store(), Equals, true)
c.Assert(snap.R(-9).Store(), Equals, false)
}
func (s revisionSuite) TestJSON(c *C) {
for _, n := range []int{0, 10, -9} {
r := snap.R(n)
data, err := json.Marshal(snap.R(n))
c.Assert(err, IsNil)
c.Assert(string(data), Equals, `"`+r.String()+`"`)
var got snap.Revision
err = json.Unmarshal(data, &got)
c.Assert(err, IsNil)
c.Assert(got, Equals, r)
got = snap.Revision{}
err = json.Unmarshal([]byte(strconv.Itoa(r.N)), &got)
c.Assert(err, IsNil)
c.Assert(got, Equals, r)
}
}
func (s revisionSuite) TestYAML(c *C) {
for _, v := range []struct {
n int
s string
}{
{0, "unset"},
{10, `"10"`},
{-9, "x9"},
} {
r := snap.R(v.n)
data, err := yaml.Marshal(snap.R(v.n))
c.Assert(err, IsNil)
c.Assert(string(data), Equals, v.s+"\n")
var got snap.Revision
err = yaml.Unmarshal(data, &got)
c.Assert(err, IsNil)
c.Assert(got, Equals, r)
got = snap.Revision{}
err = json.Unmarshal([]byte(strconv.Itoa(r.N)), &got)
c.Assert(err, IsNil)
c.Assert(got, Equals, r)
}
}
func (s revisionSuite) ParseRevision(c *C) {
type testItem struct {
s string
n int
e string
}
var tests = []testItem{{
s: "unset",
n: 0,
}, {
s: "x1",
n: -1,
}, {
s: "1",
n: 1,
}, {
s: "x-1",
e: `invalid snap revision: "x-1"`,
}, {
s: "x0",
e: `invalid snap revision: "x0"`,
}, {
s: "-1",
e: `invalid snap revision: "-1"`,
}, {
s: "0",
e: `invalid snap revision: "0"`,
}}
for _, test := range tests {
r, err := snap.ParseRevision(test.s)
if test.e != "" {
c.Assert(err.Error(), Equals, test.e)
continue
}
c.Assert(r, Equals, snap.R(test.n))
}
}
func (s *revisionSuite) TestR(c *C) {
type testItem struct {
v interface{}
n int
e string
}
var tests = []testItem{{
v: 0,
n: 0,
}, {
v: -1,
n: -1,
}, {
v: 1,
n: 1,
}, {
v: "unset",
n: 0,
}, {
v: "x1",
n: -1,
}, {
v: "1",
n: 1,
}, {
v: "x-1",
e: `invalid snap revision: "x-1"`,
}, {
v: "x0",
e: `invalid snap revision: "x0"`,
}, {
v: "-1",
e: `invalid snap revision: "-1"`,
}, {
v: "0",
e: `invalid snap revision: "0"`,
}, {
v: int64(1),
e: `cannot use 1 \(int64\) as a snap revision`,
}}
for _, test := range tests {
if test.e != "" {
f := func() { snap.R(test.v) }
c.Assert(f, PanicMatches, test.e)
continue
}
c.Assert(snap.R(test.v), Equals, snap.R(test.n))
}
}
snapd-2.37.4~14.04.1/snap/pack/ 0000775 0000000 0000000 00000000000 13435556260 012420 5 ustar snapd-2.37.4~14.04.1/snap/pack/pack_test.go 0000664 0000000 0000000 00000017370 13435556260 014734 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package pack_test
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/pack"
"github.com/snapcore/snapd/snap/squashfs"
"github.com/snapcore/snapd/testutil"
)
func Test(t *testing.T) { TestingT(t) }
type packSuite struct {
testutil.BaseTest
}
var _ = Suite(&packSuite{})
func (s *packSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
// chdir into a tempdir
pwd, err := os.Getwd()
c.Assert(err, IsNil)
s.AddCleanup(func() { os.Chdir(pwd) })
err = os.Chdir(c.MkDir())
c.Assert(err, IsNil)
// use fake root
dirs.SetRootDir(c.MkDir())
}
func (s *packSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
}
func makeExampleSnapSourceDir(c *C, snapYamlContent string) string {
tempdir := c.MkDir()
c.Assert(os.Chmod(tempdir, 0755), IsNil)
// use meta/snap.yaml
metaDir := filepath.Join(tempdir, "meta")
err := os.Mkdir(metaDir, 0755)
c.Assert(err, IsNil)
err = ioutil.WriteFile(filepath.Join(metaDir, "snap.yaml"), []byte(snapYamlContent), 0644)
c.Assert(err, IsNil)
const helloBinContent = `#!/bin/sh
printf "hello world"
`
// an example binary
binDir := filepath.Join(tempdir, "bin")
err = os.Mkdir(binDir, 0755)
c.Assert(err, IsNil)
err = ioutil.WriteFile(filepath.Join(binDir, "hello-world"), []byte(helloBinContent), 0755)
c.Assert(err, IsNil)
// unusual permissions for dir
tmpDir := filepath.Join(tempdir, "tmp")
err = os.Mkdir(tmpDir, 0755)
c.Assert(err, IsNil)
// avoid umask
err = os.Chmod(tmpDir, 01777)
c.Assert(err, IsNil)
// and file
someFile := filepath.Join(tempdir, "file-with-perm")
err = ioutil.WriteFile(someFile, []byte(""), 0666)
c.Assert(err, IsNil)
err = os.Chmod(someFile, 0666)
c.Assert(err, IsNil)
// an example symlink
err = os.Symlink("bin/hello-world", filepath.Join(tempdir, "symlink"))
c.Assert(err, IsNil)
return tempdir
}
func (s *packSuite) TestPackNoManifestFails(c *C) {
sourceDir := makeExampleSnapSourceDir(c, "{name: hello, version: 0}")
c.Assert(os.Remove(filepath.Join(sourceDir, "meta", "snap.yaml")), IsNil)
_, err := pack.Snap(sourceDir, "", "")
c.Assert(err, ErrorMatches, `.*/meta/snap\.yaml: no such file or directory`)
}
func (s *packSuite) TestPackMissingAppFails(c *C) {
sourceDir := makeExampleSnapSourceDir(c, `name: hello
version: 0
apps:
foo:
command: bin/hello-world
`)
c.Assert(os.Remove(filepath.Join(sourceDir, "bin", "hello-world")), IsNil)
_, err := pack.Snap(sourceDir, "", "")
c.Assert(err, Equals, snap.ErrMissingPaths)
}
func (s *packSuite) TestValidateMissingAppFailsWithErrMissingPaths(c *C) {
sourceDir := makeExampleSnapSourceDir(c, `name: hello
version: 0
apps:
foo:
command: bin/hello-world
`)
c.Assert(os.Remove(filepath.Join(sourceDir, "bin", "hello-world")), IsNil)
err := pack.CheckSkeleton(sourceDir)
c.Assert(err, Equals, snap.ErrMissingPaths)
}
func (s *packSuite) TestPackExcludesBackups(c *C) {
sourceDir := makeExampleSnapSourceDir(c, "{name: hello, version: 0}")
target := c.MkDir()
// add a backup file
c.Assert(ioutil.WriteFile(filepath.Join(sourceDir, "foo~"), []byte("hi"), 0755), IsNil)
snapfile, err := pack.Snap(sourceDir, c.MkDir(), "")
c.Assert(err, IsNil)
c.Assert(squashfs.New(snapfile).Unpack("*", target), IsNil)
cmd := exec.Command("diff", "-qr", sourceDir, target)
cmd.Env = append(cmd.Env, "LANG=C")
out, err := cmd.Output()
c.Check(err, NotNil)
c.Check(string(out), Matches, `(?m)Only in \S+: foo~`)
}
func (s *packSuite) TestPackExcludesTopLevelDEBIAN(c *C) {
sourceDir := makeExampleSnapSourceDir(c, "{name: hello, version: 0}")
target := c.MkDir()
// add a toplevel DEBIAN
c.Assert(os.MkdirAll(filepath.Join(sourceDir, "DEBIAN", "foo"), 0755), IsNil)
// and a non-toplevel DEBIAN
c.Assert(os.MkdirAll(filepath.Join(sourceDir, "bar", "DEBIAN", "baz"), 0755), IsNil)
snapfile, err := pack.Snap(sourceDir, c.MkDir(), "")
c.Assert(err, IsNil)
c.Assert(squashfs.New(snapfile).Unpack("*", target), IsNil)
cmd := exec.Command("diff", "-qr", sourceDir, target)
cmd.Env = append(cmd.Env, "LANG=C")
out, err := cmd.Output()
c.Check(err, NotNil)
c.Check(string(out), Matches, `(?m)Only in \S+: DEBIAN`)
// but *only one* DEBIAN is skipped
c.Check(strings.Count(string(out), "Only in"), Equals, 1)
}
func (s *packSuite) TestPackExcludesWholeDirs(c *C) {
sourceDir := makeExampleSnapSourceDir(c, "{name: hello, version: 0}")
target := c.MkDir()
// add a file inside a skipped dir
c.Assert(os.Mkdir(filepath.Join(sourceDir, ".bzr"), 0755), IsNil)
c.Assert(ioutil.WriteFile(filepath.Join(sourceDir, ".bzr", "foo"), []byte("hi"), 0755), IsNil)
snapfile, err := pack.Snap(sourceDir, c.MkDir(), "")
c.Assert(err, IsNil)
c.Assert(squashfs.New(snapfile).Unpack("*", target), IsNil)
out, _ := exec.Command("find", sourceDir).Output()
c.Check(string(out), Not(Equals), "")
cmd := exec.Command("diff", "-qr", sourceDir, target)
cmd.Env = append(cmd.Env, "LANG=C")
out, err = cmd.Output()
c.Check(err, NotNil)
c.Check(string(out), Matches, `(?m)Only in \S+: \.bzr`)
}
func (s *packSuite) TestDebArchitecture(c *C) {
c.Check(pack.DebArchitecture(&snap.Info{Architectures: []string{"foo"}}), Equals, "foo")
c.Check(pack.DebArchitecture(&snap.Info{Architectures: []string{"foo", "bar"}}), Equals, "multi")
c.Check(pack.DebArchitecture(&snap.Info{Architectures: nil}), Equals, "all")
}
func (s *packSuite) TestPackSimple(c *C) {
sourceDir := makeExampleSnapSourceDir(c, `name: hello
version: 1.0.1
architectures: ["i386", "amd64"]
integration:
app:
apparmor-profile: meta/hello.apparmor
`)
outputDir := filepath.Join(c.MkDir(), "output")
absSnapFile := filepath.Join(c.MkDir(), "foo.snap")
type T struct {
outputDir, filename, expected string
}
table := []T{
// no output dir, no filename -> default in .
{"", "", "hello_1.0.1_multi.snap"},
// no output dir, relative filename -> filename in .
{"", "foo.snap", "foo.snap"},
// no putput dir, absolute filename -> absolute filename
{"", absSnapFile, absSnapFile},
// output dir, no filename -> default in outputdir
{outputDir, "", filepath.Join(outputDir, "hello_1.0.1_multi.snap")},
// output dir, relative filename -> filename in outputDir
{filepath.Join(outputDir, "inner"), "../foo.snap", filepath.Join(outputDir, "foo.snap")},
// output dir, absolute filename -> absolute filename
{outputDir, absSnapFile, absSnapFile},
}
for i, t := range table {
comm := Commentf("%d", i)
resultSnap, err := pack.Snap(sourceDir, t.outputDir, t.filename)
c.Assert(err, IsNil, comm)
// check that there is result
_, err = os.Stat(resultSnap)
c.Assert(err, IsNil, comm)
c.Assert(resultSnap, Equals, t.expected, comm)
// check that the content looks sane
output, err := exec.Command("unsquashfs", "-ll", resultSnap).CombinedOutput()
c.Assert(err, IsNil, comm)
for _, needle := range []string{
"meta/snap.yaml",
"bin/hello-world",
"symlink -> bin/hello-world",
} {
expr := fmt.Sprintf(`(?ms).*%s.*`, regexp.QuoteMeta(needle))
c.Assert(string(output), Matches, expr, comm)
}
}
}
snapd-2.37.4~14.04.1/snap/pack/pack.go 0000664 0000000 0000000 00000010365 13435556260 013672 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package pack
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snapdir"
"github.com/snapcore/snapd/snap/squashfs"
)
// this could be shipped as a file like "info", and save on the memory and the
// overhead of creating and removing the tempfile, but on darwin we can't AFAIK
// easily know where it's been placed. As it's not really that big, just
// shipping it in memory seems fine for now.
// from click's click.build.ClickBuilderBase, and there from
// @Dpkg::Source::Package::tar_ignore_default_pattern;
// changed to match squashfs's "-wildcards" syntax
//
// for anchored vs non-anchored syntax see RELEASE_README in squashfs-tools.
const excludesContent = `
# "anchored", only match at top level
DEBIAN
.arch-ids
.arch-inventory
.bzr
.bzr-builddeb
.bzr.backup
.bzr.tags
.bzrignore
.cvsignore
.git
.gitattributes
.gitignore
.gitmodules
.hg
.hgignore
.hgsigs
.hgtags
.shelf
.svn
CVS
DEADJOE
RCS
_MTN
_darcs
{arch}
.snapignore
# "non-anchored", match anywhere
... .[#~]*
... *.snap
... *.click
... .*.sw?
... *~
... ,,*
`
// small helper that returns the architecture of the snap, or "multi" if it's multiple arches
func debArchitecture(info *snap.Info) string {
switch len(info.Architectures) {
case 0:
return "all"
case 1:
return info.Architectures[0]
default:
return "multi"
}
}
// CheckSkeleton attempts to validate snap data in source directory
func CheckSkeleton(sourceDir string) error {
_, err := loadAndValidate(sourceDir)
return err
}
func loadAndValidate(sourceDir string) (*snap.Info, error) {
// ensure we have valid content
yaml, err := ioutil.ReadFile(filepath.Join(sourceDir, "meta", "snap.yaml"))
if err != nil {
return nil, err
}
info, err := snap.InfoFromSnapYaml(yaml)
if err != nil {
return nil, err
}
if err := snap.Validate(info); err != nil {
return nil, fmt.Errorf("cannot validate snap %q: %v", info.InstanceName(), err)
}
if err := snap.ValidateContainer(snapdir.New(sourceDir), info, logger.Noticef); err != nil {
return nil, err
}
return info, nil
}
func snapPath(info *snap.Info, targetDir, snapName string) string {
if snapName == "" {
snapName = fmt.Sprintf("%s_%s_%v.snap", info.InstanceName(), info.Version, debArchitecture(info))
}
if targetDir != "" && !filepath.IsAbs(snapName) {
snapName = filepath.Join(targetDir, snapName)
}
return snapName
}
func prepare(sourceDir, targetDir string) (*snap.Info, error) {
info, err := loadAndValidate(sourceDir)
if err != nil {
return nil, err
}
if targetDir != "" {
if err := os.MkdirAll(targetDir, 0755); err != nil {
return nil, err
}
}
return info, nil
}
func excludesFile() (filename string, err error) {
tmpf, err := ioutil.TempFile("", ".snap-pack-exclude-")
if err != nil {
return "", err
}
// inspited by ioutil.WriteFile
n, err := tmpf.Write([]byte(excludesContent))
if err == nil && n < len(excludesContent) {
err = io.ErrShortWrite
}
if err1 := tmpf.Close(); err == nil {
err = err1
}
if err == nil {
filename = tmpf.Name()
}
return filename, err
}
// Snap the given sourceDirectory and return the generated
// snap file
func Snap(sourceDir, targetDir, snapName string) (string, error) {
info, err := prepare(sourceDir, targetDir)
if err != nil {
return "", err
}
excludes, err := excludesFile()
if err != nil {
return "", err
}
defer os.Remove(excludes)
snapName = snapPath(info, targetDir, snapName)
d := squashfs.New(snapName)
if err = d.Build(sourceDir, string(info.Type), excludes); err != nil {
return "", err
}
return snapName, nil
}
snapd-2.37.4~14.04.1/snap/pack/export_test.go 0000664 0000000 0000000 00000001332 13435556260 015326 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package pack
var (
DebArchitecture = debArchitecture
)
snapd-2.37.4~14.04.1/snap/snapdir/ 0000775 0000000 0000000 00000000000 13435556260 013142 5 ustar snapd-2.37.4~14.04.1/snap/snapdir/snapdir_test.go 0000664 0000000 0000000 00000007525 13435556260 016201 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snapdir_test
import (
"fmt"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"testing"
"time"
"github.com/snapcore/snapd/snap/snapdir"
. "gopkg.in/check.v1"
)
// Hook up check.v1 into the "go test" runner
func Test(t *testing.T) { TestingT(t) }
type SnapdirTestSuite struct {
}
var _ = Suite(&SnapdirTestSuite{})
func (s *SnapdirTestSuite) TestReadFile(c *C) {
d := c.MkDir()
needle := []byte(`stuff`)
err := ioutil.WriteFile(filepath.Join(d, "foo"), needle, 0644)
c.Assert(err, IsNil)
snap := snapdir.New(d)
content, err := snap.ReadFile("foo")
c.Assert(err, IsNil)
c.Assert(content, DeepEquals, needle)
}
func (s *SnapdirTestSuite) TestListDir(c *C) {
d := c.MkDir()
err := os.MkdirAll(filepath.Join(d, "test"), 0755)
c.Assert(err, IsNil)
err = ioutil.WriteFile(filepath.Join(d, "test", "test1"), nil, 0644)
c.Assert(err, IsNil)
err = ioutil.WriteFile(filepath.Join(d, "test", "test2"), nil, 0644)
c.Assert(err, IsNil)
snap := snapdir.New(d)
fileNames, err := snap.ListDir("test")
c.Assert(err, IsNil)
c.Assert(fileNames, HasLen, 2)
c.Check(fileNames[0], Equals, "test1")
c.Check(fileNames[1], Equals, "test2")
}
func (s *SnapdirTestSuite) TestInstall(c *C) {
tryBaseDir := c.MkDir()
snap := snapdir.New(tryBaseDir)
varLibSnapd := c.MkDir()
targetPath := filepath.Join(varLibSnapd, "foo_1.0.snap")
err := snap.Install(targetPath, "unused-mount-dir")
c.Assert(err, IsNil)
symlinkTarget, err := filepath.EvalSymlinks(targetPath)
c.Assert(err, IsNil)
c.Assert(symlinkTarget, Equals, tryBaseDir)
}
func walkEqual(tryBaseDir, sub string, c *C) {
fpw := map[string]os.FileInfo{}
filepath.Walk(filepath.Join(tryBaseDir, sub), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
path, err = filepath.Rel(tryBaseDir, path)
if err != nil {
return err
}
fpw[path] = info
return nil
})
sdw := map[string]os.FileInfo{}
snap := snapdir.New(tryBaseDir)
snap.Walk(sub, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
sdw[path] = info
return nil
})
for k, v := range fpw {
c.Check(os.SameFile(sdw[k], v), Equals, true, Commentf(k))
}
for k, v := range sdw {
c.Check(os.SameFile(fpw[k], v), Equals, true, Commentf(k))
}
}
func (s *SnapdirTestSuite) TestWalk(c *C) {
// probably already done elsewhere, but just in case
rand.Seed(time.Now().UTC().UnixNano())
// https://en.wikipedia.org/wiki/Metasyntactic_variable
ns := []string{
"foobar", "foo", "bar", "baz", "qux", "quux", "quuz", "corge",
"grault", "garply", "waldo", "fred", "plugh", "xyzzy", "thud",
"wibble", "wobble", "wubble", "flob", "blep", "blah", "boop",
}
p := 1.0 / float32(len(ns))
var f func(string, int)
f = func(d string, n int) {
for _, b := range ns {
d1 := filepath.Join(d, fmt.Sprintf("%s%d", b, n))
c.Assert(os.Mkdir(d1, 0755), IsNil)
if n < 20 && rand.Float32() < p {
f(d1, n+1)
}
}
}
subs := make([]string, len(ns)+3)
// three ways of saying the same thing ¯\_(ツ)_/¯
copy(subs, []string{"/", ".", ""})
copy(subs[3:], ns)
for i := 0; i < 10; i++ {
tryBaseDir := c.MkDir()
f(tryBaseDir, 1)
for _, sub := range subs {
walkEqual(tryBaseDir, sub, c)
}
}
}
snapd-2.37.4~14.04.1/snap/snapdir/snapdir.go 0000664 0000000 0000000 00000010022 13435556260 015124 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2015 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snapdir
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
)
// SnapDir is the snapdir based snap.
type SnapDir struct {
path string
}
// Path returns the path of the backing container.
func (s *SnapDir) Path() string {
return s.path
}
// New returns a new snap directory container.
func New(path string) *SnapDir {
return &SnapDir{path: path}
}
func (s *SnapDir) Size() (size int64, err error) {
totalSize := int64(0)
f := func(_ string, info os.FileInfo, err error) error {
totalSize += info.Size()
return err
}
filepath.Walk(s.path, f)
return totalSize, nil
}
func (s *SnapDir) Install(targetPath, mountDir string) error {
return os.Symlink(s.path, targetPath)
}
func (s *SnapDir) ReadFile(file string) (content []byte, err error) {
return ioutil.ReadFile(filepath.Join(s.path, file))
}
func littleWalk(dirPath string, dirHandle *os.File, dirstack *[]string, walkFn filepath.WalkFunc) error {
const numSt = 100
sts, err := dirHandle.Readdir(numSt)
if err != nil {
return err
}
for _, st := range sts {
path := filepath.Join(dirPath, st.Name())
if err := walkFn(path, st, nil); err != nil {
if st.IsDir() && err == filepath.SkipDir {
// caller wants to skip this directory
continue
}
return err
} else if st.IsDir() {
*dirstack = append(*dirstack, path)
}
}
return nil
}
// Walk (part of snap.Container) is like filepath.Walk, without the ordering guarantee.
func (s *SnapDir) Walk(relative string, walkFn filepath.WalkFunc) error {
relative = filepath.Clean(relative)
if relative == "" || relative == "/" {
relative = "."
} else if relative[0] == '/' {
// I said relative, darn it :-)
relative = relative[1:]
}
root := filepath.Join(s.path, relative)
// we could just filepath.Walk(root, walkFn), but that doesn't scale
// well to insanely big directories as it reads the whole directory,
// in order to sort it. This Walk doesn't do that.
//
// Also the directory is always relative to the top of the container
// for us, which would make it a little more messy to get right.
f, err := os.Open(root)
if err != nil {
return walkFn(relative, nil, err)
}
defer func() {
if f != nil {
f.Close()
}
}()
st, err := f.Stat()
if err != nil {
return walkFn(relative, nil, err)
}
err = walkFn(relative, st, nil)
if err != nil {
return err
}
if !st.IsDir() {
return nil
}
var dirstack []string
for {
if err := littleWalk(relative, f, &dirstack, walkFn); err != nil {
if err != io.EOF {
err = walkFn(relative, nil, err)
if err != nil {
return err
}
}
if len(dirstack) == 0 {
// finished
break
}
f.Close()
f = nil
for f == nil && len(dirstack) > 0 {
relative = dirstack[0]
f, err = os.Open(filepath.Join(s.path, relative))
if err != nil {
err = walkFn(relative, nil, err)
if err != nil {
return err
}
}
dirstack = dirstack[1:]
}
if f == nil {
break
}
continue
}
}
return nil
}
func (s *SnapDir) ListDir(path string) ([]string, error) {
fileInfos, err := ioutil.ReadDir(filepath.Join(s.path, path))
if err != nil {
return nil, err
}
var fileNames []string
for _, fileInfo := range fileInfos {
fileNames = append(fileNames, fileInfo.Name())
}
return fileNames, nil
}
func (s *SnapDir) Unpack(src, dstDir string) error {
return fmt.Errorf("unpack is not supported with snaps of type snapdir")
}
snapd-2.37.4~14.04.1/snap/info_test.go 0000664 0000000 0000000 00000144030 13435556260 014025 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
. "gopkg.in/check.v1"
"gopkg.in/yaml.v2"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/snap/squashfs"
"github.com/snapcore/snapd/testutil"
)
type infoSuite struct {
testutil.BaseTest
}
type infoSimpleSuite struct{}
var _ = Suite(&infoSuite{})
var _ = Suite(&infoSimpleSuite{})
func (s *infoSimpleSuite) SetUpTest(c *C) {
dirs.SetRootDir(c.MkDir())
}
func (s *infoSimpleSuite) TearDownTest(c *C) {
dirs.SetRootDir("")
}
func (s *infoSimpleSuite) TestReadInfoPanicsIfSanitizeUnset(c *C) {
si := &snap.SideInfo{Revision: snap.R(1)}
snaptest.MockSnap(c, sampleYaml, si)
c.Assert(func() { snap.ReadInfo("sample", si) }, Panics, `SanitizePlugsSlots function not set`)
}
func (s *infoSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
dirs.SetRootDir(c.MkDir())
hookType := snap.NewHookType(regexp.MustCompile(".*"))
s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
s.BaseTest.AddCleanup(snap.MockSupportedHookTypes([]*snap.HookType{hookType}))
}
func (s *infoSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
dirs.SetRootDir("")
}
func (s *infoSuite) TestSideInfoOverrides(c *C) {
info := &snap.Info{
SuggestedName: "name",
OriginalSummary: "summary",
OriginalDescription: "desc",
}
info.SideInfo = snap.SideInfo{
RealName: "newname",
EditedSummary: "fixed summary",
EditedDescription: "fixed desc",
Revision: snap.R(1),
SnapID: "snapidsnapidsnapidsnapidsnapidsn",
}
c.Check(info.InstanceName(), Equals, "newname")
c.Check(info.Summary(), Equals, "fixed summary")
c.Check(info.Description(), Equals, "fixed desc")
c.Check(info.Revision, Equals, snap.R(1))
c.Check(info.SnapID, Equals, "snapidsnapidsnapidsnapidsnapidsn")
}
func (s *infoSuite) TestAppInfoSecurityTag(c *C) {
appInfo := &snap.AppInfo{Snap: &snap.Info{SuggestedName: "http"}, Name: "GET"}
c.Check(appInfo.SecurityTag(), Equals, "snap.http.GET")
}
func (s *infoSuite) TestPlugSlotSecurityTags(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(`name: name
apps:
app1:
app2:
hooks:
hook1:
plugs:
plug:
slots:
slot:
`))
c.Assert(err, IsNil)
c.Assert(info.Plugs["plug"].SecurityTags(), DeepEquals, []string{
"snap.name.app1", "snap.name.app2", "snap.name.hook.hook1"})
c.Assert(info.Slots["slot"].SecurityTags(), DeepEquals, []string{
"snap.name.app1", "snap.name.app2", "snap.name.hook.hook1"})
}
func (s *infoSuite) TestAppInfoWrapperPath(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(`name: foo
apps:
foo:
bar:
`))
c.Assert(err, IsNil)
c.Check(info.Apps["bar"].WrapperPath(), Equals, filepath.Join(dirs.SnapBinariesDir, "foo.bar"))
c.Check(info.Apps["foo"].WrapperPath(), Equals, filepath.Join(dirs.SnapBinariesDir, "foo"))
}
func (s *infoSuite) TestAppInfoCompleterPath(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(`name: foo
apps:
foo:
bar:
`))
c.Assert(err, IsNil)
c.Check(info.Apps["bar"].CompleterPath(), Equals, filepath.Join(dirs.CompletersDir, "foo.bar"))
c.Check(info.Apps["foo"].CompleterPath(), Equals, filepath.Join(dirs.CompletersDir, "foo"))
}
func (s *infoSuite) TestAppInfoLauncherCommand(c *C) {
dirs.SetRootDir("")
info, err := snap.InfoFromSnapYaml([]byte(`name: foo
apps:
foo:
command: foo-bin
bar:
command: bar-bin -x
baz:
command: bar-bin -x
timer: 10:00-12:00,,mon,12:00~14:00
`))
c.Assert(err, IsNil)
info.Revision = snap.R(42)
c.Check(info.Apps["bar"].LauncherCommand(), Equals, "/usr/bin/snap run foo.bar")
c.Check(info.Apps["bar"].LauncherStopCommand(), Equals, "/usr/bin/snap run --command=stop foo.bar")
c.Check(info.Apps["bar"].LauncherReloadCommand(), Equals, "/usr/bin/snap run --command=reload foo.bar")
c.Check(info.Apps["bar"].LauncherPostStopCommand(), Equals, "/usr/bin/snap run --command=post-stop foo.bar")
c.Check(info.Apps["foo"].LauncherCommand(), Equals, "/usr/bin/snap run foo")
c.Check(info.Apps["baz"].LauncherCommand(), Equals, `/usr/bin/snap run --timer="10:00-12:00,,mon,12:00~14:00" foo.baz`)
// snap with instance key
info.InstanceKey = "instance"
c.Check(info.Apps["bar"].LauncherCommand(), Equals, "/usr/bin/snap run foo_instance.bar")
c.Check(info.Apps["bar"].LauncherStopCommand(), Equals, "/usr/bin/snap run --command=stop foo_instance.bar")
c.Check(info.Apps["bar"].LauncherReloadCommand(), Equals, "/usr/bin/snap run --command=reload foo_instance.bar")
c.Check(info.Apps["bar"].LauncherPostStopCommand(), Equals, "/usr/bin/snap run --command=post-stop foo_instance.bar")
c.Check(info.Apps["foo"].LauncherCommand(), Equals, "/usr/bin/snap run foo_instance")
c.Check(info.Apps["baz"].LauncherCommand(), Equals, `/usr/bin/snap run --timer="10:00-12:00,,mon,12:00~14:00" foo_instance.baz`)
}
const sampleYaml = `
name: sample
version: 1
apps:
app:
command: foo
app2:
command: bar
sample:
command: foobar
command-chain: [chain]
hooks:
configure:
command-chain: [hookchain]
`
func (s *infoSuite) TestReadInfo(c *C) {
si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"}
snapInfo1 := snaptest.MockSnap(c, sampleYaml, si)
snapInfo2, err := snap.ReadInfo("sample", si)
c.Assert(err, IsNil)
c.Check(snapInfo2.InstanceName(), Equals, "sample")
c.Check(snapInfo2.Revision, Equals, snap.R(42))
c.Check(snapInfo2.Summary(), Equals, "esummary")
c.Check(snapInfo2.Apps["app"].Command, Equals, "foo")
c.Check(snapInfo2.Apps["sample"].CommandChain, DeepEquals, []string{"chain"})
c.Check(snapInfo2.Hooks["configure"].CommandChain, DeepEquals, []string{"hookchain"})
c.Check(snapInfo2, DeepEquals, snapInfo1)
}
func (s *infoSuite) TestReadInfoWithInstance(c *C) {
si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "instance summary"}
snapInfo1 := snaptest.MockSnapInstance(c, "sample_instance", sampleYaml, si)
snapInfo2, err := snap.ReadInfo("sample_instance", si)
c.Assert(err, IsNil)
c.Check(snapInfo2.InstanceName(), Equals, "sample_instance")
c.Check(snapInfo2.SnapName(), Equals, "sample")
c.Check(snapInfo2.Revision, Equals, snap.R(42))
c.Check(snapInfo2.Summary(), Equals, "instance summary")
c.Check(snapInfo2.Apps["app"].Command, Equals, "foo")
c.Check(snapInfo2.Apps["sample"].CommandChain, DeepEquals, []string{"chain"})
c.Check(snapInfo2.Hooks["configure"].CommandChain, DeepEquals, []string{"hookchain"})
c.Check(snapInfo2, DeepEquals, snapInfo1)
}
func (s *infoSuite) TestReadCurrentInfo(c *C) {
si := &snap.SideInfo{Revision: snap.R(42)}
snapInfo1 := snaptest.MockSnapCurrent(c, sampleYaml, si)
snapInfo2, err := snap.ReadCurrentInfo("sample")
c.Assert(err, IsNil)
c.Check(snapInfo2.InstanceName(), Equals, "sample")
c.Check(snapInfo2.Revision, Equals, snap.R(42))
c.Check(snapInfo2, DeepEquals, snapInfo1)
snapInfo3, err := snap.ReadCurrentInfo("not-sample")
c.Check(snapInfo3, IsNil)
c.Assert(err, ErrorMatches, `cannot find current revision for snap not-sample:.*`)
}
func (s *infoSuite) TestReadCurrentInfoWithInstance(c *C) {
si := &snap.SideInfo{Revision: snap.R(42)}
snapInfo1 := snaptest.MockSnapInstanceCurrent(c, "sample_instance", sampleYaml, si)
snapInfo2, err := snap.ReadCurrentInfo("sample_instance")
c.Assert(err, IsNil)
c.Check(snapInfo2.InstanceName(), Equals, "sample_instance")
c.Check(snapInfo2.SnapName(), Equals, "sample")
c.Check(snapInfo2.Revision, Equals, snap.R(42))
c.Check(snapInfo2, DeepEquals, snapInfo1)
snapInfo3, err := snap.ReadCurrentInfo("sample_other")
c.Check(snapInfo3, IsNil)
c.Assert(err, ErrorMatches, `cannot find current revision for snap sample_other:.*`)
}
func (s *infoSuite) TestInstallDate(c *C) {
si := &snap.SideInfo{Revision: snap.R(1)}
info := snaptest.MockSnap(c, sampleYaml, si)
// not current -> Zero
c.Check(info.InstallDate().IsZero(), Equals, true)
c.Check(snap.InstallDate(info.InstanceName()).IsZero(), Equals, true)
mountdir := info.MountDir()
dir, rev := filepath.Split(mountdir)
c.Assert(os.MkdirAll(dir, 0755), IsNil)
cur := filepath.Join(dir, "current")
c.Assert(os.Symlink(rev, cur), IsNil)
st, err := os.Lstat(cur)
c.Assert(err, IsNil)
instTime := st.ModTime()
// sanity
c.Check(instTime.IsZero(), Equals, false)
c.Check(info.InstallDate().Equal(instTime), Equals, true)
c.Check(snap.InstallDate(info.InstanceName()).Equal(instTime), Equals, true)
}
func (s *infoSuite) TestReadInfoNotFound(c *C) {
si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"}
info, err := snap.ReadInfo("sample", si)
c.Check(info, IsNil)
c.Check(err, ErrorMatches, `cannot find installed snap "sample" at revision 42: missing file .*sample/42/meta/snap.yaml`)
bse, ok := err.(snap.BrokenSnapError)
c.Assert(ok, Equals, true)
c.Check(bse.Broken(), Equals, bse.Error())
}
func (s *infoSuite) TestReadInfoUnreadable(c *C) {
si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"}
c.Assert(os.MkdirAll(filepath.Join(snap.MinimalPlaceInfo("sample", si.Revision).MountDir(), "meta", "snap.yaml"), 0755), IsNil)
info, err := snap.ReadInfo("sample", si)
c.Check(info, IsNil)
// TODO: maybe improve this error message
c.Check(err, ErrorMatches, ".* is a directory")
}
func (s *infoSuite) TestReadInfoUnparsable(c *C) {
si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"}
p := filepath.Join(snap.MinimalPlaceInfo("sample", si.Revision).MountDir(), "meta", "snap.yaml")
c.Assert(os.MkdirAll(filepath.Dir(p), 0755), IsNil)
c.Assert(ioutil.WriteFile(p, []byte(`- :`), 0644), IsNil)
info, err := snap.ReadInfo("sample", si)
c.Check(info, IsNil)
// TODO: maybe improve this error message
c.Check(err, ErrorMatches, `cannot use installed snap "sample" at revision 42: cannot parse snap.yaml: yaml: .*`)
bse, ok := err.(snap.BrokenSnapError)
c.Assert(ok, Equals, true)
c.Check(bse.Broken(), Equals, bse.Error())
}
func (s *infoSuite) TestReadInfoUnfindable(c *C) {
si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"}
p := filepath.Join(snap.MinimalPlaceInfo("sample", si.Revision).MountDir(), "meta", "snap.yaml")
c.Assert(os.MkdirAll(filepath.Dir(p), 0755), IsNil)
c.Assert(ioutil.WriteFile(p, []byte(``), 0644), IsNil)
info, err := snap.ReadInfo("sample", si)
c.Check(err, ErrorMatches, `cannot find installed snap "sample" at revision 42: missing file .*var/lib/snapd/snaps/sample_42.snap`)
c.Check(info, IsNil)
}
func (s *infoSuite) TestReadInfoDanglingSymlink(c *C) {
si := &snap.SideInfo{Revision: snap.R(42), EditedSummary: "esummary"}
mpi := snap.MinimalPlaceInfo("sample", si.Revision)
p := filepath.Join(mpi.MountDir(), "meta", "snap.yaml")
c.Assert(os.MkdirAll(filepath.Dir(p), 0755), IsNil)
c.Assert(ioutil.WriteFile(p, []byte(`name: test`), 0644), IsNil)
c.Assert(os.MkdirAll(filepath.Dir(mpi.MountFile()), 0755), IsNil)
c.Assert(os.Symlink("/dangling", mpi.MountFile()), IsNil)
info, err := snap.ReadInfo("sample", si)
c.Check(err, IsNil)
c.Check(info.SnapName(), Equals, "test")
c.Check(info.Revision, Equals, snap.R(42))
c.Check(info.Summary(), Equals, "esummary")
c.Check(info.Size, Equals, int64(0))
}
// makeTestSnap here can also be used to produce broken snaps (differently from snaptest.MakeTestSnapWithFiles)!
func makeTestSnap(c *C, snapYaml string) string {
var m struct {
Type string `yaml:"type"`
}
yaml.Unmarshal([]byte(snapYaml), &m) // yes, ignore the error
tmp := c.MkDir()
snapSource := filepath.Join(tmp, "snapsrc")
err := os.MkdirAll(filepath.Join(snapSource, "meta"), 0755)
c.Assert(err, IsNil)
// our regular snap.yaml
err = ioutil.WriteFile(filepath.Join(snapSource, "meta", "snap.yaml"), []byte(snapYaml), 0644)
c.Assert(err, IsNil)
dest := filepath.Join(tmp, "foo.snap")
snap := squashfs.New(dest)
err = snap.Build(snapSource, m.Type)
c.Assert(err, IsNil)
return dest
}
// produce descrs for empty hooks suitable for snaptest.PopulateDir
func emptyHooks(hookNames ...string) (emptyHooks [][]string) {
for _, hookName := range hookNames {
emptyHooks = append(emptyHooks, []string{filepath.Join("meta", "hooks", hookName), ""})
}
return
}
func (s *infoSuite) TestReadInfoFromSnapFile(c *C) {
yaml := `name: foo
version: 1.0
type: app
epoch: 1*
confinement: devmode`
snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, nil)
snapf, err := snap.Open(snapPath)
c.Assert(err, IsNil)
info, err := snap.ReadInfoFromSnapFile(snapf, nil)
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "foo")
c.Check(info.Version, Equals, "1.0")
c.Check(info.Type, Equals, snap.TypeApp)
c.Check(info.Revision, Equals, snap.R(0))
c.Check(info.Epoch.String(), Equals, "1*")
c.Check(info.Confinement, Equals, snap.DevModeConfinement)
c.Check(info.NeedsDevMode(), Equals, true)
c.Check(info.NeedsClassic(), Equals, false)
}
func (s *infoSuite) TestReadInfoFromClassicSnapFile(c *C) {
yaml := `name: foo
version: 1.0
type: app
confinement: classic`
snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, nil)
snapf, err := snap.Open(snapPath)
c.Assert(err, IsNil)
info, err := snap.ReadInfoFromSnapFile(snapf, nil)
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "foo")
c.Check(info.Version, Equals, "1.0")
c.Check(info.Type, Equals, snap.TypeApp)
c.Check(info.Revision, Equals, snap.R(0))
c.Check(info.Confinement, Equals, snap.ClassicConfinement)
c.Check(info.NeedsDevMode(), Equals, false)
c.Check(info.NeedsClassic(), Equals, true)
}
func (s *infoSuite) TestReadInfoFromSnapFileMissingEpoch(c *C) {
yaml := `name: foo
version: 1.0
type: app`
snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, nil)
snapf, err := snap.Open(snapPath)
c.Assert(err, IsNil)
info, err := snap.ReadInfoFromSnapFile(snapf, nil)
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "foo")
c.Check(info.Version, Equals, "1.0")
c.Check(info.Type, Equals, snap.TypeApp)
c.Check(info.Revision, Equals, snap.R(0))
c.Check(info.Epoch.String(), Equals, "0") // Defaults to 0
c.Check(info.Confinement, Equals, snap.StrictConfinement)
c.Check(info.NeedsDevMode(), Equals, false)
}
func (s *infoSuite) TestReadInfoFromSnapFileWithSideInfo(c *C) {
yaml := `name: foo
version: 1.0
type: app`
snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, nil)
snapf, err := snap.Open(snapPath)
c.Assert(err, IsNil)
info, err := snap.ReadInfoFromSnapFile(snapf, &snap.SideInfo{
RealName: "baz",
Revision: snap.R(42),
})
c.Assert(err, IsNil)
c.Check(info.InstanceName(), Equals, "baz")
c.Check(info.Version, Equals, "1.0")
c.Check(info.Type, Equals, snap.TypeApp)
c.Check(info.Revision, Equals, snap.R(42))
}
func (s *infoSuite) TestReadInfoFromSnapFileValidates(c *C) {
yaml := `name: foo.bar
version: 1.0
type: app`
snapPath := makeTestSnap(c, yaml)
snapf, err := snap.Open(snapPath)
c.Assert(err, IsNil)
_, err = snap.ReadInfoFromSnapFile(snapf, nil)
c.Assert(err, ErrorMatches, `invalid snap name.*`)
}
func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidType(c *C) {
yaml := `name: foo
version: 1.0
type: foo`
snapPath := makeTestSnap(c, yaml)
snapf, err := snap.Open(snapPath)
c.Assert(err, IsNil)
_, err = snap.ReadInfoFromSnapFile(snapf, nil)
c.Assert(err, ErrorMatches, ".*invalid snap type.*")
}
func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidConfinement(c *C) {
yaml := `name: foo
version: 1.0
confinement: foo`
snapPath := makeTestSnap(c, yaml)
snapf, err := snap.Open(snapPath)
c.Assert(err, IsNil)
_, err = snap.ReadInfoFromSnapFile(snapf, nil)
c.Assert(err, ErrorMatches, ".*invalid confinement type.*")
}
func (s *infoSuite) TestAppEnvSimple(c *C) {
yaml := `name: foo
version: 1.0
type: app
environment:
global-k: global-v
apps:
foo:
environment:
app-k: app-v
`
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
env := info.Apps["foo"].Env()
sort.Strings(env)
c.Check(env, DeepEquals, []string{
"app-k=app-v",
"global-k=global-v",
})
}
func (s *infoSuite) TestAppEnvOverrideGlobal(c *C) {
yaml := `name: foo
version: 1.0
type: app
environment:
global-k: global-v
global-and-local: global-v
apps:
foo:
environment:
app-k: app-v
global-and-local: local-v
`
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
env := info.Apps["foo"].Env()
sort.Strings(env)
c.Check(env, DeepEquals, []string{
"app-k=app-v",
"global-and-local=local-v",
"global-k=global-v",
})
}
func (s *infoSuite) TestHookEnvSimple(c *C) {
yaml := `name: foo
version: 1.0
type: app
environment:
global-k: global-v
hooks:
foo:
environment:
app-k: app-v
`
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
env := info.Hooks["foo"].Env()
sort.Strings(env)
c.Check(env, DeepEquals, []string{
"app-k=app-v",
"global-k=global-v",
})
}
func (s *infoSuite) TestHookEnvOverrideGlobal(c *C) {
yaml := `name: foo
version: 1.0
type: app
environment:
global-k: global-v
global-and-local: global-v
hooks:
foo:
environment:
app-k: app-v
global-and-local: local-v
`
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)
env := info.Hooks["foo"].Env()
sort.Strings(env)
c.Check(env, DeepEquals, []string{
"app-k=app-v",
"global-and-local=local-v",
"global-k=global-v",
})
}
func (s *infoSuite) TestSplitSnapApp(c *C) {
for _, t := range []struct {
in string
out []string
}{
// normal cases
{"foo.bar", []string{"foo", "bar"}},
{"foo.bar.baz", []string{"foo", "bar.baz"}},
// special case, snapName == appName
{"foo", []string{"foo", "foo"}},
// snap instance names
{"foo_instance.bar", []string{"foo_instance", "bar"}},
{"foo_instance.bar.baz", []string{"foo_instance", "bar.baz"}},
{"foo_instance", []string{"foo_instance", "foo"}},
} {
snap, app := snap.SplitSnapApp(t.in)
c.Check([]string{snap, app}, DeepEquals, t.out)
}
}
func (s *infoSuite) TestJoinSnapApp(c *C) {
for _, t := range []struct {
in []string
out string
}{
// normal cases
{[]string{"foo", "bar"}, "foo.bar"},
{[]string{"foo", "bar-baz"}, "foo.bar-baz"},
// special case, snapName == appName
{[]string{"foo", "foo"}, "foo"},
// snap instance names
{[]string{"foo_instance", "bar"}, "foo_instance.bar"},
{[]string{"foo_instance", "bar-baz"}, "foo_instance.bar-baz"},
{[]string{"foo_instance", "foo"}, "foo_instance"},
} {
snapApp := snap.JoinSnapApp(t.in[0], t.in[1])
c.Check(snapApp, Equals, t.out)
}
}
func ExampleSplitSnapApp() {
fmt.Println(snap.SplitSnapApp("hello-world.env"))
// Output: hello-world env
}
func ExampleSplitSnapApp_short() {
fmt.Println(snap.SplitSnapApp("hello-world"))
// Output: hello-world hello-world
}
func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidHook(c *C) {
yaml := `name: foo
version: 1.0
hooks:
123abc:`
snapPath := makeTestSnap(c, yaml)
snapf, err := snap.Open(snapPath)
c.Assert(err, IsNil)
_, err = snap.ReadInfoFromSnapFile(snapf, nil)
c.Assert(err, ErrorMatches, ".*invalid hook name.*")
}
func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidImplicitHook(c *C) {
yaml := `name: foo
version: 1.0`
snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, emptyHooks("123abc"))
snapf, err := snap.Open(snapPath)
c.Assert(err, IsNil)
_, err = snap.ReadInfoFromSnapFile(snapf, nil)
c.Assert(err, ErrorMatches, ".*invalid hook name.*")
}
func (s *infoSuite) checkInstalledSnapAndSnapFile(c *C, instanceName, yaml string, contents string, hooks []string, checker func(c *C, info *snap.Info)) {
// First check installed snap
sideInfo := &snap.SideInfo{Revision: snap.R(42)}
info0 := snaptest.MockSnapInstance(c, instanceName, yaml, sideInfo)
snaptest.PopulateDir(info0.MountDir(), emptyHooks(hooks...))
info, err := snap.ReadInfo(info0.InstanceName(), sideInfo)
c.Check(err, IsNil)
checker(c, info)
// Now check snap file
snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, emptyHooks(hooks...))
snapf, err := snap.Open(snapPath)
c.Assert(err, IsNil)
info, err = snap.ReadInfoFromSnapFile(snapf, nil)
c.Check(err, IsNil)
checker(c, info)
}
func (s *infoSuite) TestReadInfoNoHooks(c *C) {
yaml := `name: foo
version: 1.0`
s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", nil, func(c *C, info *snap.Info) {
// Verify that no hooks were loaded for this snap
c.Check(info.Hooks, HasLen, 0)
})
}
func (s *infoSuite) TestReadInfoSingleImplicitHook(c *C) {
yaml := `name: foo
version: 1.0`
s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"test-hook"}, func(c *C, info *snap.Info) {
// Verify that the `test-hook` hook has now been loaded, and that it has
// no associated plugs.
c.Check(info.Hooks, HasLen, 1)
verifyImplicitHook(c, info, "test-hook", nil)
})
}
func (s *infoSuite) TestReadInfoMultipleImplicitHooks(c *C) {
yaml := `name: foo
version: 1.0`
s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"foo", "bar"}, func(c *C, info *snap.Info) {
// Verify that both hooks have now been loaded, and that neither have any
// associated plugs.
c.Check(info.Hooks, HasLen, 2)
verifyImplicitHook(c, info, "foo", nil)
verifyImplicitHook(c, info, "bar", nil)
})
}
func (s *infoSuite) TestReadInfoInvalidImplicitHook(c *C) {
hookType := snap.NewHookType(regexp.MustCompile("foo"))
s.BaseTest.AddCleanup(snap.MockSupportedHookTypes([]*snap.HookType{hookType}))
yaml := `name: foo
version: 1.0`
s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"foo", "bar"}, func(c *C, info *snap.Info) {
// Verify that only foo has been loaded, not bar
c.Check(info.Hooks, HasLen, 1)
verifyImplicitHook(c, info, "foo", nil)
})
}
func (s *infoSuite) TestReadInfoImplicitAndExplicitHooks(c *C) {
yaml := `name: foo
version: 1.0
hooks:
explicit:
plugs: [test-plug]
slots: [test-slot]`
s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"explicit", "implicit"}, func(c *C, info *snap.Info) {
// Verify that the `implicit` hook has now been loaded, and that it has
// no associated plugs. Also verify that the `explicit` hook is still
// valid.
c.Check(info.Hooks, HasLen, 2)
verifyImplicitHook(c, info, "implicit", nil)
verifyExplicitHook(c, info, "explicit", []string{"test-plug"}, []string{"test-slot"})
})
}
func (s *infoSuite) TestReadInfoExplicitHooks(c *C) {
yaml := `name: foo
version: 1.0
plugs:
test-plug:
slots:
test-slot:
hooks:
explicit:
`
s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"explicit"}, func(c *C, info *snap.Info) {
c.Check(info.Hooks, HasLen, 1)
verifyExplicitHook(c, info, "explicit", []string{"test-plug"}, []string{"test-slot"})
})
}
func (s *infoSuite) TestParallelInstanceReadInfoImplicitAndExplicitHooks(c *C) {
yaml := `name: foo
version: 1.0
hooks:
explicit:
plugs: [test-plug]
slots: [test-slot]`
s.checkInstalledSnapAndSnapFile(c, "foo_instance", yaml, "SNAP", []string{"explicit", "implicit"}, func(c *C, info *snap.Info) {
c.Check(info.Hooks, HasLen, 2)
verifyImplicitHook(c, info, "implicit", nil)
verifyExplicitHook(c, info, "explicit", []string{"test-plug"}, []string{"test-slot"})
})
}
func (s *infoSuite) TestReadInfoImplicitHookWithTopLevelPlugSlots(c *C) {
yaml := `name: foo
version: 1.0
plugs:
test-plug:
slots:
test-slot:
hooks:
explicit:
plugs: [test-plug,other-plug]
slots: [test-slot,other-slot]
`
yaml2 := `name: foo
version: 1.0
plugs:
test-plug:
slots:
test-slot:
`
s.checkInstalledSnapAndSnapFile(c, "foo", yaml, "SNAP", []string{"implicit"}, func(c *C, info *snap.Info) {
c.Check(info.Hooks, HasLen, 2)
implicitHook := info.Hooks["implicit"]
c.Assert(implicitHook, NotNil)
c.Assert(implicitHook.Explicit, Equals, false)
c.Assert(implicitHook.Plugs, HasLen, 1)
c.Assert(implicitHook.Slots, HasLen, 1)
c.Check(info.Plugs, HasLen, 2)
c.Check(info.Slots, HasLen, 2)
plug := info.Plugs["test-plug"]
c.Assert(plug, NotNil)
c.Assert(implicitHook.Plugs["test-plug"], DeepEquals, plug)
slot := info.Slots["test-slot"]
c.Assert(slot, NotNil)
c.Assert(implicitHook.Slots["test-slot"], DeepEquals, slot)
explicitHook := info.Hooks["explicit"]
c.Assert(explicitHook, NotNil)
c.Assert(explicitHook.Explicit, Equals, true)
c.Assert(explicitHook.Plugs, HasLen, 2)
c.Assert(explicitHook.Slots, HasLen, 2)
plug = info.Plugs["test-plug"]
c.Assert(plug, NotNil)
c.Assert(explicitHook.Plugs["test-plug"], DeepEquals, plug)
slot = info.Slots["test-slot"]
c.Assert(slot, NotNil)
c.Assert(explicitHook.Slots["test-slot"], DeepEquals, slot)
})
s.checkInstalledSnapAndSnapFile(c, "foo", yaml2, "SNAP", []string{"implicit"}, func(c *C, info *snap.Info) {
c.Check(info.Hooks, HasLen, 1)
implicitHook := info.Hooks["implicit"]
c.Assert(implicitHook, NotNil)
c.Assert(implicitHook.Explicit, Equals, false)
c.Assert(implicitHook.Plugs, HasLen, 1)
c.Assert(implicitHook.Slots, HasLen, 1)
c.Check(info.Plugs, HasLen, 1)
c.Check(info.Slots, HasLen, 1)
plug := info.Plugs["test-plug"]
c.Assert(plug, NotNil)
c.Assert(implicitHook.Plugs["test-plug"], DeepEquals, plug)
slot := info.Slots["test-slot"]
c.Assert(slot, NotNil)
c.Assert(implicitHook.Slots["test-slot"], DeepEquals, slot)
})
}
func verifyImplicitHook(c *C, info *snap.Info, hookName string, plugNames []string) {
hook := info.Hooks[hookName]
c.Assert(hook, NotNil, Commentf("Expected hooks to contain %q", hookName))
c.Check(hook.Name, Equals, hookName)
if len(plugNames) == 0 {
c.Check(hook.Plugs, IsNil)
}
for _, plugName := range plugNames {
// Verify that the HookInfo and PlugInfo point to each other
plug := hook.Plugs[plugName]
c.Assert(plug, NotNil, Commentf("Expected hook plugs to contain %q", plugName))
c.Check(plug.Name, Equals, plugName)
c.Check(plug.Hooks, HasLen, 1)
hook = plug.Hooks[hookName]
c.Assert(hook, NotNil, Commentf("Expected plug to be associated with hook %q", hookName))
c.Check(hook.Name, Equals, hookName)
// Verify also that the hook plug made it into info.Plugs
c.Check(info.Plugs[plugName], DeepEquals, plug)
}
}
func verifyExplicitHook(c *C, info *snap.Info, hookName string, plugNames []string, slotNames []string) {
hook := info.Hooks[hookName]
c.Assert(hook, NotNil, Commentf("Expected hooks to contain %q", hookName))
c.Check(hook.Name, Equals, hookName)
c.Check(hook.Plugs, HasLen, len(plugNames))
c.Check(hook.Slots, HasLen, len(slotNames))
for _, plugName := range plugNames {
// Verify that the HookInfo and PlugInfo point to each other
plug := hook.Plugs[plugName]
c.Assert(plug, NotNil, Commentf("Expected hook plugs to contain %q", plugName))
c.Check(plug.Name, Equals, plugName)
c.Check(plug.Hooks, HasLen, 1)
hook = plug.Hooks[hookName]
c.Assert(hook, NotNil, Commentf("Expected plug to be associated with hook %q", hookName))
c.Check(hook.Name, Equals, hookName)
// Verify also that the hook plug made it into info.Plugs
c.Check(info.Plugs[plugName], DeepEquals, plug)
}
for _, slotName := range slotNames {
// Verify that the HookInfo and SlotInfo point to each other
slot := hook.Slots[slotName]
c.Assert(slot, NotNil, Commentf("Expected hook slots to contain %q", slotName))
c.Check(slot.Name, Equals, slotName)
c.Check(slot.Hooks, HasLen, 1)
hook = slot.Hooks[hookName]
c.Assert(hook, NotNil, Commentf("Expected slot to be associated with hook %q", hookName))
c.Check(hook.Name, Equals, hookName)
// Verify also that the hook plug made it into info.Slots
c.Check(info.Slots[slotName], DeepEquals, slot)
}
}
func (s *infoSuite) TestMinimalInfoDirAndFileMethods(c *C) {
dirs.SetRootDir("")
info := snap.MinimalPlaceInfo("name", snap.R("1"))
s.testDirAndFileMethods(c, info)
}
func (s *infoSuite) TestDirAndFileMethods(c *C) {
dirs.SetRootDir("")
info := &snap.Info{SuggestedName: "name"}
info.SideInfo = snap.SideInfo{Revision: snap.R(1)}
s.testDirAndFileMethods(c, info)
}
func (s *infoSuite) testDirAndFileMethods(c *C, info snap.PlaceInfo) {
c.Check(info.MountDir(), Equals, fmt.Sprintf("%s/name/1", dirs.SnapMountDir))
c.Check(info.MountFile(), Equals, "/var/lib/snapd/snaps/name_1.snap")
c.Check(info.HooksDir(), Equals, fmt.Sprintf("%s/name/1/meta/hooks", dirs.SnapMountDir))
c.Check(info.DataDir(), Equals, "/var/snap/name/1")
c.Check(info.UserDataDir("/home/bob"), Equals, "/home/bob/snap/name/1")
c.Check(info.UserCommonDataDir("/home/bob"), Equals, "/home/bob/snap/name/common")
c.Check(info.CommonDataDir(), Equals, "/var/snap/name/common")
c.Check(info.UserXdgRuntimeDir(12345), Equals, "/run/user/12345/snap.name")
// XXX: Those are actually a globs, not directories
c.Check(info.DataHomeDir(), Equals, "/home/*/snap/name/1")
c.Check(info.CommonDataHomeDir(), Equals, "/home/*/snap/name/common")
c.Check(info.XdgRuntimeDirs(), Equals, "/run/user/*/snap.name")
}
func (s *infoSuite) TestMinimalInfoDirAndFileMethodsParallelInstall(c *C) {
dirs.SetRootDir("")
info := snap.MinimalPlaceInfo("name_instance", snap.R("1"))
s.testInstanceDirAndFileMethods(c, info)
}
func (s *infoSuite) TestDirAndFileMethodsParallelInstall(c *C) {
dirs.SetRootDir("")
info := &snap.Info{SuggestedName: "name", InstanceKey: "instance"}
info.SideInfo = snap.SideInfo{Revision: snap.R(1)}
s.testInstanceDirAndFileMethods(c, info)
}
func (s *infoSuite) testInstanceDirAndFileMethods(c *C, info snap.PlaceInfo) {
c.Check(info.MountDir(), Equals, fmt.Sprintf("%s/name_instance/1", dirs.SnapMountDir))
c.Check(info.MountFile(), Equals, "/var/lib/snapd/snaps/name_instance_1.snap")
c.Check(info.HooksDir(), Equals, fmt.Sprintf("%s/name_instance/1/meta/hooks", dirs.SnapMountDir))
c.Check(info.DataDir(), Equals, "/var/snap/name_instance/1")
c.Check(info.UserDataDir("/home/bob"), Equals, "/home/bob/snap/name_instance/1")
c.Check(info.UserCommonDataDir("/home/bob"), Equals, "/home/bob/snap/name_instance/common")
c.Check(info.CommonDataDir(), Equals, "/var/snap/name_instance/common")
c.Check(info.UserXdgRuntimeDir(12345), Equals, "/run/user/12345/snap.name_instance")
// XXX: Those are actually a globs, not directories
c.Check(info.DataHomeDir(), Equals, "/home/*/snap/name_instance/1")
c.Check(info.CommonDataHomeDir(), Equals, "/home/*/snap/name_instance/common")
c.Check(info.XdgRuntimeDirs(), Equals, "/run/user/*/snap.name_instance")
}
func makeFakeDesktopFile(c *C, name, content string) string {
df := filepath.Join(dirs.SnapDesktopFilesDir, name)
err := os.MkdirAll(filepath.Dir(df), 0755)
c.Assert(err, IsNil)
err = ioutil.WriteFile(df, []byte(content), 0644)
c.Assert(err, IsNil)
return df
}
func (s *infoSuite) TestAppDesktopFile(c *C) {
snaptest.MockSnap(c, sampleYaml, &snap.SideInfo{})
snapInfo, err := snap.ReadInfo("sample", &snap.SideInfo{})
c.Assert(err, IsNil)
c.Check(snapInfo.InstanceName(), Equals, "sample")
c.Check(snapInfo.Apps["app"].DesktopFile(), Matches, `.*/var/lib/snapd/desktop/applications/sample_app.desktop`)
c.Check(snapInfo.Apps["sample"].DesktopFile(), Matches, `.*/var/lib/snapd/desktop/applications/sample_sample.desktop`)
// snap with instance key
snapInfo.InstanceKey = "instance"
c.Check(snapInfo.InstanceName(), Equals, "sample_instance")
c.Check(snapInfo.Apps["app"].DesktopFile(), Matches, `.*/var/lib/snapd/desktop/applications/sample_instance_app.desktop`)
c.Check(snapInfo.Apps["sample"].DesktopFile(), Matches, `.*/var/lib/snapd/desktop/applications/sample_instance_sample.desktop`)
}
const coreSnapYaml = `name: core
version: 0
type: os
plugs:
network-bind:
core-support:
`
// reading snap via ReadInfoFromSnapFile renames clashing core plugs
func (s *infoSuite) TestReadInfoFromSnapFileRenamesCorePlus(c *C) {
snapPath := snaptest.MakeTestSnapWithFiles(c, coreSnapYaml, nil)
snapf, err := snap.Open(snapPath)
c.Assert(err, IsNil)
info, err := snap.ReadInfoFromSnapFile(snapf, nil)
c.Assert(err, IsNil)
c.Check(info.Plugs["network-bind"], IsNil)
c.Check(info.Plugs["core-support"], IsNil)
c.Check(info.Plugs["network-bind-plug"], NotNil)
c.Check(info.Plugs["core-support-plug"], NotNil)
}
// reading snap via ReadInfo renames clashing core plugs
func (s *infoSuite) TestReadInfoRenamesCorePlugs(c *C) {
si := &snap.SideInfo{Revision: snap.R(42), RealName: "core"}
snaptest.MockSnap(c, coreSnapYaml, si)
info, err := snap.ReadInfo("core", si)
c.Assert(err, IsNil)
c.Check(info.Plugs["network-bind"], IsNil)
c.Check(info.Plugs["core-support"], IsNil)
c.Check(info.Plugs["network-bind-plug"], NotNil)
c.Check(info.Plugs["core-support-plug"], NotNil)
}
// reading snap via InfoFromSnapYaml renames clashing core plugs
func (s *infoSuite) TestInfoFromSnapYamlRenamesCorePlugs(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(coreSnapYaml))
c.Assert(err, IsNil)
c.Check(info.Plugs["network-bind"], IsNil)
c.Check(info.Plugs["core-support"], IsNil)
c.Check(info.Plugs["network-bind-plug"], NotNil)
c.Check(info.Plugs["core-support-plug"], NotNil)
}
func (s *infoSuite) TestInfoServices(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(`name: pans
apps:
svc1:
daemon: potato
svc2:
daemon: no
app1:
app2:
`))
c.Assert(err, IsNil)
svcNames := []string{}
svcs := info.Services()
for i := range svcs {
svcNames = append(svcNames, svcs[i].ServiceName())
}
sort.Strings(svcNames)
c.Check(svcNames, DeepEquals, []string{
"snap.pans.svc1.service",
"snap.pans.svc2.service",
})
// snap with instance
info.InstanceKey = "instance"
svcNames = []string{}
for i := range info.Services() {
svcNames = append(svcNames, svcs[i].ServiceName())
}
sort.Strings(svcNames)
c.Check(svcNames, DeepEquals, []string{
"snap.pans_instance.svc1.service",
"snap.pans_instance.svc2.service",
})
}
func (s *infoSuite) TestAppInfoIsService(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(`name: pans
apps:
svc1:
daemon: potato
svc2:
daemon: no
app1:
app2:
`))
c.Assert(err, IsNil)
svc := info.Apps["svc1"]
c.Check(svc.IsService(), Equals, true)
c.Check(svc.ServiceName(), Equals, "snap.pans.svc1.service")
c.Check(svc.ServiceFile(), Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans.svc1.service")
c.Check(info.Apps["svc2"].IsService(), Equals, true)
c.Check(info.Apps["app1"].IsService(), Equals, false)
c.Check(info.Apps["app1"].IsService(), Equals, false)
// snap with instance key
info.InstanceKey = "instance"
c.Check(svc.ServiceName(), Equals, "snap.pans_instance.svc1.service")
c.Check(svc.ServiceFile(), Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans_instance.svc1.service")
}
func (s *infoSuite) TestAppInfoStringer(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(`name: asnap
apps:
one:
daemon: simple
`))
c.Assert(err, IsNil)
c.Check(fmt.Sprintf("%q", info.Apps["one"].String()), Equals, `"asnap.one"`)
}
func (s *infoSuite) TestSocketFile(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(`name: pans
apps:
app1:
daemon: true
sockets:
sock1:
listen-stream: /tmp/sock1.socket
`))
c.Assert(err, IsNil)
app := info.Apps["app1"]
socket := app.Sockets["sock1"]
c.Check(socket.File(), Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans.app1.sock1.socket")
// snap with instance key
info.InstanceKey = "instance"
c.Check(socket.File(), Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans_instance.app1.sock1.socket")
}
func (s *infoSuite) TestTimerFile(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(`name: pans
apps:
app1:
daemon: true
timer: mon,10:00-12:00
`))
c.Assert(err, IsNil)
app := info.Apps["app1"]
timerFile := app.Timer.File()
c.Check(timerFile, Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans.app1.timer")
c.Check(strings.TrimSuffix(app.ServiceFile(), ".service")+".timer", Equals, timerFile)
// snap with instance key
info.InstanceKey = "instance"
c.Check(app.Timer.File(), Equals, dirs.GlobalRootDir+"/etc/systemd/system/snap.pans_instance.app1.timer")
}
func (s *infoSuite) TestLayoutParsing(c *C) {
info, err := snap.InfoFromSnapYaml([]byte(`name: layout-demo
layout:
/usr:
bind: $SNAP/usr
/mytmp:
type: tmpfs
mode: 1777
/mylink:
symlink: /link/target
`))
c.Assert(err, IsNil)
layout := info.Layout
c.Assert(layout, NotNil)
c.Check(layout["/usr"], DeepEquals, &snap.Layout{
Snap: info,
Path: "/usr",
User: "root",
Group: "root",
Mode: 0755,
Bind: "$SNAP/usr",
})
c.Check(layout["/mytmp"], DeepEquals, &snap.Layout{
Snap: info,
Path: "/mytmp",
Type: "tmpfs",
User: "root",
Group: "root",
Mode: 01777,
})
c.Check(layout["/mylink"], DeepEquals, &snap.Layout{
Snap: info,
Path: "/mylink",
User: "root",
Group: "root",
Mode: 0755,
Symlink: "/link/target",
})
}
func (s *infoSuite) TestPlugInfoString(c *C) {
plug := &snap.PlugInfo{Snap: &snap.Info{SuggestedName: "snap"}, Name: "plug"}
c.Assert(plug.String(), Equals, "snap:plug")
}
func (s *infoSuite) TestSlotInfoString(c *C) {
slot := &snap.SlotInfo{Snap: &snap.Info{SuggestedName: "snap"}, Name: "slot"}
c.Assert(slot.String(), Equals, "snap:slot")
}
func (s *infoSuite) TestPlugInfoAttr(c *C) {
var val string
var intVal int
plug := &snap.PlugInfo{Snap: &snap.Info{SuggestedName: "snap"}, Name: "plug", Interface: "interface", Attrs: map[string]interface{}{"key": "value", "number": int(123)}}
c.Assert(plug.Attr("key", &val), IsNil)
c.Check(val, Equals, "value")
c.Assert(plug.Attr("number", &intVal), IsNil)
c.Check(intVal, Equals, 123)
c.Check(plug.Attr("key", &intVal), ErrorMatches, `snap "snap" has interface "interface" with invalid value type for "key" attribute`)
c.Check(plug.Attr("unknown", &val), ErrorMatches, `snap "snap" does not have attribute "unknown" for interface "interface"`)
c.Check(plug.Attr("key", intVal), ErrorMatches, `internal error: cannot get "key" attribute of interface "interface" with non-pointer value`)
}
func (s *infoSuite) TestSlotInfoAttr(c *C) {
var val string
var intVal int
slot := &snap.SlotInfo{Snap: &snap.Info{SuggestedName: "snap"}, Name: "plug", Interface: "interface", Attrs: map[string]interface{}{"key": "value", "number": int(123)}}
c.Assert(slot.Attr("key", &val), IsNil)
c.Check(val, Equals, "value")
c.Assert(slot.Attr("number", &intVal), IsNil)
c.Check(intVal, Equals, 123)
c.Check(slot.Attr("key", &intVal), ErrorMatches, `snap "snap" has interface "interface" with invalid value type for "key" attribute`)
c.Check(slot.Attr("unknown", &val), ErrorMatches, `snap "snap" does not have attribute "unknown" for interface "interface"`)
c.Check(slot.Attr("key", intVal), ErrorMatches, `internal error: cannot get "key" attribute of interface "interface" with non-pointer value`)
}
func (s *infoSuite) TestDottedPathSlot(c *C) {
attrs := map[string]interface{}{
"nested": map[string]interface{}{
"foo": "bar",
},
}
slot := &snap.SlotInfo{Attrs: attrs}
c.Assert(slot, NotNil)
v, ok := slot.Lookup("nested.foo")
c.Assert(ok, Equals, true)
c.Assert(v, Equals, "bar")
v, ok = slot.Lookup("nested")
c.Assert(ok, Equals, true)
c.Assert(v, DeepEquals, map[string]interface{}{
"foo": "bar",
})
_, ok = slot.Lookup("x")
c.Assert(ok, Equals, false)
_, ok = slot.Lookup("..")
c.Assert(ok, Equals, false)
_, ok = slot.Lookup("nested.foo.x")
c.Assert(ok, Equals, false)
_, ok = slot.Lookup("nested.x")
c.Assert(ok, Equals, false)
}
func (s *infoSuite) TestDottedPathPlug(c *C) {
attrs := map[string]interface{}{
"nested": map[string]interface{}{
"foo": "bar",
},
}
plug := &snap.PlugInfo{Attrs: attrs}
c.Assert(plug, NotNil)
v, ok := plug.Lookup("nested")
c.Assert(ok, Equals, true)
c.Assert(v, DeepEquals, map[string]interface{}{
"foo": "bar",
})
v, ok = plug.Lookup("nested.foo")
c.Assert(ok, Equals, true)
c.Assert(v, Equals, "bar")
_, ok = plug.Lookup("x")
c.Assert(ok, Equals, false)
_, ok = plug.Lookup("..")
c.Assert(ok, Equals, false)
_, ok = plug.Lookup("nested.foo.x")
c.Assert(ok, Equals, false)
}
func (s *infoSuite) TestExpandSnapVariables(c *C) {
dirs.SetRootDir("")
info, err := snap.InfoFromSnapYaml([]byte(`name: foo`))
c.Assert(err, IsNil)
info.Revision = snap.R(42)
c.Assert(info.ExpandSnapVariables("$SNAP/stuff"), Equals, "/snap/foo/42/stuff")
c.Assert(info.ExpandSnapVariables("$SNAP_DATA/stuff"), Equals, "/var/snap/foo/42/stuff")
c.Assert(info.ExpandSnapVariables("$SNAP_COMMON/stuff"), Equals, "/var/snap/foo/common/stuff")
c.Assert(info.ExpandSnapVariables("$GARBAGE/rocks"), Equals, "/rocks")
info.InstanceKey = "instance"
// Despite setting the instance key the variables expand to the same
// value as before. This is because they are used from inside the mount
// namespace of the instantiated snap where the mount backend will
// ensure that the regular (non-instance) paths contain
// instance-specific code and data.
c.Assert(info.ExpandSnapVariables("$SNAP/stuff"), Equals, "/snap/foo/42/stuff")
c.Assert(info.ExpandSnapVariables("$SNAP_DATA/stuff"), Equals, "/var/snap/foo/42/stuff")
c.Assert(info.ExpandSnapVariables("$SNAP_COMMON/stuff"), Equals, "/var/snap/foo/common/stuff")
c.Assert(info.ExpandSnapVariables("$GARBAGE/rocks"), Equals, "/rocks")
}
func (s *infoSuite) TestStopModeTypeKillMode(c *C) {
for _, t := range []struct {
stopMode string
killall bool
}{
{"", true},
{"sigterm", false},
{"sigterm-all", true},
{"sighup", false},
{"sighup-all", true},
{"sigusr1", false},
{"sigusr1-all", true},
{"sigusr2", false},
{"sigusr2-all", true},
} {
c.Check(snap.StopModeType(t.stopMode).KillAll(), Equals, t.killall, Commentf("wrong KillAll for %v", t.stopMode))
}
}
func (s *infoSuite) TestStopModeTypeKillSignal(c *C) {
for _, t := range []struct {
stopMode string
killSig string
}{
{"", ""},
{"sigterm", "SIGTERM"},
{"sigterm-all", "SIGTERM"},
{"sighup", "SIGHUP"},
{"sighup-all", "SIGHUP"},
{"sigusr1", "SIGUSR1"},
{"sigusr1-all", "SIGUSR1"},
{"sigusr2", "SIGUSR2"},
{"sigusr2-all", "SIGUSR2"},
} {
c.Check(snap.StopModeType(t.stopMode).KillSignal(), Equals, t.killSig)
}
}
func (s *infoSuite) TestSplitInstanceName(c *C) {
snapName, instanceKey := snap.SplitInstanceName("foo_bar")
c.Check(snapName, Equals, "foo")
c.Check(instanceKey, Equals, "bar")
snapName, instanceKey = snap.SplitInstanceName("foo")
c.Check(snapName, Equals, "foo")
c.Check(instanceKey, Equals, "")
// all following instance names are invalid
snapName, instanceKey = snap.SplitInstanceName("_bar")
c.Check(snapName, Equals, "")
c.Check(instanceKey, Equals, "bar")
snapName, instanceKey = snap.SplitInstanceName("foo___bar_bar")
c.Check(snapName, Equals, "foo")
c.Check(instanceKey, Equals, "__bar_bar")
snapName, instanceKey = snap.SplitInstanceName("")
c.Check(snapName, Equals, "")
c.Check(instanceKey, Equals, "")
}
func (s *infoSuite) TestInstanceSnapName(c *C) {
c.Check(snap.InstanceSnap("foo_bar"), Equals, "foo")
c.Check(snap.InstanceSnap("foo"), Equals, "foo")
c.Check(snap.InstanceName("foo", "bar"), Equals, "foo_bar")
c.Check(snap.InstanceName("foo", ""), Equals, "foo")
}
func (s *infoSuite) TestInstanceNameInSnapInfo(c *C) {
info := &snap.Info{
SuggestedName: "snap-name",
InstanceKey: "foo",
}
c.Check(info.InstanceName(), Equals, "snap-name_foo")
c.Check(info.SnapName(), Equals, "snap-name")
info.InstanceKey = ""
c.Check(info.InstanceName(), Equals, "snap-name")
c.Check(info.SnapName(), Equals, "snap-name")
}
func (s *infoSuite) TestIsActive(c *C) {
info1 := snaptest.MockSnap(c, sampleYaml, &snap.SideInfo{Revision: snap.R(1)})
info2 := snaptest.MockSnap(c, sampleYaml, &snap.SideInfo{Revision: snap.R(2)})
// no current -> not active
c.Check(info1.IsActive(), Equals, false)
c.Check(info2.IsActive(), Equals, false)
mountdir := info1.MountDir()
dir, rev := filepath.Split(mountdir)
c.Assert(os.MkdirAll(dir, 0755), IsNil)
cur := filepath.Join(dir, "current")
c.Assert(os.Symlink(rev, cur), IsNil)
// is current -> is active
c.Check(info1.IsActive(), Equals, true)
c.Check(info2.IsActive(), Equals, false)
}
func (s *infoSuite) TestDirAndFileHelpers(c *C) {
dirs.SetRootDir("")
c.Check(snap.MountDir("name", snap.R(1)), Equals, fmt.Sprintf("%s/name/1", dirs.SnapMountDir))
c.Check(snap.MountFile("name", snap.R(1)), Equals, "/var/lib/snapd/snaps/name_1.snap")
c.Check(snap.HooksDir("name", snap.R(1)), Equals, fmt.Sprintf("%s/name/1/meta/hooks", dirs.SnapMountDir))
c.Check(snap.DataDir("name", snap.R(1)), Equals, "/var/snap/name/1")
c.Check(snap.CommonDataDir("name"), Equals, "/var/snap/name/common")
c.Check(snap.UserDataDir("/home/bob", "name", snap.R(1)), Equals, "/home/bob/snap/name/1")
c.Check(snap.UserCommonDataDir("/home/bob", "name"), Equals, "/home/bob/snap/name/common")
c.Check(snap.UserXdgRuntimeDir(12345, "name"), Equals, "/run/user/12345/snap.name")
c.Check(snap.UserSnapDir("/home/bob", "name"), Equals, "/home/bob/snap/name")
c.Check(snap.MountDir("name_instance", snap.R(1)), Equals, fmt.Sprintf("%s/name_instance/1", dirs.SnapMountDir))
c.Check(snap.MountFile("name_instance", snap.R(1)), Equals, "/var/lib/snapd/snaps/name_instance_1.snap")
c.Check(snap.HooksDir("name_instance", snap.R(1)), Equals, fmt.Sprintf("%s/name_instance/1/meta/hooks", dirs.SnapMountDir))
c.Check(snap.DataDir("name_instance", snap.R(1)), Equals, "/var/snap/name_instance/1")
c.Check(snap.CommonDataDir("name_instance"), Equals, "/var/snap/name_instance/common")
c.Check(snap.UserDataDir("/home/bob", "name_instance", snap.R(1)), Equals, "/home/bob/snap/name_instance/1")
c.Check(snap.UserCommonDataDir("/home/bob", "name_instance"), Equals, "/home/bob/snap/name_instance/common")
c.Check(snap.UserXdgRuntimeDir(12345, "name_instance"), Equals, "/run/user/12345/snap.name_instance")
c.Check(snap.UserSnapDir("/home/bob", "name_instance"), Equals, "/home/bob/snap/name_instance")
}
func (s *infoSuite) TestSortByType(c *C) {
infos := []*snap.Info{
{SuggestedName: "app1", Type: "app"},
{SuggestedName: "os1", Type: "os"},
{SuggestedName: "base1", Type: "base"},
{SuggestedName: "gadget1", Type: "gadget"},
{SuggestedName: "kernel1", Type: "kernel"},
{SuggestedName: "app2", Type: "app"},
{SuggestedName: "os2", Type: "os"},
{SuggestedName: "snapd", Type: "snapd"},
{SuggestedName: "base2", Type: "base"},
{SuggestedName: "gadget2", Type: "gadget"},
{SuggestedName: "kernel2", Type: "kernel"},
}
sort.Stable(snap.ByType(infos))
c.Check(infos, DeepEquals, []*snap.Info{
{SuggestedName: "snapd", Type: "snapd"},
{SuggestedName: "os1", Type: "os"},
{SuggestedName: "os2", Type: "os"},
{SuggestedName: "kernel1", Type: "kernel"},
{SuggestedName: "kernel2", Type: "kernel"},
{SuggestedName: "base1", Type: "base"},
{SuggestedName: "base2", Type: "base"},
{SuggestedName: "gadget1", Type: "gadget"},
{SuggestedName: "gadget2", Type: "gadget"},
{SuggestedName: "app1", Type: "app"},
{SuggestedName: "app2", Type: "app"},
})
}
func (s *infoSuite) TestSortByTypeAgain(c *C) {
core := &snap.Info{Type: snap.TypeOS}
base := &snap.Info{Type: snap.TypeBase}
app := &snap.Info{Type: snap.TypeApp}
snapd := &snap.Info{}
snapd.SideInfo = snap.SideInfo{RealName: "snapd"}
byType := func(snaps ...*snap.Info) []*snap.Info {
sort.Stable(snap.ByType(snaps))
return snaps
}
c.Check(byType(base, core), DeepEquals, []*snap.Info{core, base})
c.Check(byType(app, core), DeepEquals, []*snap.Info{core, app})
c.Check(byType(app, base), DeepEquals, []*snap.Info{base, app})
c.Check(byType(app, base, core), DeepEquals, []*snap.Info{core, base, app})
c.Check(byType(app, core, base), DeepEquals, []*snap.Info{core, base, app})
c.Check(byType(app, core, base, snapd), DeepEquals, []*snap.Info{snapd, core, base, app})
c.Check(byType(app, snapd, core, base), DeepEquals, []*snap.Info{snapd, core, base, app})
}
func (s *infoSuite) TestMedia(c *C) {
c.Check(snap.MediaInfos{}.Screenshots(), HasLen, 0)
c.Check(snap.MediaInfos{}.IconURL(), Equals, "")
media := snap.MediaInfos{
{
Type: "screenshot",
URL: "https://example.com/shot1.svg",
}, {
Type: "icon",
URL: "https://example.com/icon.png",
}, {
Type: "screenshot",
URL: "https://example.com/shot2.svg",
Width: 42,
Height: 17,
},
}
c.Check(media.IconURL(), Equals, "https://example.com/icon.png")
c.Check(media.Screenshots(), DeepEquals, []snap.ScreenshotInfo{
{
URL: "https://example.com/shot1.svg",
Note: snap.ScreenshotsDeprecationNotice,
}, {
URL: "https://example.com/shot2.svg",
Width: 42,
Height: 17,
Note: snap.ScreenshotsDeprecationNotice,
},
})
}
func (s *infoSuite) TestSortApps(c *C) {
tcs := []struct {
err string
apps []*snap.AppInfo
sorted []string
}{{
apps: []*snap.AppInfo{
{Name: "bar", Before: []string{"baz"}},
{Name: "baz", After: []string{"bar", "foo"}},
{Name: "foo"},
},
sorted: []string{"bar", "foo", "baz"},
}, {
apps: []*snap.AppInfo{
{Name: "foo", After: []string{"bar", "zed"}},
{Name: "bar", Before: []string{"foo"}},
{Name: "baz", After: []string{"foo"}},
{Name: "zed"},
},
sorted: []string{"bar", "zed", "foo", "baz"},
}, {
apps: []*snap.AppInfo{
{Name: "foo", After: []string{"baz"}},
{Name: "bar", Before: []string{"baz"}},
{Name: "baz"},
{Name: "zed", After: []string{"foo", "bar", "baz"}},
},
sorted: []string{"bar", "baz", "foo", "zed"},
}, {
apps: []*snap.AppInfo{
{Name: "foo", Before: []string{"bar"}, After: []string{"zed"}},
{Name: "bar", Before: []string{"baz"}},
{Name: "baz", Before: []string{"zed"}},
{Name: "zed"},
},
err: `applications are part of a before/after cycle: ((foo|bar|baz|zed)(, )?){4}`,
}, {
apps: []*snap.AppInfo{
{Name: "foo", Before: []string{"bar"}},
{Name: "bar", Before: []string{"foo"}},
{Name: "baz", Before: []string{"foo"}, After: []string{"bar"}},
},
err: `applications are part of a before/after cycle: ((foo|bar|baz)(, )?){3}`,
}, {
apps: []*snap.AppInfo{
{Name: "baz", After: []string{"bar"}},
{Name: "foo"},
{Name: "bar", After: []string{"foo"}},
},
sorted: []string{"foo", "bar", "baz"},
}}
for _, tc := range tcs {
sorted, err := snap.SortServices(tc.apps)
if tc.err != "" {
c.Assert(err, ErrorMatches, tc.err)
} else {
c.Assert(err, IsNil)
c.Assert(sorted, HasLen, len(tc.sorted))
sortedNames := make([]string, len(sorted))
for i, app := range sorted {
sortedNames[i] = app.Name
}
c.Assert(sortedNames, DeepEquals, tc.sorted)
}
}
}
snapd-2.37.4~14.04.1/snap/errors.go 0000664 0000000 0000000 00000002400 13435556260 013341 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import "fmt"
type AlreadyInstalledError struct {
Snap string
}
func (e AlreadyInstalledError) Error() string {
return fmt.Sprintf("snap %q is already installed", e.Snap)
}
type NotInstalledError struct {
Snap string
Rev Revision
}
func (e NotInstalledError) Error() string {
if e.Rev.Unset() {
return fmt.Sprintf("snap %q is not installed", e.Snap)
}
return fmt.Sprintf("revision %s of snap %q is not installed", e.Rev, e.Snap)
}
type NotSnapError struct {
Path string
}
func (e NotSnapError) Error() string {
return fmt.Sprintf("%q is not a snap or snapdir", e.Path)
}
snapd-2.37.4~14.04.1/snap/export_test.go 0000664 0000000 0000000 00000001761 13435556260 014416 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
var (
ValidateSocketName = validateSocketName
ValidateDescription = validateDescription
ValidateTitle = validateTitle
InfoFromSnapYamlWithSideInfo = infoFromSnapYamlWithSideInfo
)
func (info *Info) ForceRenamePlug(oldName, newName string) {
info.forceRenamePlug(oldName, newName)
}
snapd-2.37.4~14.04.1/snap/restartcond_test.go 0000664 0000000 0000000 00000002471 13435556260 015424 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap_test
import (
. "gopkg.in/check.v1"
"gopkg.in/yaml.v2"
"github.com/snapcore/snapd/snap"
)
type restartcondSuite struct{}
var _ = Suite(&restartcondSuite{})
func (*restartcondSuite) TestRestartCondUnmarshal(c *C) {
for name, cond := range snap.RestartMap {
bs := []byte(name)
var rc snap.RestartCondition
c.Check(yaml.Unmarshal(bs, &rc), IsNil)
c.Check(rc, Equals, cond, Commentf(name))
}
}
func (restartcondSuite) TestRestartCondString(c *C) {
for name, cond := range snap.RestartMap {
if name == "never" {
c.Check(cond.String(), Equals, "no")
} else {
c.Check(cond.String(), Equals, name, Commentf(name))
}
}
}
snapd-2.37.4~14.04.1/snap/broken.go 0000664 0000000 0000000 00000006360 13435556260 013316 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"fmt"
"path/filepath"
"strings"
"github.com/snapcore/snapd/dirs"
)
// GuessAppsForBroken guesses what apps and services a broken snap has
// on the system by searching for matches based on the snap name in
// the snap binaries and service file directories. It returns a
// mapping from app names to partial AppInfo.
func GuessAppsForBroken(info *Info) map[string]*AppInfo {
out := make(map[string]*AppInfo)
// guess binaries first
name := info.InstanceName()
for _, p := range []string{name, fmt.Sprintf("%s.*", name)} {
matches, _ := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, p))
for _, m := range matches {
l := strings.SplitN(filepath.Base(m), ".", 2)
var appname string
if len(l) == 1 {
// when app is named the same as snap, it will
// be available under '' name, if the snap
// was installed with instance key, the app will
// be named `_'
appname = InstanceSnap(l[0])
} else {
appname = l[1]
}
out[appname] = &AppInfo{
Snap: info,
Name: appname,
}
}
}
// guess the services next
matches, _ := filepath.Glob(filepath.Join(dirs.SnapServicesDir, fmt.Sprintf("snap.%s.*.service", name)))
for _, m := range matches {
appname := strings.Split(m, ".")[2]
out[appname] = &AppInfo{
Snap: info,
Name: appname,
Daemon: "simple",
}
}
return out
}
// renameClashingCorePlugs renames plugs that clash with slot names on core snap.
//
// Some released core snaps had explicitly defined plugs "network-bind" and
// "core-support" that clashed with implicit slots with the same names but this
// was not validated before. To avoid a flag day and any potential issues,
// transparently rename the two clashing plugs by appending the "-plug" suffix.
func (info *Info) renameClashingCorePlugs() {
if info.InstanceName() == "core" && info.Type == TypeOS {
for _, plugName := range []string{"network-bind", "core-support"} {
info.forceRenamePlug(plugName, plugName+"-plug")
}
}
}
// forceRenamePlug renames the plug from oldName to newName, if present.
func (info *Info) forceRenamePlug(oldName, newName string) {
if plugInfo, ok := info.Plugs[oldName]; ok {
delete(info.Plugs, oldName)
info.Plugs[newName] = plugInfo
plugInfo.Name = newName
for _, appInfo := range info.Apps {
if _, ok := appInfo.Plugs[oldName]; ok {
delete(appInfo.Plugs, oldName)
appInfo.Plugs[newName] = plugInfo
}
}
for _, hookInfo := range info.Hooks {
if _, ok := hookInfo.Plugs[oldName]; ok {
delete(hookInfo.Plugs, oldName)
hookInfo.Plugs[newName] = plugInfo
}
}
}
}
snapd-2.37.4~14.04.1/snap/gadget.go 0000664 0000000 0000000 00000014045 13435556260 013270 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v2"
)
type GadgetInfo struct {
Volumes map[string]GadgetVolume `yaml:"volumes,omitempty"`
// Default configuration for snaps (snap-id => key => value).
Defaults map[string]map[string]interface{} `yaml:"defaults,omitempty"`
Connections []GadgetConnection `yaml:"connections"`
}
type GadgetVolume struct {
Schema string `yaml:"schema"`
Bootloader string `yaml:"bootloader"`
ID string `yaml:"id"`
Structure []VolumeStructure `yaml:"structure"`
}
// TODO Offsets and sizes are strings to support unit suffixes.
// Is that a good idea? *2^N or *10^N? We'll probably want a richer
// type when we actually handle these.
type VolumeStructure struct {
Label string `yaml:"filesystem-label"`
Offset string `yaml:"offset"`
OffsetWrite string `yaml:"offset-write"`
Size string `yaml:"size"`
Type string `yaml:"type"`
ID string `yaml:"id"`
Filesystem string `yaml:"filesystem"`
Content []VolumeContent `yaml:"content"`
}
type VolumeContent struct {
Source string `yaml:"source"`
Target string `yaml:"target"`
Image string `yaml:"image"`
Offset string `yaml:"offset"`
OffsetWrite string `yaml:"offset-write"`
Size string `yaml:"size"`
Unpack bool `yaml:"unpack"`
}
// GadgetConnect describes an interface connection requested by the gadget
// between seeded snaps. The syntax is of a mapping like:
//
// plug: (|system):plug
// [slot: (|system):slot]
//
// "system" indicates a system plug or slot.
// Fully omitting the slot part indicates a system slot with the same name
// as the plug.
type GadgetConnection struct {
Plug GadgetConnectionPlug `yaml:"plug"`
Slot GadgetConnectionSlot `yaml:"slot"`
}
type GadgetConnectionPlug struct {
SnapID string
Plug string
}
func (gcplug *GadgetConnectionPlug) Empty() bool {
return gcplug.SnapID == "" && gcplug.Plug == ""
}
func (gcplug *GadgetConnectionPlug) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err
}
snapID, name, err := parseSnapIDColonName(s)
if err != nil {
return fmt.Errorf("in gadget connection plug: %v", err)
}
gcplug.SnapID = snapID
gcplug.Plug = name
return nil
}
type GadgetConnectionSlot struct {
SnapID string
Slot string
}
func (gcslot *GadgetConnectionSlot) Empty() bool {
return gcslot.SnapID == "" && gcslot.Slot == ""
}
func (gcslot *GadgetConnectionSlot) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err
}
snapID, name, err := parseSnapIDColonName(s)
if err != nil {
return fmt.Errorf("in gadget connection slot: %v", err)
}
gcslot.SnapID = snapID
gcslot.Slot = name
return nil
}
func parseSnapIDColonName(s string) (snapID, name string, err error) {
parts := strings.Split(s, ":")
if len(parts) == 2 {
snapID = parts[0]
name = parts[1]
}
if snapID == "" || name == "" {
return "", "", fmt.Errorf(`expected "(|system):name" not %q`, s)
}
return snapID, name, nil
}
func systemOrSnapID(s string) bool {
if s != "system" && len(s) != validSnapIDLength {
return false
}
return true
}
// ReadGadgetInfo reads the gadget specific metadata from gadget.yaml
// in the snap. classic set to true means classic rules apply,
// i.e. content/presence of gadget.yaml is fully optional.
func ReadGadgetInfo(info *Info, classic bool) (*GadgetInfo, error) {
const errorFormat = "cannot read gadget snap details: %s"
if info.Type != TypeGadget {
return nil, fmt.Errorf(errorFormat, "not a gadget snap")
}
var gi GadgetInfo
gadgetYamlFn := filepath.Join(info.MountDir(), "meta", "gadget.yaml")
gmeta, err := ioutil.ReadFile(gadgetYamlFn)
if classic && os.IsNotExist(err) {
// gadget.yaml is optional for classic gadgets
return &gi, nil
}
if err != nil {
return nil, fmt.Errorf(errorFormat, err)
}
if err := yaml.Unmarshal(gmeta, &gi); err != nil {
return nil, fmt.Errorf(errorFormat, err)
}
for k, v := range gi.Defaults {
if !systemOrSnapID(k) {
return nil, fmt.Errorf(`default stanza not keyed by "system" or snap-id: %s`, k)
}
dflt, err := normalizeYamlValue(v)
if err != nil {
return nil, fmt.Errorf("default value %q of %q: %v", v, k, err)
}
gi.Defaults[k] = dflt.(map[string]interface{})
}
for i, gconn := range gi.Connections {
if gconn.Plug.Empty() {
return nil, fmt.Errorf("gadget connection plug cannot be empty")
}
if gconn.Slot.Empty() {
gi.Connections[i].Slot.SnapID = "system"
gi.Connections[i].Slot.Slot = gconn.Plug.Plug
}
}
if classic && len(gi.Volumes) == 0 {
// volumes can be left out on classic
// can still specify defaults though
return &gi, nil
}
// basic validation
var bootloadersFound int
for _, v := range gi.Volumes {
switch v.Bootloader {
case "":
// pass
case "grub", "u-boot", "android-boot":
bootloadersFound += 1
default:
return nil, fmt.Errorf(errorFormat, "bootloader must be one of grub, u-boot or android-boot")
}
}
switch {
case bootloadersFound == 0:
return nil, fmt.Errorf(errorFormat, "bootloader not declared in any volume")
case bootloadersFound > 1:
return nil, fmt.Errorf(errorFormat, fmt.Sprintf("too many (%d) bootloaders declared", bootloadersFound))
}
return &gi, nil
}
snapd-2.37.4~14.04.1/snap/hooktypes.go 0000664 0000000 0000000 00000004532 13435556260 014062 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package snap
import (
"regexp"
)
var supportedHooks = []*HookType{
NewHookType(regexp.MustCompile("^prepare-device$")),
NewHookType(regexp.MustCompile("^configure$")),
NewHookType(regexp.MustCompile("^install$")),
NewHookType(regexp.MustCompile("^pre-refresh$")),
NewHookType(regexp.MustCompile("^post-refresh$")),
NewHookType(regexp.MustCompile("^remove$")),
NewHookType(regexp.MustCompile("^prepare-(?:plug|slot)-[-a-z0-9]+$")),
NewHookType(regexp.MustCompile("^unprepare-(?:plug|slot)-[-a-z0-9]+$")),
NewHookType(regexp.MustCompile("^connect-(?:plug|slot)-[-a-z0-9]+$")),
NewHookType(regexp.MustCompile("^disconnect-(?:plug|slot)-[-a-z0-9]+$")),
}
// HookType represents a pattern of supported hook names.
type HookType struct {
pattern *regexp.Regexp
}
// NewHookType returns a new HookType with the given pattern.
func NewHookType(pattern *regexp.Regexp) *HookType {
return &HookType{
pattern: pattern,
}
}
// Match returns true if the given hook name matches this hook type.
func (hookType HookType) Match(hookName string) bool {
return hookType.pattern.MatchString(hookName)
}
// IsHookSupported returns true if the given hook name matches one of the
// supported hooks.
func IsHookSupported(hookName string) bool {
for _, hookType := range supportedHooks {
if hookType.Match(hookName) {
return true
}
}
return false
}
func MockSupportedHookTypes(hookTypes []*HookType) (restore func()) {
old := supportedHooks
supportedHooks = hookTypes
return func() { supportedHooks = old }
}
func MockAppendSupportedHookTypes(hookTypes []*HookType) (restore func()) {
old := supportedHooks
supportedHooks = append(supportedHooks, hookTypes...)
return func() { supportedHooks = old }
}
snapd-2.37.4~14.04.1/cmd/ 0000775 0000000 0000000 00000000000 13435556260 011304 5 ustar snapd-2.37.4~14.04.1/cmd/snap-update-ns/ 0000775 0000000 0000000 00000000000 13435556260 014143 5 ustar snapd-2.37.4~14.04.1/cmd/snap-update-ns/freezer.go 0000664 0000000 0000000 00000005064 13435556260 016141 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
)
var freezerCgroupDir = "/sys/fs/cgroup/freezer"
// freezeSnapProcesses freezes all the processes originating from the given snap.
// Processes are frozen regardless of which particular snap application they
// originate from.
func freezeSnapProcesses(snapName string) error {
fname := filepath.Join(freezerCgroupDir, fmt.Sprintf("snap.%s", snapName), "freezer.state")
if err := ioutil.WriteFile(fname, []byte("FROZEN"), 0644); err != nil && os.IsNotExist(err) {
// When there's no freezer cgroup we don't have to freeze anything.
// This can happen when no process belonging to a given snap has been
// started yet.
return nil
} else if err != nil {
return fmt.Errorf("cannot freeze processes of snap %q, %v", snapName, err)
}
for i := 0; i < 30; i++ {
data, err := ioutil.ReadFile(fname)
if err != nil {
return fmt.Errorf("cannot determine the freeze state of processes of snap %q, %v", snapName, err)
}
// If the cgroup is still freezing then wait a moment and try again.
if bytes.Equal(data, []byte("FREEZING")) {
time.Sleep(100 * time.Millisecond)
continue
}
return nil
}
// If we got here then we timed out after seeing FREEZING for too long.
thawSnapProcesses(snapName) // ignore the error, this is best-effort.
return fmt.Errorf("cannot finish freezing processes of snap %q", snapName)
}
func thawSnapProcesses(snapName string) error {
fname := filepath.Join(freezerCgroupDir, fmt.Sprintf("snap.%s", snapName), "freezer.state")
if err := ioutil.WriteFile(fname, []byte("THAWED"), 0644); err != nil && os.IsNotExist(err) {
// When there's no freezer cgroup we don't have to thaw anything.
// This can happen when no process belonging to a given snap has been
// started yet.
return nil
} else if err != nil {
return fmt.Errorf("cannot thaw processes of snap %q", snapName)
}
return nil
}
snapd-2.37.4~14.04.1/cmd/snap-update-ns/utils_test.go 0000664 0000000 0000000 00000144733 13435556260 016705 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package main_test
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"syscall"
. "gopkg.in/check.v1"
update "github.com/snapcore/snapd/cmd/snap-update-ns"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/osutil/sys"
"github.com/snapcore/snapd/testutil"
)
type utilsSuite struct {
testutil.BaseTest
sys *testutil.SyscallRecorder
log *bytes.Buffer
as *update.Assumptions
}
var _ = Suite(&utilsSuite{})
func (s *utilsSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
s.sys = &testutil.SyscallRecorder{}
s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys))
buf, restore := logger.MockLogger()
s.BaseTest.AddCleanup(restore)
s.log = buf
s.as = &update.Assumptions{}
}
func (s *utilsSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
s.sys.CheckForStrayDescriptors(c)
}
// secure-mkdir-all
// Ensure that we reject unclean paths.
func (s *utilsSuite) TestSecureMkdirAllUnclean(c *C) {
err := update.MkdirAll("/unclean//path", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot split unclean path .*`)
c.Assert(s.sys.RCalls(), HasLen, 0)
}
// Ensure that we refuse to create a directory with an relative path.
func (s *utilsSuite) TestSecureMkdirAllRelative(c *C) {
err := update.MkdirAll("rel/path", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot create directory with relative path: "rel/path"`)
c.Assert(s.sys.RCalls(), HasLen, 0)
}
// Ensure that we can "create the root directory.
func (s *utilsSuite) TestSecureMkdirAllLevel0(c *C) {
c.Assert(update.MkdirAll("/", 0755, 123, 456, nil), IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `close 3`},
})
}
// Ensure that we can create a directory in the top-level directory.
func (s *utilsSuite) TestSecureMkdirAllLevel1(c *C) {
c.Assert(update.MkdirAll("/path", 0755, 123, 456, nil), IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "path" 0755`},
{C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 123 456`},
{C: `close 4`},
{C: `close 3`},
})
}
// Ensure that we can create a directory two levels from the top-level directory.
func (s *utilsSuite) TestSecureMkdirAllLevel2(c *C) {
c.Assert(update.MkdirAll("/path/to", 0755, 123, 456, nil), IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "path" 0755`},
{C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 123 456`},
{C: `close 3`},
{C: `mkdirat 4 "to" 0755`},
{C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 123 456`},
{C: `close 3`},
{C: `close 4`},
})
}
// Ensure that we can create a directory three levels from the top-level directory.
func (s *utilsSuite) TestSecureMkdirAllLevel3(c *C) {
c.Assert(update.MkdirAll("/path/to/something", 0755, 123, 456, nil), IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "path" 0755`},
{C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 123 456`},
{C: `mkdirat 4 "to" 0755`},
{C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5},
{C: `fchown 5 123 456`},
{C: `close 4`},
{C: `close 3`},
{C: `mkdirat 5 "something" 0755`},
{C: `openat 5 "something" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 123 456`},
{C: `close 3`},
{C: `close 5`},
})
}
// Ensure that writes to /etc/demo are interrupted if /etc is restricted.
func (s *utilsSuite) TestSecureMkdirAllWithRestrictedEtc(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic})
s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST)
rs := s.as.RestrictionsFor("/etc/demo")
err := update.MkdirAll("/etc/demo", 0755, 123, 456, rs)
c.Assert(err, ErrorMatches, `cannot write to "/etc/demo" because it would affect the host in "/etc"`)
c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/etc")
c.Assert(err.(*update.TrespassingError).DesiredPath, Equals, "/etc/demo")
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
// we are inspecting the type of the filesystem we are about to perform operation on.
{C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}},
{C: `fstat 3 `, R: syscall.Stat_t{}},
{C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST},
{C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
// ext4 is writable, refuse further operations.
{C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 4`},
})
}
// Ensure that writes to /etc/demo allowed if /etc is unrestricted.
func (s *utilsSuite) TestSecureMkdirAllWithUnrestrictedEtc(c *C) {
defer s.as.MockUnrestrictedPaths("/etc")() // Mark /etc as unrestricted.
s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST)
rs := s.as.RestrictionsFor("/etc/demo")
c.Assert(update.MkdirAll("/etc/demo", 0755, 123, 456, rs), IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
// We are not interested in the type of filesystem at /
{C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST},
{C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
// We are not interested in the type of filesystem at /etc
{C: `close 3`},
{C: `mkdirat 4 "demo" 0755`},
{C: `openat 4 "demo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 123 456`},
{C: `close 3`},
{C: `close 4`},
})
}
// Ensure that we can detect read only filesystems.
func (s *utilsSuite) TestSecureMkdirAllROFS(c *C) {
s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) // just realistic
s.sys.InsertFault(`mkdirat 4 "path" 0755`, syscall.EROFS)
err := update.MkdirAll("/rofs/path", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`)
c.Assert(err.(*update.ReadOnlyFsError).Path, Equals, "/rofs")
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `mkdirat 4 "path" 0755`, E: syscall.EROFS},
{C: `close 4`},
})
}
// Ensure that we don't chown existing directories.
func (s *utilsSuite) TestSecureMkdirAllExistingDirsDontChown(c *C) {
s.sys.InsertFault(`mkdirat 3 "abs" 0755`, syscall.EEXIST)
s.sys.InsertFault(`mkdirat 4 "path" 0755`, syscall.EEXIST)
err := update.MkdirAll("/abs/path", 0755, 123, 456, nil)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "abs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `mkdirat 4 "path" 0755`, E: syscall.EEXIST},
{C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `close 3`},
{C: `close 4`},
})
}
// Ensure that we we close everything when mkdirat fails.
func (s *utilsSuite) TestSecureMkdirAllMkdiratError(c *C) {
s.sys.InsertFault(`mkdirat 3 "abs" 0755`, errTesting)
err := update.MkdirAll("/abs", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot create directory "/abs": testing`)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "abs" 0755`, E: errTesting},
{C: `close 3`},
})
}
// Ensure that we we close everything when fchown fails.
func (s *utilsSuite) TestSecureMkdirAllFchownError(c *C) {
s.sys.InsertFault(`fchown 4 123 456`, errTesting)
err := update.MkdirAll("/path", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot chown directory "/path" to 123.456: testing`)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "path" 0755`},
{C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 123 456`, E: errTesting},
{C: `close 4`},
{C: `close 3`},
})
}
// Check error path when we cannot open root directory.
func (s *utilsSuite) TestSecureMkdirAllOpenRootError(c *C) {
s.sys.InsertFault(`open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting)
err := update.MkdirAll("/abs/path", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, "cannot open root directory: testing")
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting},
})
}
// Check error path when we cannot open non-root directory.
func (s *utilsSuite) TestSecureMkdirAllOpenError(c *C) {
s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting)
err := update.MkdirAll("/abs/path", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot open directory "/abs": testing`)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "abs" 0755`},
{C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting},
{C: `close 3`},
})
}
func (s *utilsSuite) TestPlanWritableMimic(c *C) {
s.sys.InsertSysLstatResult(`lstat "/foo" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755})
restore := update.MockReadDir(func(dir string) ([]os.FileInfo, error) {
c.Assert(dir, Equals, "/foo")
return []os.FileInfo{
testutil.FakeFileInfo("file", 0),
testutil.FakeFileInfo("dir", os.ModeDir),
testutil.FakeFileInfo("symlink", os.ModeSymlink),
testutil.FakeFileInfo("error-symlink-readlink", os.ModeSymlink),
// NOTE: None of the filesystem entries below are supported because
// they cannot be placed inside snaps or can only be created at
// runtime in areas that are already writable and this would never
// have to be handled in a writable mimic.
testutil.FakeFileInfo("block-dev", os.ModeDevice),
testutil.FakeFileInfo("char-dev", os.ModeDevice|os.ModeCharDevice),
testutil.FakeFileInfo("socket", os.ModeSocket),
testutil.FakeFileInfo("pipe", os.ModeNamedPipe),
}, nil
})
defer restore()
restore = update.MockReadlink(func(name string) (string, error) {
switch name {
case "/foo/symlink":
return "target", nil
case "/foo/error-symlink-readlink":
return "", errTesting
}
panic("unexpected")
})
defer restore()
changes, err := update.PlanWritableMimic("/foo", "/foo/bar")
c.Assert(err, IsNil)
c.Assert(changes, DeepEquals, []*update.Change{
// Store /foo in /tmp/.snap/foo while we set things up
{Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount},
// Put a tmpfs over /foo
{Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar", "mode=0755", "uid=0", "gid=0"}}, Action: update.Mount},
// Bind mount files and directories over. Note that files are identified by x-snapd.kind=file option.
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
// Create symlinks.
// Bad symlinks and all other file types are skipped and not
// recorded in mount changes.
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
// Unmount the safe-keeping directory
{Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount},
})
}
func (s *utilsSuite) TestPlanWritableMimicErrors(c *C) {
s.sys.InsertSysLstatResult(`lstat "/foo" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755})
restore := update.MockReadDir(func(dir string) ([]os.FileInfo, error) {
c.Assert(dir, Equals, "/foo")
return nil, errTesting
})
defer restore()
restore = update.MockReadlink(func(name string) (string, error) {
return "", errTesting
})
defer restore()
changes, err := update.PlanWritableMimic("/foo", "/foo/bar")
c.Assert(err, ErrorMatches, "testing")
c.Assert(changes, HasLen, 0)
}
func (s *utilsSuite) TestExecWirableMimicSuccess(c *C) {
// This plan is the same as in the test above. This is what comes out of planWritableMimic.
plan := []*update.Change{
{Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount},
}
// Mock the act of performing changes, each of the change we perform is coming from the plan.
restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) {
c.Assert(plan, testutil.DeepContains, chg)
return nil, nil
})
defer restore()
// The executed plan leaves us with a simplified view of the plan that is suitable for undo.
undoPlan, err := update.ExecWritableMimic(plan, s.as)
c.Assert(err, IsNil)
c.Assert(undoPlan, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar", "x-snapd.detach"}}, Action: update.Mount},
})
}
func (s *utilsSuite) TestExecWirableMimicErrorWithRecovery(c *C) {
// This plan is the same as in the test above. This is what comes out of planWritableMimic.
plan := []*update.Change{
{Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
// NOTE: the next perform will fail. Notably the symlink did not fail.
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount},
}
// Mock the act of performing changes. Before we inject a failure we ensure
// that each of the change we perform is coming from the plan. For the
// purpose of the test the change that bind mounts the "dir" over itself
// will fail and will trigger an recovery path. The changes performed in
// the recovery path are recorded.
var recoveryPlan []*update.Change
recovery := false
restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) {
if !recovery {
c.Assert(plan, testutil.DeepContains, chg)
if chg.Entry.Name == "/tmp/.snap/foo/dir" {
recovery = true // switch to recovery mode
return nil, errTesting
}
} else {
recoveryPlan = append(recoveryPlan, chg)
}
return nil, nil
})
defer restore()
// The executed plan fails, leaving us with the error and an empty undo plan.
undoPlan, err := update.ExecWritableMimic(plan, s.as)
c.Assert(err, Equals, errTesting)
c.Assert(undoPlan, HasLen, 0)
// The changes we managed to perform were undone correctly.
c.Assert(recoveryPlan, DeepEquals, []*update.Change{
// NOTE: there is no symlink undo entry as it is implicitly undone by unmounting the tmpfs.
{Entry: osutil.MountEntry{Name: "/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Unmount},
{Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Unmount},
{Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind", "x-snapd.detach"}}, Action: update.Unmount},
})
}
func (s *utilsSuite) TestExecWirableMimicErrorNothingDone(c *C) {
// This plan is the same as in the test above. This is what comes out of planWritableMimic.
plan := []*update.Change{
{Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount},
}
// Mock the act of performing changes and just fail on any request.
restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) {
return nil, errTesting
})
defer restore()
// The executed plan fails, the recovery didn't fail (it's empty) so we just return that error.
undoPlan, err := update.ExecWritableMimic(plan, s.as)
c.Assert(err, Equals, errTesting)
c.Assert(undoPlan, HasLen, 0)
}
func (s *utilsSuite) TestExecWirableMimicErrorCannotUndo(c *C) {
// This plan is the same as in the test above. This is what comes out of planWritableMimic.
plan := []*update.Change{
{Entry: osutil.MountEntry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"rbind"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"rbind", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=target", "x-snapd.synthetic", "x-snapd.needed-by=/foo/bar"}}, Action: update.Mount},
{Entry: osutil.MountEntry{Name: "none", Dir: "/tmp/.snap/foo", Options: []string{"x-snapd.detach"}}, Action: update.Unmount},
}
// Mock the act of performing changes. After performing the first change
// correctly we will fail forever (this includes the recovery path) so the
// execute function ends up in a situation where it cannot perform the
// recovery path and will have to return a fatal error.
i := -1
restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) {
i++
if i > 0 {
return nil, fmt.Errorf("failure-%d", i)
}
return nil, nil
})
defer restore()
// The plan partially succeeded and we cannot undo those changes.
_, err := update.ExecWritableMimic(plan, s.as)
c.Assert(err, ErrorMatches, `cannot undo change ".*" while recovering from earlier error failure-1: failure-2`)
c.Assert(err, FitsTypeOf, &update.FatalError{})
}
// realSystemSuite is not isolated / mocked from the system.
type realSystemSuite struct {
as *update.Assumptions
}
var _ = Suite(&realSystemSuite{})
func (s *realSystemSuite) SetUpTest(c *C) {
s.as = &update.Assumptions{}
s.as.AddUnrestrictedPaths("/tmp")
}
// Check that we can actually create directories.
// This doesn't test the chown logic as that requires root.
func (s *realSystemSuite) TestSecureMkdirAllForReal(c *C) {
d := c.MkDir()
// Create d (which already exists) with mode 0777 (but c.MkDir() used 0700
// internally and since we are not creating the directory we should not be
// changing that.
c.Assert(update.MkdirAll(d, 0777, sys.FlagID, sys.FlagID, nil), IsNil)
fi, err := os.Stat(d)
c.Assert(err, IsNil)
c.Check(fi.IsDir(), Equals, true)
c.Check(fi.Mode().Perm(), Equals, os.FileMode(0700))
// Create d1, which is a simple subdirectory, with a distinct mode and
// check that it was applied. Note that default umask 022 is subtracted so
// effective directory has different permissions.
d1 := filepath.Join(d, "subdir")
c.Assert(update.MkdirAll(d1, 0707, sys.FlagID, sys.FlagID, nil), IsNil)
fi, err = os.Stat(d1)
c.Assert(err, IsNil)
c.Check(fi.IsDir(), Equals, true)
c.Check(fi.Mode().Perm(), Equals, os.FileMode(0705))
// Create d2, which is a deeper subdirectory, with another distinct mode
// and check that it was applied.
d2 := filepath.Join(d, "subdir/subdir/subdir")
c.Assert(update.MkdirAll(d2, 0750, sys.FlagID, sys.FlagID, nil), IsNil)
fi, err = os.Stat(d2)
c.Assert(err, IsNil)
c.Check(fi.IsDir(), Equals, true)
c.Check(fi.Mode().Perm(), Equals, os.FileMode(0750))
}
// secure-mkfile-all
// Ensure that we reject unclean paths.
func (s *utilsSuite) TestSecureMkfileAllUnclean(c *C) {
err := update.MkfileAll("/unclean//path", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot split unclean path .*`)
c.Assert(s.sys.RCalls(), HasLen, 0)
}
// Ensure that we refuse to create a file with an relative path.
func (s *utilsSuite) TestSecureMkfileAllRelative(c *C) {
err := update.MkfileAll("rel/path", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot create file with relative path: "rel/path"`)
c.Assert(s.sys.RCalls(), HasLen, 0)
}
// Ensure that we refuse creating the root directory as a file.
func (s *utilsSuite) TestSecureMkfileAllLevel0(c *C) {
err := update.MkfileAll("/", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot create non-file path: "/"`)
c.Assert(s.sys.RCalls(), HasLen, 0)
}
// Ensure that we can create a file in the top-level directory.
func (s *utilsSuite) TestSecureMkfileAllLevel1(c *C) {
c.Assert(update.MkfileAll("/path", 0755, 123, 456, nil), IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4},
{C: `fchown 4 123 456`},
{C: `close 4`},
{C: `close 3`},
})
}
// Ensure that we can create a file two levels from the top-level directory.
func (s *utilsSuite) TestSecureMkfileAllLevel2(c *C) {
c.Assert(update.MkfileAll("/path/to", 0755, 123, 456, nil), IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "path" 0755`},
{C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 123 456`},
{C: `close 3`},
{C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3},
{C: `fchown 3 123 456`},
{C: `close 3`},
{C: `close 4`},
})
}
// Ensure that we can create a file three levels from the top-level directory.
func (s *utilsSuite) TestSecureMkfileAllLevel3(c *C) {
c.Assert(update.MkfileAll("/path/to/something", 0755, 123, 456, nil), IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "path" 0755`},
{C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 123 456`},
{C: `mkdirat 4 "to" 0755`},
{C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5},
{C: `fchown 5 123 456`},
{C: `close 4`},
{C: `close 3`},
{C: `openat 5 "something" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3},
{C: `fchown 3 123 456`},
{C: `close 3`},
{C: `close 5`},
})
}
// Ensure that we can detect read only filesystems.
func (s *utilsSuite) TestSecureMkfileAllROFS(c *C) {
s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) // just realistic
s.sys.InsertFault(`openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EROFS)
err := update.MkfileAll("/rofs/path", 0755, 123, 456, nil)
c.Check(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`)
c.Assert(err.(*update.ReadOnlyFsError).Path, Equals, "/rofs")
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EROFS},
{C: `close 4`},
})
}
// Ensure that we don't chown existing files or directories.
func (s *utilsSuite) TestSecureMkfileAllExistingDirsDontChown(c *C) {
s.sys.InsertFault(`mkdirat 3 "abs" 0755`, syscall.EEXIST)
s.sys.InsertFault(`openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EEXIST)
err := update.MkfileAll("/abs/path", 0755, 123, 456, nil)
c.Check(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "abs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EEXIST},
{C: `openat 4 "path" O_NOFOLLOW|O_CLOEXEC 0`, R: 3},
{C: `close 3`},
{C: `close 4`},
})
}
// Ensure that we we close everything when openat fails.
func (s *utilsSuite) TestSecureMkfileAllOpenat2ndError(c *C) {
s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EEXIST)
s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC 0`, errTesting)
err := update.MkfileAll("/abs", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot open file "/abs": testing`)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EEXIST},
{C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC 0`, E: errTesting},
{C: `close 3`},
})
}
// Ensure that we we close everything when openat (non-exclusive) fails.
func (s *utilsSuite) TestSecureMkfileAllOpenatError(c *C) {
s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting)
err := update.MkfileAll("/abs", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot open file "/abs": testing`)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: errTesting},
{C: `close 3`},
})
}
// Ensure that we we close everything when fchown fails.
func (s *utilsSuite) TestSecureMkfileAllFchownError(c *C) {
s.sys.InsertFault(`fchown 4 123 456`, errTesting)
err := update.MkfileAll("/path", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot chown file "/path" to 123.456: testing`)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4},
{C: `fchown 4 123 456`, E: errTesting},
{C: `close 4`},
{C: `close 3`},
})
}
// Check error path when we cannot open root directory.
func (s *utilsSuite) TestSecureMkfileAllOpenRootError(c *C) {
s.sys.InsertFault(`open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting)
err := update.MkfileAll("/abs/path", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, "cannot open root directory: testing")
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting},
})
}
// Check error path when we cannot open non-root directory.
func (s *utilsSuite) TestSecureMkfileAllOpenError(c *C) {
s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting)
err := update.MkfileAll("/abs/path", 0755, 123, 456, nil)
c.Assert(err, ErrorMatches, `cannot open directory "/abs": testing`)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "abs" 0755`},
{C: `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, E: errTesting},
{C: `close 3`},
})
}
// We want to create a symlink in $SNAP_DATA and that's fine.
func (s *utilsSuite) TestSecureMksymlinkAllInSnapData(c *C) {
s.sys.InsertFault(`mkdirat 3 "var" 0755`, syscall.EEXIST)
s.sys.InsertFault(`mkdirat 4 "snap" 0755`, syscall.EEXIST)
s.sys.InsertFault(`mkdirat 5 "foo" 0755`, syscall.EEXIST)
s.sys.InsertFault(`mkdirat 6 "42" 0755`, syscall.EEXIST)
err := update.MksymlinkAll("/var/snap/foo/42/symlink", 0755, 0, 0, "/oldname", nil)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "var" 0755`, E: syscall.EEXIST},
{C: `openat 3 "var" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `mkdirat 4 "snap" 0755`, E: syscall.EEXIST},
{C: `openat 4 "snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5},
{C: `mkdirat 5 "foo" 0755`, E: syscall.EEXIST},
{C: `openat 5 "foo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 6},
{C: `mkdirat 6 "42" 0755`, E: syscall.EEXIST},
{C: `openat 6 "42" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 7},
{C: `close 6`},
{C: `close 5`},
{C: `close 4`},
{C: `close 3`},
{C: `symlinkat "/oldname" 7 "symlink"`},
{C: `close 7`},
})
}
// We want to create a symlink in /etc but the host filesystem would be affected.
func (s *utilsSuite) TestSecureMksymlinkAllInEtc(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic})
s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST)
rs := s.as.RestrictionsFor("/etc/symlink")
err := update.MksymlinkAll("/etc/symlink", 0755, 0, 0, "/oldname", rs)
c.Assert(err, ErrorMatches, `cannot write to "/etc/symlink" because it would affect the host in "/etc"`)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}},
{C: `fstat 3 `, R: syscall.Stat_t{}},
{C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST},
{C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 4`},
})
}
// We want to create a symlink deep in /etc but the host filesystem would be affected.
// This just shows that we pick the right place to construct the mimic
func (s *utilsSuite) TestSecureMksymlinkAllDeepInEtc(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic})
s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST)
rs := s.as.RestrictionsFor("/etc/some/other/stuff/symlink")
err := update.MksymlinkAll("/etc/some/other/stuff/symlink", 0755, 0, 0, "/oldname", rs)
c.Assert(err, ErrorMatches, `cannot write to "/etc/some/other/stuff/symlink" because it would affect the host in "/etc"`)
c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/etc")
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}},
{C: `fstat 3 `, R: syscall.Stat_t{}},
{C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST},
{C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 4`},
{C: `close 3`},
})
}
// We want to create a file in /etc but the host filesystem would be affected.
func (s *utilsSuite) TestSecureMkfileAllInEtc(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic})
s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST)
rs := s.as.RestrictionsFor("/etc/file")
err := update.MkfileAll("/etc/file", 0755, 0, 0, rs)
c.Assert(err, ErrorMatches, `cannot write to "/etc/file" because it would affect the host in "/etc"`)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}},
{C: `fstat 3 `, R: syscall.Stat_t{}},
{C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST},
{C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 4`},
})
}
// We want to create a directory in /etc but the host filesystem would be affected.
func (s *utilsSuite) TestSecureMkdirAllInEtc(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic})
s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST)
rs := s.as.RestrictionsFor("/etc/dir")
err := update.MkdirAll("/etc/dir", 0755, 0, 0, rs)
c.Assert(err, ErrorMatches, `cannot write to "/etc/dir" because it would affect the host in "/etc"`)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}},
{C: `fstat 3 `, R: syscall.Stat_t{}},
{C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST},
{C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 4`},
})
}
// We want to create a directory in /snap/foo/42/dir and want to know what happens.
func (s *utilsSuite) TestSecureMkdirAllInSNAP(c *C) {
// Allow creating directories under /snap/ related to this snap ("foo").
// This matches what is done inside main().
restore := s.as.MockUnrestrictedPaths("/snap/foo")
defer restore()
s.sys.InsertFault(`mkdirat 3 "snap" 0755`, syscall.EEXIST)
s.sys.InsertFault(`mkdirat 4 "foo" 0755`, syscall.EEXIST)
s.sys.InsertFault(`mkdirat 5 "42" 0755`, syscall.EEXIST)
rs := s.as.RestrictionsFor("/snap/foo/42/dir")
err := update.MkdirAll("/snap/foo/42/dir", 0755, 0, 0, rs)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "snap" 0755`, E: syscall.EEXIST},
{C: `openat 3 "snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `mkdirat 4 "foo" 0755`, E: syscall.EEXIST},
{C: `openat 4 "foo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5},
{C: `mkdirat 5 "42" 0755`, E: syscall.EEXIST},
{C: `openat 5 "42" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 6},
{C: `close 5`},
{C: `close 4`},
{C: `close 3`},
{C: `mkdirat 6 "dir" 0755`},
{C: `openat 6 "dir" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 6`},
})
}
// We want to create a symlink in /etc which is a tmpfs that we mounted so that is ok.
func (s *utilsSuite) TestSecureMksymlinkAllInEtcAfterMimic(c *C) {
// Because /etc is not on a list of unrestricted paths the write to
// /etc/symlink must be validated with step-by-step operation.
rootStatfs := syscall.Statfs_t{Type: update.SquashfsMagic, Flags: update.StReadOnly}
rootStat := syscall.Stat_t{}
etcStatfs := syscall.Statfs_t{Type: update.TmpfsMagic}
etcStat := syscall.Stat_t{}
s.as.AddChange(&update.Change{Action: update.Mount, Entry: osutil.MountEntry{Dir: "/etc", Type: "tmpfs", Name: "tmpfs"}})
s.sys.InsertFstatfsResult(`fstatfs 3 `, rootStatfs)
s.sys.InsertFstatResult(`fstat 3 `, rootStat)
s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST)
s.sys.InsertFstatfsResult(`fstatfs 4 `, etcStatfs)
s.sys.InsertFstatResult(`fstat 4 `, etcStat)
rs := s.as.RestrictionsFor("/etc/symlink")
err := update.MksymlinkAll("/etc/symlink", 0755, 0, 0, "/oldname", rs)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fstatfs 3 `, R: rootStatfs},
{C: `fstat 3 `, R: rootStat},
{C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST},
{C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `fstatfs 4 `, R: etcStatfs},
{C: `fstat 4 `, R: etcStat},
{C: `symlinkat "/oldname" 4 "symlink"`},
{C: `close 4`},
})
}
// We want to create a file in /etc which is a tmpfs created by snapd so that's okay.
func (s *utilsSuite) TestSecureMkfileAllInEtcAfterMimic(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic})
s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST)
s.as.AddChange(&update.Change{Action: update.Mount, Entry: osutil.MountEntry{Dir: "/etc", Type: "tmpfs", Name: "tmpfs"}})
rs := s.as.RestrictionsFor("/etc/file")
err := update.MkfileAll("/etc/file", 0755, 0, 0, rs)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}},
{C: `fstat 3 `, R: syscall.Stat_t{}},
{C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST},
{C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `openat 4 "file" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 4`},
})
}
// We want to create a directory in /etc which is a tmpfs created by snapd so that is ok.
func (s *utilsSuite) TestSecureMkdirAllInEtcAfterMimic(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic})
s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST)
s.as.AddChange(&update.Change{Action: update.Mount, Entry: osutil.MountEntry{Dir: "/etc", Type: "tmpfs", Name: "tmpfs"}})
rs := s.as.RestrictionsFor("/etc/dir")
err := update.MkdirAll("/etc/dir", 0755, 0, 0, rs)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}},
{C: `fstat 3 `, R: syscall.Stat_t{}},
{C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST},
{C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `mkdirat 4 "dir" 0755`},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 4`},
})
}
// Check that we can actually create files.
// This doesn't test the chown logic as that requires root.
func (s *realSystemSuite) TestSecureMkfileAllForReal(c *C) {
d := c.MkDir()
// Create f1, which is a simple subdirectory, with a distinct mode and
// check that it was applied. Note that default umask 022 is subtracted so
// effective directory has different permissions.
f1 := filepath.Join(d, "file")
c.Assert(update.MkfileAll(f1, 0707, sys.FlagID, sys.FlagID, nil), IsNil)
fi, err := os.Stat(f1)
c.Assert(err, IsNil)
c.Check(fi.Mode().IsRegular(), Equals, true)
c.Check(fi.Mode().Perm(), Equals, os.FileMode(0705))
// Create f2, which is a deeper subdirectory, with another distinct mode
// and check that it was applied.
f2 := filepath.Join(d, "subdir/subdir/file")
c.Assert(update.MkfileAll(f2, 0750, sys.FlagID, sys.FlagID, nil), IsNil)
fi, err = os.Stat(f2)
c.Assert(err, IsNil)
c.Check(fi.Mode().IsRegular(), Equals, true)
c.Check(fi.Mode().Perm(), Equals, os.FileMode(0750))
}
// Check that we can actually create symlinks.
// This doesn't test the chown logic as that requires root.
func (s *realSystemSuite) TestSecureMksymlinkAllForReal(c *C) {
d := c.MkDir()
// Create symlink f1 that points to "oldname" and check that it
// is correct. Note that symlink permissions are always set to 0777
f1 := filepath.Join(d, "symlink")
err := update.MksymlinkAll(f1, 0755, sys.FlagID, sys.FlagID, "oldname", nil)
c.Assert(err, IsNil)
fi, err := os.Lstat(f1)
c.Assert(err, IsNil)
c.Check(fi.Mode()&os.ModeSymlink, Equals, os.ModeSymlink)
c.Check(fi.Mode().Perm(), Equals, os.FileMode(0777))
target, err := os.Readlink(f1)
c.Assert(err, IsNil)
c.Check(target, Equals, "oldname")
// Create an identical symlink to see that it doesn't fail.
err = update.MksymlinkAll(f1, 0755, sys.FlagID, sys.FlagID, "oldname", nil)
c.Assert(err, IsNil)
// Create a different symlink and see that it fails now
err = update.MksymlinkAll(f1, 0755, sys.FlagID, sys.FlagID, "other", nil)
c.Assert(err, ErrorMatches, `cannot create symbolic link ".*/symlink": existing symbolic link in the way`)
// Create an file and check that it clashes with a symlink we attempt to create.
f2 := filepath.Join(d, "file")
err = update.MkfileAll(f2, 0755, sys.FlagID, sys.FlagID, nil)
c.Assert(err, IsNil)
err = update.MksymlinkAll(f2, 0755, sys.FlagID, sys.FlagID, "oldname", nil)
c.Assert(err, ErrorMatches, `cannot create symbolic link ".*/file": existing file in the way`)
// Create an file and check that it clashes with a symlink we attempt to create.
f3 := filepath.Join(d, "dir")
err = update.MkdirAll(f3, 0755, sys.FlagID, sys.FlagID, nil)
c.Assert(err, IsNil)
err = update.MksymlinkAll(f3, 0755, sys.FlagID, sys.FlagID, "oldname", nil)
c.Assert(err, ErrorMatches, `cannot create symbolic link ".*/dir": existing file in the way`)
err = update.MksymlinkAll("/", 0755, sys.FlagID, sys.FlagID, "oldname", nil)
c.Assert(err, ErrorMatches, `cannot create non-file path: "/"`)
}
func (s *utilsSuite) TestCleanTrailingSlash(c *C) {
// This is a sanity test for the use of filepath.Clean in secureMk{dir,file}All
c.Assert(filepath.Clean("/path/"), Equals, "/path")
c.Assert(filepath.Clean("path/"), Equals, "path")
c.Assert(filepath.Clean("path/."), Equals, "path")
c.Assert(filepath.Clean("path/.."), Equals, ".")
c.Assert(filepath.Clean("other/path/.."), Equals, "other")
}
// secure-open-path
func (s *utilsSuite) TestSecureOpenPath(c *C) {
stat := syscall.Stat_t{Mode: syscall.S_IFDIR}
s.sys.InsertFstatResult("fstat 5 ", stat)
fd, err := update.OpenPath("/foo/bar")
c.Assert(err, IsNil)
defer s.sys.Close(fd)
c.Assert(fd, Equals, 5)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "foo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "bar" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: stat},
{C: `close 4`},
{C: `close 3`},
})
}
func (s *utilsSuite) TestSecureOpenPathSingleSegment(c *C) {
stat := syscall.Stat_t{Mode: syscall.S_IFDIR}
s.sys.InsertFstatResult("fstat 4 ", stat)
fd, err := update.OpenPath("/foo")
c.Assert(err, IsNil)
defer s.sys.Close(fd)
c.Assert(fd, Equals, 4)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "foo" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: stat},
{C: `close 3`},
})
}
func (s *utilsSuite) TestSecureOpenPathRoot(c *C) {
stat := syscall.Stat_t{Mode: syscall.S_IFDIR}
s.sys.InsertFstatResult("fstat 3 ", stat)
fd, err := update.OpenPath("/")
c.Assert(err, IsNil)
defer s.sys.Close(fd)
c.Assert(fd, Equals, 3)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `fstat 3 `, R: stat},
})
}
func (s *realSystemSuite) TestSecureOpenPathDirectory(c *C) {
path := filepath.Join(c.MkDir(), "test")
c.Assert(os.Mkdir(path, 0755), IsNil)
fd, err := update.OpenPath(path)
c.Assert(err, IsNil)
defer syscall.Close(fd)
// check that the file descriptor is for the expected path
origDir, err := os.Getwd()
c.Assert(err, IsNil)
defer os.Chdir(origDir)
c.Assert(syscall.Fchdir(fd), IsNil)
cwd, err := os.Getwd()
c.Assert(err, IsNil)
c.Check(cwd, Equals, path)
}
func (s *realSystemSuite) TestSecureOpenPathRelativePath(c *C) {
fd, err := update.OpenPath("relative/path")
c.Check(fd, Equals, -1)
c.Check(err, ErrorMatches, "path .* is not absolute")
}
func (s *realSystemSuite) TestSecureOpenPathUncleanPath(c *C) {
base := c.MkDir()
path := filepath.Join(base, "test")
c.Assert(os.Mkdir(path, 0755), IsNil)
fd, err := update.OpenPath(base + "//test")
c.Check(fd, Equals, -1)
c.Check(err, ErrorMatches, `cannot open path: cannot iterate over unclean path ".*//test"`)
fd, err = update.OpenPath(base + "/./test")
c.Check(fd, Equals, -1)
c.Check(err, ErrorMatches, `cannot open path: cannot iterate over unclean path ".*/./test"`)
fd, err = update.OpenPath(base + "/test/../test")
c.Check(fd, Equals, -1)
c.Check(err, ErrorMatches, `cannot open path: cannot iterate over unclean path ".*/test/../test"`)
}
func (s *realSystemSuite) TestSecureOpenPathFile(c *C) {
path := filepath.Join(c.MkDir(), "file.txt")
c.Assert(ioutil.WriteFile(path, []byte("hello"), 0644), IsNil)
fd, err := update.OpenPath(path)
c.Assert(err, IsNil)
defer syscall.Close(fd)
// Check that the file descriptor matches the file.
var pathStat, fdStat syscall.Stat_t
c.Assert(syscall.Stat(path, &pathStat), IsNil)
c.Assert(syscall.Fstat(fd, &fdStat), IsNil)
c.Check(pathStat, Equals, fdStat)
}
func (s *realSystemSuite) TestSecureOpenPathNotFound(c *C) {
path := filepath.Join(c.MkDir(), "test")
fd, err := update.OpenPath(path)
c.Check(fd, Equals, -1)
c.Check(err, ErrorMatches, "no such file or directory")
}
func (s *realSystemSuite) TestSecureOpenPathSymlink(c *C) {
base := c.MkDir()
dir := filepath.Join(base, "test")
c.Assert(os.Mkdir(dir, 0755), IsNil)
symlink := filepath.Join(base, "symlink")
c.Assert(os.Symlink(dir, symlink), IsNil)
fd, err := update.OpenPath(symlink)
c.Check(fd, Equals, -1)
c.Check(err, ErrorMatches, `".*" is a symbolic link`)
}
func (s *realSystemSuite) TestSecureOpenPathSymlinkedParent(c *C) {
base := c.MkDir()
dir := filepath.Join(base, "dir1")
symlink := filepath.Join(base, "symlink")
path := filepath.Join(dir, "dir2")
symlinkedPath := filepath.Join(symlink, "dir2")
c.Assert(os.Mkdir(dir, 0755), IsNil)
c.Assert(os.Symlink(dir, symlink), IsNil)
c.Assert(os.Mkdir(path, 0755), IsNil)
fd, err := update.OpenPath(symlinkedPath)
c.Check(fd, Equals, -1)
c.Check(err, ErrorMatches, "not a directory")
}
snapd-2.37.4~14.04.1/cmd/snap-update-ns/secure_bindmount_test.go 0000664 0000000 0000000 00000020665 13435556260 021107 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package main_test
import (
"syscall"
. "gopkg.in/check.v1"
update "github.com/snapcore/snapd/cmd/snap-update-ns"
"github.com/snapcore/snapd/testutil"
)
type secureBindMountSuite struct {
testutil.BaseTest
sys *testutil.SyscallRecorder
}
var _ = Suite(&secureBindMountSuite{})
func (s *secureBindMountSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
s.sys = &testutil.SyscallRecorder{}
s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys))
}
func (s *secureBindMountSuite) TearDownTest(c *C) {
s.sys.CheckForStrayDescriptors(c)
s.BaseTest.TearDownTest(c)
}
func (s *secureBindMountSuite) TestMount(c *C) {
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/source"
{C: `close 3`}, // "/"
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/target"
{C: `close 3`}, // "/"
{C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`},
{C: `close 6`}, // "/target/dir"
{C: `close 5`}, // "/source/dir"
})
}
func (s *secureBindMountSuite) TestMountRecursive(c *C) {
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_REC)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/source"
{C: `close 3`}, // "/"
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/target"
{C: `close 3`}, // "/"
{C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND|MS_REC ""`},
{C: `close 6`}, // "/target/dir"
{C: `close 5`}, // "/source/dir"
})
}
func (s *secureBindMountSuite) TestMountReadOnly(c *C) {
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{})
err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/source"
{C: `close 3`}, // "/"
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/target"
{C: `close 3`}, // "/"
{C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7},
{C: `fstat 7 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/target"
{C: `close 3`}, // "/"
{C: `mount "none" "/proc/self/fd/7" "" MS_REMOUNT|MS_BIND|MS_RDONLY ""`},
{C: `close 7`}, // "/target/dir"
{C: `close 6`}, // "/target/dir"
{C: `close 5`}, // "/source/dir"
})
}
func (s *secureBindMountSuite) TestBindFlagRequired(c *C) {
err := update.BindMount("/source/dir", "/target/dir", syscall.MS_REC)
c.Assert(err, ErrorMatches, "cannot perform non-bind mount operation")
c.Check(s.sys.RCalls(), HasLen, 0)
}
func (s *secureBindMountSuite) TestMountReadOnlyRecursive(c *C) {
err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY|syscall.MS_REC)
c.Assert(err, ErrorMatches, "cannot use MS_RDONLY and MS_REC together")
c.Check(s.sys.RCalls(), HasLen, 0)
}
func (s *secureBindMountSuite) TestBindMountFails(c *C) {
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
s.sys.InsertFault(`mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`, errTesting)
err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY)
c.Assert(err, ErrorMatches, "testing")
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/source"
{C: `close 3`}, // "/"
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/target"
{C: `close 3`}, // "/"
{C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`, E: errTesting},
{C: `close 6`}, // "/target/dir"
{C: `close 5`}, // "/source/dir"
})
}
func (s *secureBindMountSuite) TestRemountReadOnlyFails(c *C) {
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{})
s.sys.InsertFault(`mount "none" "/proc/self/fd/7" "" MS_REMOUNT|MS_BIND|MS_RDONLY ""`, errTesting)
err := update.BindMount("/source/dir", "/target/dir", syscall.MS_BIND|syscall.MS_RDONLY)
c.Assert(err, ErrorMatches, "testing")
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/source"
{C: `close 3`}, // "/"
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/target"
{C: `close 3`}, // "/"
{C: `mount "/proc/self/fd/5" "/proc/self/fd/6" "" MS_BIND ""`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "dir" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7},
{C: `fstat 7 `, R: syscall.Stat_t{}},
{C: `close 4`}, // "/target"
{C: `close 3`}, // "/"
{C: `mount "none" "/proc/self/fd/7" "" MS_REMOUNT|MS_BIND|MS_RDONLY ""`, E: errTesting},
{C: `unmount "/proc/self/fd/7" UMOUNT_NOFOLLOW|MNT_DETACH`},
{C: `close 7`}, // "/target/dir"
{C: `close 6`}, // "/target/dir"
{C: `close 5`}, // "/source/dir"
})
}
snapd-2.37.4~14.04.1/cmd/snap-update-ns/change.go 0000664 0000000 0000000 00000040177 13435556260 015730 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"syscall"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
)
// Action represents a mount action (mount, remount, unmount, etc).
type Action string
const (
// Keep indicates that a given mount entry should be kept as-is.
Keep Action = "keep"
// Mount represents an action that results in mounting something somewhere.
Mount Action = "mount"
// Unmount represents an action that results in unmounting something from somewhere.
Unmount Action = "unmount"
// Remount when needed
)
var (
// ErrIgnoredMissingMount is returned when a mount entry has
// been marked with x-snapd.ignore-missing, and the mount
// source or target do not exist.
ErrIgnoredMissingMount = errors.New("mount source or target are missing")
)
// Change describes a change to the mount table (action and the entry to act on).
type Change struct {
Entry osutil.MountEntry
Action Action
}
// String formats mount change to a human-readable line.
func (c Change) String() string {
return fmt.Sprintf("%s (%s)", c.Action, c.Entry)
}
// changePerform is Change.Perform that can be mocked for testing.
var changePerform func(*Change, *Assumptions) ([]*Change, error)
// mimicRequired provides information if an error warrants a writable mimic.
//
// The returned path is the location where a mimic should be constructed.
func mimicRequired(err error) (needsMimic bool, path string) {
switch err.(type) {
case *ReadOnlyFsError:
rofsErr := err.(*ReadOnlyFsError)
return true, rofsErr.Path
case *TrespassingError:
tErr := err.(*TrespassingError)
return true, tErr.ViolatedPath
}
return false, ""
}
func (c *Change) createPath(path string, pokeHoles bool, as *Assumptions) ([]*Change, error) {
// If we've been asked to create a missing path, and the mount
// entry uses the ignore-missing option, return an error.
if c.Entry.XSnapdIgnoreMissing() {
return nil, ErrIgnoredMissingMount
}
var err error
var changes []*Change
// In case we need to create something, some constants.
const (
mode = 0755
uid = 0
gid = 0
)
// If the element doesn't exist we can attempt to create it. We will
// create the parent directory and then the final element relative to it.
// The traversed space may be writable so we just try to create things
// first.
kind := c.Entry.XSnapdKind()
// TODO: re-factor this, if possible, with inspection and preemptive
// creation after the current release ships. This should be possible but
// will affect tests heavily (churn, not safe before release).
rs := as.RestrictionsFor(path)
switch kind {
case "":
err = MkdirAll(path, mode, uid, gid, rs)
case "file":
err = MkfileAll(path, mode, uid, gid, rs)
case "symlink":
err = MksymlinkAll(path, mode, uid, gid, c.Entry.XSnapdSymlink(), rs)
}
if needsMimic, mimicPath := mimicRequired(err); needsMimic && pokeHoles {
// If the error can be recovered by using a writable mimic
// then construct one and try again.
changes, err = createWritableMimic(mimicPath, path, as)
if err != nil {
err = fmt.Errorf("cannot create writable mimic over %q: %s", mimicPath, err)
} else {
// Try once again. Note that we care *just* about the error. We have already
// performed the hole poking and thus additional changes must be nil.
_, err = c.createPath(path, false, as)
}
}
return changes, err
}
func (c *Change) ensureTarget(as *Assumptions) ([]*Change, error) {
var changes []*Change
kind := c.Entry.XSnapdKind()
path := c.Entry.Dir
// We use lstat to ensure that we don't follow a symlink in case one was
// set up by the snap. Note that at the time this is run, all the snap's
// processes are frozen but if the path is a directory controlled by the
// user (typically in /home) then we may still race with user processes
// that change it.
fi, err := osLstat(path)
if err == nil {
// If the element already exists we just need to ensure it is of
// the correct type. The desired type depends on the kind of entry
// we are working with.
switch kind {
case "":
if !fi.Mode().IsDir() {
err = fmt.Errorf("cannot use %q as mount point: not a directory", path)
}
case "file":
if !fi.Mode().IsRegular() {
err = fmt.Errorf("cannot use %q as mount point: not a regular file", path)
}
case "symlink":
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
// Create path verifies the symlink or fails if it is not what we wanted.
_, err = c.createPath(path, false, as)
} else {
err = fmt.Errorf("cannot create symlink in %q: existing file in the way", path)
}
}
} else if os.IsNotExist(err) {
changes, err = c.createPath(path, true, as)
} else {
// If we cannot inspect the element let's just bail out.
err = fmt.Errorf("cannot inspect %q: %v", path, err)
}
return changes, err
}
func (c *Change) ensureSource(as *Assumptions) ([]*Change, error) {
var changes []*Change
// We only have to do ensure bind mount source exists.
// This also rules out symlinks.
flags, _ := osutil.MountOptsToCommonFlags(c.Entry.Options)
if flags&syscall.MS_BIND == 0 {
return nil, nil
}
kind := c.Entry.XSnapdKind()
path := c.Entry.Name
fi, err := osLstat(path)
if err == nil {
// If the element already exists we just need to ensure it is of
// the correct type. The desired type depends on the kind of entry
// we are working with.
switch kind {
case "":
if !fi.Mode().IsDir() {
err = fmt.Errorf("cannot use %q as bind-mount source: not a directory", path)
}
case "file":
if !fi.Mode().IsRegular() {
err = fmt.Errorf("cannot use %q as bind-mount source: not a regular file", path)
}
}
} else if os.IsNotExist(err) {
// NOTE: This createPath is using pokeHoles, to make read-only places
// writable, but only for layouts and not for other (typically content
// sharing) mount entries.
//
// This is done because the changes made with pokeHoles=true are only
// visible in this current mount namespace and are not generally
// visible from other snaps because they inhabit different namespaces.
//
// In other words, changes made here are only observable by the single
// snap they apply to. As such they are useless for content sharing but
// very much useful to layouts.
pokeHoles := c.Entry.XSnapdOrigin() == "layout"
changes, err = c.createPath(path, pokeHoles, as)
} else {
// If we cannot inspect the element let's just bail out.
err = fmt.Errorf("cannot inspect %q: %v", path, err)
}
return changes, err
}
// changePerformImpl is the real implementation of Change.Perform
func changePerformImpl(c *Change, as *Assumptions) (changes []*Change, err error) {
if c.Action == Mount {
var changesSource, changesTarget []*Change
// We may be asked to bind mount a file, bind mount a directory, mount
// a filesystem over a directory, or create a symlink (which is abusing
// the "mount" concept slightly). That actual operation is performed in
// c.lowLevelPerform. Here we just set the stage to make that possible.
//
// As a result of this ensure call we may need to make the medium writable
// and that's why we may return more changes as a result of performing this
// one.
changesTarget, err = c.ensureTarget(as)
// NOTE: we are collecting changes even if things fail. This is so that
// upper layers can perform undo correctly.
changes = append(changes, changesTarget...)
if err != nil {
return changes, err
}
// At this time we can be sure that the target element (for files and
// directories) exists and is of the right type or that it (for
// symlinks) doesn't exist but the parent directory does.
// This property holds as long as we don't interact with locations that
// are under the control of regular (non-snap) processes that are not
// suspended and may be racing with us.
changesSource, err = c.ensureSource(as)
// NOTE: we are collecting changes even if things fail. This is so that
// upper layers can perform undo correctly.
changes = append(changes, changesSource...)
if err != nil {
return changes, err
}
}
// Perform the underlying mount / unmount / unlink call.
err = c.lowLevelPerform(as)
return changes, err
}
func init() {
changePerform = changePerformImpl
}
// Perform executes the desired mount or unmount change using system calls.
// Filesystems that depend on helper programs or multiple independent calls to
// the kernel (--make-shared, for example) are unsupported.
//
// Perform may synthesize *additional* changes that were necessary to perform
// this change (such as mounted tmpfs or overlayfs).
func (c *Change) Perform(as *Assumptions) ([]*Change, error) {
return changePerform(c, as)
}
// lowLevelPerform is simple bridge from Change to mount / unmount syscall.
func (c *Change) lowLevelPerform(as *Assumptions) error {
var err error
switch c.Action {
case Mount:
kind := c.Entry.XSnapdKind()
switch kind {
case "symlink":
// symlinks are handled in createInode directly, nothing to do here.
case "", "file":
flags, unparsed := osutil.MountOptsToCommonFlags(c.Entry.Options)
// Use Secure.BindMount for bind mounts
if flags&syscall.MS_BIND == syscall.MS_BIND {
err = BindMount(c.Entry.Name, c.Entry.Dir, uint(flags))
} else {
err = sysMount(c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flags), strings.Join(unparsed, ","))
}
logger.Debugf("mount %q %q %q %d %q (error: %v)", c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flags), strings.Join(unparsed, ","), err)
if err == nil {
as.AddChange(c)
}
}
return err
case Unmount:
kind := c.Entry.XSnapdKind()
switch kind {
case "symlink":
err = osRemove(c.Entry.Dir)
logger.Debugf("remove %q (error: %v)", c.Entry.Dir, err)
case "", "file":
// Detach the mount point instead of unmounting it if requested.
flags := umountNoFollow
if c.Entry.XSnapdDetach() {
flags |= syscall.MNT_DETACH
}
// Perform the raw unmount operation.
err = sysUnmount(c.Entry.Dir, flags)
if err == nil {
as.AddChange(c)
}
logger.Debugf("umount %q (error: %v)", c.Entry.Dir, err)
if err != nil {
return err
}
// Open a path of the file we are considering the removal of.
path := c.Entry.Dir
var fd int
fd, err = OpenPath(path)
if err != nil {
return err
}
defer sysClose(fd)
// Don't attempt to remove anything from squashfs.
var statfsBuf syscall.Statfs_t
err = sysFstatfs(fd, &statfsBuf)
if err != nil {
return err
}
if statfsBuf.Type == SquashfsMagic {
return nil
}
if kind == "file" {
// Don't attempt to remove non-empty files since they cannot be
// the placeholders we created.
var statBuf syscall.Stat_t
err = sysFstat(fd, &statBuf)
if err != nil {
return err
}
if statBuf.Size != 0 {
return nil
}
}
// Remove the file or directory while using the full path. There's
// no way to avoid a race here since there's no way to unlink a
// file solely by file descriptor.
err = osRemove(path)
// Unpack the low-level error that osRemove wraps into PathError.
if packed, ok := err.(*os.PathError); ok {
err = packed.Err
}
// If we were removing a directory but it was not empty then just
// ignore the error. This is the equivalent of the non-empty file
// check we do above. See rmdir(2) for explanation why we accept
// more than one errno value.
if kind == "" && (err == syscall.ENOTEMPTY || err == syscall.EEXIST) {
return nil
}
}
return err
case Keep:
return nil
}
return fmt.Errorf("cannot process mount change: unknown action: %q", c.Action)
}
// NeededChanges computes the changes required to change current to desired mount entries.
//
// The current and desired profiles is a fstab like list of mount entries. The
// lists are processed and a "diff" of mount changes is produced. The mount
// changes, when applied in order, transform the current profile into the
// desired profile.
func NeededChanges(currentProfile, desiredProfile *osutil.MountProfile) []*Change {
// Copy both profiles as we will want to mutate them.
current := make([]osutil.MountEntry, len(currentProfile.Entries))
copy(current, currentProfile.Entries)
desired := make([]osutil.MountEntry, len(desiredProfile.Entries))
copy(desired, desiredProfile.Entries)
// Clean the directory part of both profiles. This is done so that we can
// easily test if a given directory is a subdirectory with
// strings.HasPrefix coupled with an extra slash character.
for i := range current {
current[i].Dir = filepath.Clean(current[i].Dir)
}
for i := range desired {
desired[i].Dir = filepath.Clean(desired[i].Dir)
}
// Sort both lists by directory name with implicit trailing slash.
sort.Sort(byOriginAndMagicDir(current))
sort.Sort(byOriginAndMagicDir(desired))
// Construct a desired directory map.
desiredMap := make(map[string]*osutil.MountEntry)
for i := range desired {
desiredMap[desired[i].Dir] = &desired[i]
}
// Indexed by mount point path.
reuse := make(map[string]bool)
// Indexed by entry ID
desiredIDs := make(map[string]bool)
var skipDir string
// Collect the IDs of desired changes.
// We need that below to keep implicit changes from the current profile.
for i := range desired {
desiredIDs[desired[i].XSnapdEntryID()] = true
}
// Compute reusable entries: those which are equal in current and desired and which
// are not prefixed by another entry that changed.
for i := range current {
dir := current[i].Dir
if skipDir != "" && strings.HasPrefix(dir, skipDir) {
logger.Debugf("skipping entry %q", current[i])
continue
}
skipDir = "" // reset skip prefix as it no longer applies
// Reuse synthetic entries if their needed-by entry is desired.
// Synthetic entries cannot exist on their own and always couple to a
// non-synthetic entry.
// NOTE: Synthetic changes have a special purpose.
//
// They are a "shadow" of mount events that occurred to allow one of
// the desired mount entries to be possible. The changes have only one
// goal: tell snap-update-ns how those mount events can be undone in
// case they are no longer needed. The actual changes may have been
// different and may have involved steps not represented as synthetic
// mount entires as long as those synthetic entries can be undone to
// reverse the effect. In reality each non-tmpfs synthetic entry was
// constructed using a temporary bind mount that contained the original
// mount entries of a directory that was hidden with a tmpfs, but this
// fact was lost.
if current[i].XSnapdSynthetic() && desiredIDs[current[i].XSnapdNeededBy()] {
logger.Debugf("reusing synthetic entry %q", current[i])
reuse[dir] = true
continue
}
// Reuse entries that are desired and identical in the current profile.
if entry, ok := desiredMap[dir]; ok && current[i].Equal(entry) {
logger.Debugf("reusing unchanged entry %q", current[i])
reuse[dir] = true
continue
}
skipDir = strings.TrimSuffix(dir, "/") + "/"
}
logger.Debugf("desiredIDs: %v", desiredIDs)
logger.Debugf("reuse: %v", reuse)
// We are now ready to compute the necessary mount changes.
var changes []*Change
// Unmount entries not reused in reverse to handle children before their parent.
for i := len(current) - 1; i >= 0; i-- {
if reuse[current[i].Dir] {
changes = append(changes, &Change{Action: Keep, Entry: current[i]})
} else {
changes = append(changes, &Change{Action: Unmount, Entry: current[i]})
}
}
// Mount desired entries not reused.
for i := range desired {
if !reuse[desired[i].Dir] {
changes = append(changes, &Change{Action: Mount, Entry: desired[i]})
}
}
return changes
}
snapd-2.37.4~14.04.1/cmd/snap-update-ns/trespassing.go 0000664 0000000 0000000 00000023374 13435556260 017045 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017-2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package main
import (
"fmt"
"path/filepath"
"strings"
"syscall"
"github.com/snapcore/snapd/logger"
)
// Assumptions track the assumptions about the state of the filesystem.
//
// Assumptions constitute the global part of the write restriction management.
// Assumptions are global in the sense that they span multiple distinct write
// operations. In contrast, Restrictions track per-operation state.
type Assumptions struct {
unrestrictedPaths []string
pastChanges []*Change
// verifiedDevices represents the set of devices that are verified as a tmpfs
// that was mounted by snapd. Those are only discovered on-demand. The
// major:minor number is packed into one uint64 as in syscall.Stat_t.Dev
// field.
verifiedDevices map[uint64]bool
}
// AddUnrestrictedPaths adds a list of directories where writing is allowed
// even if it would hit the real host filesystem (or transit through the host
// filesystem). This is intended to be used with certain well-known locations
// such as /tmp, $SNAP_DATA and $SNAP.
func (as *Assumptions) AddUnrestrictedPaths(paths ...string) {
as.unrestrictedPaths = append(as.unrestrictedPaths, paths...)
}
// isRestricted checks whether a path falls under restricted writing scheme.
//
// Provided path is the full, absolute path of the entity that needs to be
// created (directory, file or symbolic link).
func (as *Assumptions) isRestricted(path string) bool {
// Anything rooted at one of the unrestricted paths is not restricted.
// Those are for things like /var/snap/, for example.
for _, p := range as.unrestrictedPaths {
if p == "/" || p == path || strings.HasPrefix(path, filepath.Clean(p)+"/") {
return false
}
}
// All other paths are restricted
return true
}
// MockUnrestrictedPaths replaces the set of path paths without any restrictions.
func (as *Assumptions) MockUnrestrictedPaths(paths ...string) (restore func()) {
old := as.unrestrictedPaths
as.unrestrictedPaths = paths
return func() {
as.unrestrictedPaths = old
}
}
// AddChange records the fact that a change was applied to the system.
func (as *Assumptions) AddChange(change *Change) {
as.pastChanges = append(as.pastChanges, change)
}
// canWriteToDirectory returns true if writing to a given directory is allowed.
//
// Writing is allowed in one of thee cases:
// 1) The directory is in one of the explicitly permitted locations.
// This is the strongest permission as it explicitly allows writing to
// places that may show up on the host, one of the examples being $SNAP_DATA.
// 2) The directory is on a read-only filesystem.
// 3) The directory is on a tmpfs created by snapd.
func (as *Assumptions) canWriteToDirectory(dirFd int, dirName string) (bool, error) {
if !as.isRestricted(dirName) {
return true, nil
}
var fsData syscall.Statfs_t
if err := sysFstatfs(dirFd, &fsData); err != nil {
return false, fmt.Errorf("cannot fstatfs %q: %s", dirName, err)
}
var fileData syscall.Stat_t
if err := sysFstat(dirFd, &fileData); err != nil {
return false, fmt.Errorf("cannot fstat %q: %s", dirName, err)
}
// Writing to read only directories is allowed because EROFS is handled
// by each of the writing helpers already.
if ok := isReadOnly(dirName, &fsData); ok {
return true, nil
}
// Writing to a trusted tmpfs is allowed because those are not leaking to
// the host. Also, each time we find a good tmpfs we explicitly remember the device major/minor,
if as.verifiedDevices[fileData.Dev] {
return true, nil
}
if ok := isPrivateTmpfsCreatedBySnapd(dirName, &fsData, &fileData, as.pastChanges); ok {
if as.verifiedDevices == nil {
as.verifiedDevices = make(map[uint64]bool)
}
// Don't record 0:0 as those are all to easy to add in tests and would
// skew tests using zero-initialized structures. Real device numbers
// are not zero either so this is not a test-only conditional.
if fileData.Dev != 0 {
as.verifiedDevices[fileData.Dev] = true
}
return true, nil
}
// If writing is not not allowed by one of the three rules above then it is
// disallowed.
return false, nil
}
// RestrictionsFor computes restrictions for the desired path.
func (as *Assumptions) RestrictionsFor(desiredPath string) *Restrictions {
// Writing to a restricted path results in step-by-step validation of each
// directory, starting from the root of the file system. Unless writing is
// allowed a mimic must be constructed to ensure that writes are not visible in
// undesired locations of the host filesystem.
if as.isRestricted(desiredPath) {
return &Restrictions{assumptions: as, desiredPath: desiredPath, restricted: true}
}
return nil
}
// Restrictions contains meta-data of a compound write operation.
//
// This structure helps functions that write to the filesystem to keep track of
// the ultimate destination across several calls (e.g. the function that
// creates a file needs to call helpers to create subsequent directories).
// Keeping track of the desired path aids in constructing useful error
// messages.
//
// In addition the structure keeps track of the restricted write mode flag which
// is based on the full path of the desired object being constructed. This allows
// various write helpers to avoid trespassing on host filesystem in places that
// are not expected to be written to by snapd (e.g. outside of $SNAP_DATA).
type Restrictions struct {
assumptions *Assumptions
desiredPath string
restricted bool
}
// Check verifies whether writing to a directory would trespass on the host.
//
// The check is only performed in restricted mode. If the check fails a
// TrespassingError is returned.
func (rs *Restrictions) Check(dirFd int, dirName string) error {
if rs == nil || !rs.restricted {
return nil
}
// In restricted mode check the directory before attempting to write to it.
ok, err := rs.assumptions.canWriteToDirectory(dirFd, dirName)
if ok || err != nil {
return err
}
if dirName == "/" {
// If writing to / is not allowed then we are in a tough spot because
// we cannot construct a writable mimic over /. This should never
// happen in normal circumstances because the root filesystem is some
// kind of base snap.
return fmt.Errorf("cannot recover from trespassing over /")
}
logger.Debugf("trespassing violated %q while striving to %q", dirName, rs.desiredPath)
logger.Debugf("restricted mode: %#v", rs.restricted)
logger.Debugf("unrestricted paths: %q", rs.assumptions.unrestrictedPaths)
logger.Debugf("verified devices: %v", rs.assumptions.verifiedDevices)
logger.Debugf("past changes: %v", rs.assumptions.pastChanges)
return &TrespassingError{ViolatedPath: filepath.Clean(dirName), DesiredPath: rs.desiredPath}
}
// Lift lifts write restrictions for the desired path.
//
// This function should be called when, as subsequent components of a path are
// either discovered or created, the conditions for using restricted mode are
// no longer true.
func (rs *Restrictions) Lift() {
if rs != nil {
rs.restricted = false
}
}
// TrespassingError is an error when filesystem operation would affect the host.
type TrespassingError struct {
ViolatedPath string
DesiredPath string
}
// Error returns a formatted error message.
func (e *TrespassingError) Error() string {
return fmt.Sprintf("cannot write to %q because it would affect the host in %q", e.DesiredPath, e.ViolatedPath)
}
// isReadOnly checks whether the underlying filesystem is read only or is mounted as such.
func isReadOnly(dirName string, fsData *syscall.Statfs_t) bool {
// If something is mounted with f_flags & ST_RDONLY then is read-only.
if fsData.Flags&StReadOnly == StReadOnly {
return true
}
// If something is a known read-only file-system then it is safe.
// Older copies of snapd were not mounting squashfs as read only.
if fsData.Type == SquashfsMagic {
return true
}
return false
}
// isPrivateTmpfsCreatedBySnapd checks whether a directory resides on a tmpfs mounted by snapd
//
// The function inspects the directory and a list of changes that were applied
// to the mount namespace. A directory is trusted if it is a tmpfs that was
// mounted by snap-confine or snapd-update-ns. Note that sub-directories of a
// trusted tmpfs are not considered trusted by this function.
func isPrivateTmpfsCreatedBySnapd(dirName string, fsData *syscall.Statfs_t, fileData *syscall.Stat_t, changes []*Change) bool {
// If something is not a tmpfs it cannot be the trusted tmpfs we are looking for.
if fsData.Type != TmpfsMagic {
return false
}
// Any of the past changes that mounted a tmpfs exactly at the directory we
// are inspecting is considered as trusted. This is conservative because it
// doesn't trust sub-directories of a trusted tmpfs. This approach is
// sufficient for the intended use.
//
// The algorithm goes over all the changes in reverse and picks up the
// first tmpfs mount or unmount action that matches the directory name.
// The set of constraints in snap-update-ns and snapd prevent from mounting
// over an existing mount point so we don't need to consider e.g. a bind
// mount shadowing an active tmpfs.
for i := len(changes) - 1; i >= 0; i-- {
change := changes[i]
if change.Entry.Type == "tmpfs" && change.Entry.Dir == dirName {
return change.Action == Mount
}
}
return false
}
snapd-2.37.4~14.04.1/cmd/snap-update-ns/bootstrap.c 0000664 0000000 0000000 00000033526 13435556260 016335 0 ustar /*
* Copyright (C) 2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
// IMPORTANT: all the code in this file may be run with elevated privileges
// when invoking snap-update-ns from the setuid snap-confine.
//
// This file is a preprocessor for snap-update-ns' main() function. It will
// perform input validation and clear the environment so that snap-update-ns'
// go code runs with safe inputs when called by the setuid() snap-confine.
#include "bootstrap.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// bootstrap_errno contains a copy of errno if a system call fails.
int bootstrap_errno = 0;
// bootstrap_msg contains a static string if something fails.
const char *bootstrap_msg = NULL;
// setns_into_snap switches mount namespace into that of a given snap.
static int setns_into_snap(const char *snap_name)
{
// Construct the name of the .mnt file to open.
char buf[PATH_MAX] = {
0,
};
int n = snprintf(buf, sizeof buf, "/run/snapd/ns/%s.mnt", snap_name);
if (n >= sizeof buf || n < 0) {
bootstrap_errno = 0;
bootstrap_msg = "cannot format mount namespace file name";
return -1;
}
// Open the mount namespace file.
int fd = open(buf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (fd < 0) {
bootstrap_errno = errno;
bootstrap_msg = "cannot open mount namespace file";
return -1;
}
// Switch to the mount namespace of the given snap.
int err = setns(fd, CLONE_NEWNS);
if (err < 0) {
bootstrap_errno = errno;
bootstrap_msg = "cannot switch mount namespace";
};
close(fd);
return err;
}
// switch_to_privileged_user drops to the real user ID while retaining
// CAP_SYS_ADMIN, for operations such as mount().
static int switch_to_privileged_user()
{
uid_t real_uid;
gid_t real_gid;
real_uid = getuid();
if (real_uid == 0) {
// We're running as root: no need to switch IDs
return 0;
}
real_gid = getgid();
// _LINUX_CAPABILITY_VERSION_3 valid for kernel >= 2.6.26. See
// https://github.com/torvalds/linux/blob/master/kernel/capability.c
struct __user_cap_header_struct hdr =
{ _LINUX_CAPABILITY_VERSION_3, 0 };
struct __user_cap_data_struct data[2] = { {0} };
data[0].effective = (CAP_TO_MASK(CAP_SYS_ADMIN) |
CAP_TO_MASK(CAP_SETUID) | CAP_TO_MASK(CAP_SETGID));
data[0].permitted = data[0].effective;
data[0].inheritable = 0;
data[1].effective = 0;
data[1].permitted = 0;
data[1].inheritable = 0;
if (capset(&hdr, data) != 0) {
bootstrap_errno = errno;
bootstrap_msg = "cannot set permitted capabilities mask";
return -1;
}
if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) != 0) {
bootstrap_errno = errno;
bootstrap_msg =
"cannot tell kernel to keep capabilities over setuid";
return -1;
}
if (setgroups(1, &real_gid) != 0) {
bootstrap_errno = errno;
bootstrap_msg = "cannot drop supplementary groups";
return -1;
}
if (setgid(real_gid) != 0) {
bootstrap_errno = errno;
bootstrap_msg = "cannot switch to real group ID";
return -1;
}
if (setuid(real_uid) != 0) {
bootstrap_errno = errno;
bootstrap_msg = "cannot switch to real user ID";
return -1;
}
// After changing uid, our effective capabilities were dropped.
// Reacquire CAP_SYS_ADMIN, and discard CAP_SETUID/CAP_SETGID.
data[0].effective = CAP_TO_MASK(CAP_SYS_ADMIN);
data[0].permitted = data[0].effective;
if (capset(&hdr, data) != 0) {
bootstrap_errno = errno;
bootstrap_msg =
"cannot enable capabilities after switching to real user";
return -1;
}
return 0;
}
// TODO: reuse the code from snap-confine, if possible.
static int skip_lowercase_letters(const char **p)
{
int skipped = 0;
const char *c;
for (c = *p; *c >= 'a' && *c <= 'z'; ++c) {
skipped += 1;
}
*p = (*p) + skipped;
return skipped;
}
// TODO: reuse the code from snap-confine, if possible.
static int skip_digits(const char **p)
{
int skipped = 0;
const char *c;
for (c = *p; *c >= '0' && *c <= '9'; ++c) {
skipped += 1;
}
*p = (*p) + skipped;
return skipped;
}
// TODO: reuse the code from snap-confine, if possible.
static int skip_one_char(const char **p, char c)
{
if (**p == c) {
*p += 1;
return 1;
}
return 0;
}
// validate_snap_name performs full validation of the given name.
int validate_snap_name(const char *snap_name)
{
// NOTE: This function should be synchronized with the two other
// implementations: sc_snap_name_validate and snap.ValidateName.
// Ensure that name is not NULL
if (snap_name == NULL) {
bootstrap_msg = "snap name cannot be NULL";
return -1;
}
// This is a regexp-free routine hand-codes the following pattern:
//
// "^([a-z0-9]+-?)*[a-z](-?[a-z0-9])*$"
//
// The only motivation for not using regular expressions is so that we
// don't run untrusted input against a potentially complex regular
// expression engine.
const char *p = snap_name;
if (skip_one_char(&p, '-')) {
bootstrap_msg = "snap name cannot start with a dash";
return -1;
}
bool got_letter = false;
int n = 0, m;
for (; *p != '\0';) {
if ((m = skip_lowercase_letters(&p)) > 0) {
n += m;
got_letter = true;
continue;
}
if ((m = skip_digits(&p)) > 0) {
n += m;
continue;
}
if (skip_one_char(&p, '-') > 0) {
n++;
if (*p == '\0') {
bootstrap_msg =
"snap name cannot end with a dash";
return -1;
}
if (skip_one_char(&p, '-') > 0) {
bootstrap_msg =
"snap name cannot contain two consecutive dashes";
return -1;
}
continue;
}
bootstrap_msg =
"snap name must use lower case letters, digits or dashes";
return -1;
}
if (!got_letter) {
bootstrap_msg = "snap name must contain at least one letter";
return -1;
}
if (n < 2) {
bootstrap_msg = "snap name must be longer than 1 character";
return -1;
}
if (n > 40) {
bootstrap_msg = "snap name must be shorter than 40 characters";
return -1;
}
bootstrap_msg = NULL;
return 0;
}
static int instance_key_validate(const char *instance_key)
{
// NOTE: see snap.ValidateInstanceName for reference of a valid instance key
// format
// Ensure that name is not NULL
if (instance_key == NULL) {
bootstrap_msg = "instance key cannot be NULL";
return -1;
}
// This is a regexp-free routine hand-coding the following pattern:
//
// "^[a-z]{1,10}$"
//
// The only motivation for not using regular expressions is so that we don't
// run untrusted input against a potentially complex regular expression
// engine.
int i = 0;
for (i = 0; instance_key[i] != '\0'; i++) {
if (islower(instance_key[i]) || isdigit(instance_key[i])) {
continue;
}
bootstrap_msg =
"instance key must use lower case letters or digits";
return -1;
}
if (i == 0) {
bootstrap_msg =
"instance key must contain at least one letter or digit";
return -1;
} else if (i > 10) {
bootstrap_msg =
"instance key must be shorter than 10 characters";
return -1;
}
return 0;
}
// validate_instance_name performs full validation of the given snap instance name.
int validate_instance_name(const char *instance_name)
{
// NOTE: This function should be synchronized with the two other
// implementations: sc_instance_name_validate and snap.ValidateInstanceName.
if (instance_name == NULL) {
bootstrap_msg = "snap instance name cannot be NULL";
return -1;
}
// 40 char snap_name + '_' + 10 char instance_key + 1 extra overflow + 1
// NULL
char s[53] = { 0 };
strncpy(s, instance_name, sizeof(s) - 1);
char *t = s;
const char *snap_name = strsep(&t, "_");
const char *instance_key = strsep(&t, "_");
const char *third_separator = strsep(&t, "_");
if (third_separator != NULL) {
bootstrap_msg =
"snap instance name can contain only one underscore";
return -1;
}
if (validate_snap_name(snap_name) < 0) {
return -1;
}
// When the instance_name is a normal snap name, instance_key will be
// NULL, so only validate instance_key when we found one.
if (instance_key != NULL && instance_key_validate(instance_key) < 0) {
return -1;
}
return 0;
}
// parse the -u argument, returns -1 on failure or 0 on success.
static int parse_arg_u(int argc, char * const *argv, int *optind, unsigned long *uid_out)
{
if (*optind + 1 == argc || argv[*optind + 1] == NULL) {
bootstrap_msg = "-u requires an argument";
bootstrap_errno = 0;
return -1;
}
const char *uid_text = argv[*optind + 1];
errno = 0;
char *uid_text_end = NULL;
unsigned long parsed_uid = strtoul(uid_text, &uid_text_end, 10);
if (
/* Reject overflow in parsed representation */
(parsed_uid == ULONG_MAX && errno != 0)
/* Reject leading whitespace allowed by strtoul. */
|| (isspace(*uid_text))
/* Reject empty string. */
|| (*uid_text == '\0')
/* Reject partially parsed strings. */
|| (*uid_text != '\0' && uid_text_end != NULL
&& *uid_text_end != '\0')) {
bootstrap_msg = "cannot parse user id";
bootstrap_errno = errno;
return -1;
}
if ((long)parsed_uid < 0) {
bootstrap_msg = "user id cannot be negative";
bootstrap_errno = 0;
return -1;
}
if (uid_out != NULL) {
*uid_out = parsed_uid;
}
*optind += 1; // Account for the argument to -u.
return 0;
}
// process_arguments parses given a command line
// argc and argv are defined as for the main() function
void process_arguments(int argc, char *const *argv, const char **snap_name_out,
bool * should_setns_out, bool * process_user_fstab, unsigned long * uid_out)
{
// Find the name of the called program. If it is ending with ".test" then do nothing.
// NOTE: This lets us use cgo/go to write tests without running the bulk
// of the code automatically.
//
if (argv == NULL || argc < 1) {
bootstrap_errno = 0;
bootstrap_msg = "argv0 is corrupted";
return;
}
const char *argv0 = argv[0];
const char *argv0_suffix_maybe = strstr(argv0, ".test");
if (argv0_suffix_maybe != NULL
&& argv0_suffix_maybe[strlen(".test")] == '\0') {
bootstrap_errno = 0;
bootstrap_msg = "bootstrap is not enabled while testing";
return;
}
bool should_setns = true;
bool user_fstab = false;
const char *snap_name = NULL;
// Sanity check the command line arguments. The go parts will
// scan this too.
int i;
for (i = 1; i < argc; i++) {
const char *arg = argv[i];
if (arg[0] == '-') {
/* We have an option */
if (!strcmp(arg, "--from-snap-confine")) {
// When we are running under "--from-snap-confine"
// option skip the setns call as snap-confine has
// already placed us in the right namespace.
should_setns = false;
} else if (!strcmp(arg, "--user-mounts")) {
user_fstab = true;
// Processing the user-fstab file implies we're being
// called from snap-confine.
should_setns = false;
} else if (!strcmp(arg, "-u")) {
if (parse_arg_u(argc, argv, &i, uid_out)) {
return;
}
// Providing an user identifier implies we are performing an
// update of a specific user mount namespace and that we are
// invoked from snapd and we should setns ourselves. When
// invoked from snap-confine we are only called with
// --from-snap-confine and with --user-mounts.
should_setns = true;
user_fstab = true;
} else {
bootstrap_errno = 0;
bootstrap_msg = "unsupported option";
return;
}
} else {
// We expect a single positional argument: the snap name
if (snap_name != NULL) {
bootstrap_errno = 0;
bootstrap_msg = "too many positional arguments";
return;
}
snap_name = arg;
}
}
// If there's no snap name given, just bail out.
if (snap_name == NULL) {
bootstrap_errno = 0;
bootstrap_msg = "snap name not provided";
return;
}
// Ensure that the snap instance name is valid so that we don't blindly setns into
// something that is controlled by a potential attacker.
if (validate_instance_name(snap_name) < 0) {
bootstrap_errno = 0;
// bootstap_msg is set by validate_instance_name;
return;
}
// We have a valid snap name now so let's store it.
if (snap_name_out != NULL) {
*snap_name_out = snap_name;
}
if (should_setns_out != NULL) {
*should_setns_out = should_setns;
}
if (process_user_fstab != NULL) {
*process_user_fstab = user_fstab;
}
bootstrap_errno = 0;
bootstrap_msg = NULL;
}
// bootstrap prepares snap-update-ns to work in the namespace of the snap given
// on command line.
void bootstrap(int argc, char **argv, char **envp)
{
// We may have been started via a setuid-root snap-confine. In order to
// prevent environment-based attacks we start by erasing all environment
// variables.
char *snapd_debug = getenv("SNAPD_DEBUG");
if (clearenv() != 0) {
bootstrap_errno = 0;
bootstrap_msg = "bootstrap could not clear the environment";
return;
}
if (snapd_debug != NULL) {
setenv("SNAPD_DEBUG", snapd_debug, 0);
}
// Analyze the read process cmdline to find the snap name and decide if we
// should use setns to jump into the mount namespace of a particular snap.
// This is spread out for easier testability.
const char *snap_name = NULL;
bool should_setns = false;
bool process_user_fstab = false;
unsigned long uid = 0;
process_arguments(argc, argv, &snap_name, &should_setns,
&process_user_fstab, &uid);
if (process_user_fstab) {
switch_to_privileged_user();
// switch_to_privileged_user sets bootstrap_{errno,msg}
} else if (snap_name != NULL && should_setns) {
setns_into_snap(snap_name);
// setns_into_snap sets bootstrap_{errno,msg}
}
}
snapd-2.37.4~14.04.1/cmd/snap-update-ns/change_test.go 0000664 0000000 0000000 00000330343 13435556260 016764 0 ustar // -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package main_test
import (
"errors"
"os"
"strings"
"syscall"
. "gopkg.in/check.v1"
update "github.com/snapcore/snapd/cmd/snap-update-ns"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/testutil"
)
type changeSuite struct {
testutil.BaseTest
sys *testutil.SyscallRecorder
as *update.Assumptions
}
var (
errTesting = errors.New("testing")
)
var _ = Suite(&changeSuite{})
func (s *changeSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
// Mock and record system interactions.
s.sys = &testutil.SyscallRecorder{}
s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys))
s.as = &update.Assumptions{}
}
func (s *changeSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
s.sys.CheckForStrayDescriptors(c)
}
func (s *changeSuite) TestFakeFileInfo(c *C) {
c.Assert(testutil.FileInfoDir.IsDir(), Equals, true)
c.Assert(testutil.FileInfoFile.IsDir(), Equals, false)
c.Assert(testutil.FileInfoSymlink.IsDir(), Equals, false)
}
func (s *changeSuite) TestString(c *C) {
change := update.Change{
Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda1"},
Action: update.Mount,
}
c.Assert(change.String(), Equals, "mount (/dev/sda1 /a/b none defaults 0 0)")
}
// When there are no profiles we don't do anything.
func (s *changeSuite) TestNeededChangesNoProfiles(c *C) {
current := &osutil.MountProfile{}
desired := &osutil.MountProfile{}
changes := update.NeededChanges(current, desired)
c.Assert(changes, IsNil)
}
// When the profiles are the same we don't do anything.
func (s *changeSuite) TestNeededChangesNoChange(c *C) {
current := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}}
desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Keep},
})
}
// When the content interface is connected we should mount the new entry.
func (s *changeSuite) TestNeededChangesTrivialMount(c *C) {
current := &osutil.MountProfile{}
desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: desired.Entries[0], Action: update.Mount},
})
}
// When the content interface is disconnected we should unmount the mounted entry.
func (s *changeSuite) TestNeededChangesTrivialUnmount(c *C) {
current := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}}
desired := &osutil.MountProfile{}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: current.Entries[0], Action: update.Unmount},
})
}
// When umounting we unmount children before parents.
func (s *changeSuite) TestNeededChangesUnmountOrder(c *C) {
current := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/common/stuff/extra"},
{Dir: "/common/stuff"},
}}
desired := &osutil.MountProfile{}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Unmount},
{Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Unmount},
})
}
// When mounting we mount the parents before the children.
func (s *changeSuite) TestNeededChangesMountOrder(c *C) {
current := &osutil.MountProfile{}
desired := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/common/stuff/extra"},
{Dir: "/common/stuff"},
}}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Mount},
{Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount},
})
}
// When parent changes we don't reuse its children
func (s *changeSuite) TestNeededChangesChangedParentSameChild(c *C) {
current := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/common/stuff", Name: "/dev/sda1"},
{Dir: "/common/stuff/extra"},
{Dir: "/common/unrelated"},
}}
desired := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/common/stuff", Name: "/dev/sda2"},
{Dir: "/common/stuff/extra"},
{Dir: "/common/unrelated"},
}}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Keep},
{Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Unmount},
{Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Unmount},
{Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda2"}, Action: update.Mount},
{Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount},
})
}
// When child changes we don't touch the unchanged parent
func (s *changeSuite) TestNeededChangesSameParentChangedChild(c *C) {
current := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/common/stuff"},
{Dir: "/common/stuff/extra", Name: "/dev/sda1"},
{Dir: "/common/unrelated"},
}}
desired := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/common/stuff"},
{Dir: "/common/stuff/extra", Name: "/dev/sda2"},
{Dir: "/common/unrelated"},
}}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Keep},
{Entry: osutil.MountEntry{Dir: "/common/stuff/extra", Name: "/dev/sda1"}, Action: update.Unmount},
{Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Keep},
{Entry: osutil.MountEntry{Dir: "/common/stuff/extra", Name: "/dev/sda2"}, Action: update.Mount},
})
}
// Unused bind mount farms are unmounted.
func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUnused(c *C) {
current := &osutil.MountProfile{Entries: []osutil.MountEntry{{
// The tmpfs that lets us write into immutable squashfs. We mock
// x-snapd.needed-by to the last entry in the current profile (the bind
// mount). Mark it synthetic since it is a helper mount that is needed
// to facilitate the following mounts.
Name: "tmpfs",
Dir: "/snap/name/42/subdir",
Type: "tmpfs",
Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"},
}, {
// A bind mount to preserve a directory hidden by the tmpfs (the mount
// point is created elsewhere). We mock x-snapd.needed-by to the
// location of the bind mount below that is no longer desired.
Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing",
Dir: "/snap/name/42/subdir/existing",
Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"},
}, {
// A bind mount to put some content from another snap. The bind mount
// is nothing special but the fact that it is possible is the reason
// the two entries above exist. The mount point (created) is created
// elsewhere.
Name: "/snap/other/123/libs",
Dir: "/snap/name/42/subdir/created",
Options: []string{"bind", "ro"},
}}}
desired := &osutil.MountProfile{}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{
Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing",
Dir: "/snap/name/42/subdir/existing",
Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"},
}, Action: update.Unmount},
{Entry: osutil.MountEntry{
Name: "/snap/other/123/libs",
Dir: "/snap/name/42/subdir/created",
Options: []string{"bind", "ro"},
}, Action: update.Unmount},
{Entry: osutil.MountEntry{
Name: "tmpfs",
Dir: "/snap/name/42/subdir",
Type: "tmpfs",
Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"},
}, Action: update.Unmount},
})
}
func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUsed(c *C) {
// NOTE: the current profile is the same as in the test
// TestNeededChangesTmpfsBindMountFarmUnused written above.
current := &osutil.MountProfile{Entries: []osutil.MountEntry{{
Name: "tmpfs",
Dir: "/snap/name/42/subdir",
Type: "tmpfs",
Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"},
}, {
Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing",
Dir: "/snap/name/42/subdir/existing",
Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"},
}, {
Name: "/snap/other/123/libs",
Dir: "/snap/name/42/subdir/created",
Options: []string{"bind", "ro"},
}}}
desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{
// This is the only entry that we explicitly want but in order to
// support it we need to keep the remaining implicit entries.
Name: "/snap/other/123/libs",
Dir: "/snap/name/42/subdir/created",
Options: []string{"bind", "ro"},
}}}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{
Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing",
Dir: "/snap/name/42/subdir/existing",
Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"},
}, Action: update.Keep},
{Entry: osutil.MountEntry{
Name: "/snap/other/123/libs",
Dir: "/snap/name/42/subdir/created",
Options: []string{"bind", "ro"},
}, Action: update.Keep},
{Entry: osutil.MountEntry{
Name: "tmpfs",
Dir: "/snap/name/42/subdir",
Type: "tmpfs",
Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"},
}, Action: update.Keep},
})
}
// cur = ['/a/b', '/a/b-1', '/a/b-1/3', '/a/b/c']
// des = ['/a/b', '/a/b-1', '/a/b/c'
//
// We are smart about comparing entries as directories. Here even though "/a/b"
// is a prefix of "/a/b-1" it is correctly reused.
func (s *changeSuite) TestNeededChangesSmartEntryComparison(c *C) {
current := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/a/b", Name: "/dev/sda1"},
{Dir: "/a/b-1"},
{Dir: "/a/b-1/3"},
{Dir: "/a/b/c"},
}}
desired := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/a/b", Name: "/dev/sda2"},
{Dir: "/a/b-1"},
{Dir: "/a/b/c"},
}}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{Dir: "/a/b/c"}, Action: update.Unmount},
{Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda1"}, Action: update.Unmount},
{Entry: osutil.MountEntry{Dir: "/a/b-1/3"}, Action: update.Unmount},
{Entry: osutil.MountEntry{Dir: "/a/b-1"}, Action: update.Keep},
{Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda2"}, Action: update.Mount},
{Entry: osutil.MountEntry{Dir: "/a/b/c"}, Action: update.Mount},
})
}
// Parallel instance changes are executed first
func (s *changeSuite) TestNeededChangesParallelInstancesManyComeFirst(c *C) {
desired := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/common/stuff", Name: "/dev/sda1"},
{Dir: "/common/stuff/extra"},
{Dir: "/common/unrelated"},
{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}},
{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}},
}}
changes := update.NeededChanges(&osutil.MountProfile{}, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount},
{Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount},
{Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Mount},
{Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount},
{Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount},
})
}
// Parallel instance changes are kept if already present
func (s *changeSuite) TestNeededChangesParallelInstancesKeep(c *C) {
desired := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/common/stuff", Name: "/dev/sda1"},
{Dir: "/common/unrelated"},
{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}},
{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}},
}}
current := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}},
{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}},
}}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep},
{Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep},
{Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Mount},
{Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount},
})
}
// Parallel instance with mounts inside
func (s *changeSuite) TestNeededChangesParallelInstancesInsideMount(c *C) {
desired := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/foo/bar/baz"},
{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}},
{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}},
}}
current := &osutil.MountProfile{Entries: []osutil.MountEntry{
{Dir: "/foo/bar/zed"},
{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}},
{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}},
}}
changes := update.NeededChanges(current, desired)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: osutil.MountEntry{Dir: "/foo/bar/zed"}, Action: update.Unmount},
{Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep},
{Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep},
{Entry: osutil.MountEntry{Dir: "/foo/bar/baz"}, Action: update.Mount},
})
}
func mustReadProfile(profileStr string) *osutil.MountProfile {
profile, err := osutil.ReadMountProfile(strings.NewReader(profileStr))
if err != nil {
panic(err)
}
return profile
}
func (s *changeSuite) TestRuntimeUsingSymlinks(c *C) {
// We start with a runtime shared from one snap to another and then exposed
// to /opt with a symbolic link. This is the initial state of the
// application in version v1.
initial := mustReadProfile("")
desired_v1 := mustReadProfile(
"none /opt/runtime none x-snapd.kind=symlink,x-snapd.symlink=/snap/app/x1/runtime,x-snapd.origin=layout 0 0\n" +
"/snap/runtime/x1/opt/runtime /snap/app/x1/runtime none bind,ro 0 0\n")
// The changes we compute are trivial, simply perform each operation in order.
changes := update.NeededChanges(initial, desired_v1)
c.Assert(changes, DeepEquals, []*update.Change{
{Entry: desired_v1.Entries[0], Action: update.Mount},
{Entry: desired_v1.Entries[1], Action: update.Mount},
})
// After performing both changes we have a new synthesized entry. We get an
// extra writable mimic over /opt so that we can add our symlink. The
// content sharing into $SNAP is applied as expected since the snap ships
// the required mount point.
current_v1 := mustReadProfile(
"/snap/runtime/x1/opt/runtime /snap/app/x1/runtime none bind,ro 0 0\n" +
"none /opt/runtime none x-snapd.kind=symlink,x-snapd.symlink=/snap/app/x1/runtime,x-snapd.origin=layout 0 0\n" +
"tmpfs /opt tmpfs x-snapd.synthetic,x-snapd.needed-by=/opt/runtime,mode=0755,uid=0,gid=0 0")
// We now proceed to replace app v1 with v2 which uses a bind mount instead
// of a symlink. First, let's start with the updated desired profile:
desired_v2 := mustReadProfile(
"/snap/app/x2/runtime /opt/runtime none rbind,rw,x-snapd.origin=layout 0 0\n" +
"/snap/runtime/x1/opt/runtime /snap/app/x2/runtime none bind,ro 0 0\n")
// Let's see what the update algorithm thinks.
changes = update.NeededChanges(current_v1, desired_v2)
c.Assert(changes, DeepEquals, []*update.Change{
// We are dropping the content interface bind mount because app changed revision
{Entry: current_v1.Entries[0], Action: update.Unmount},
// We are also dropping the symlink we had in /opt/runtime
{Entry: current_v1.Entries[1], Action: update.Unmount},
// But, we are keeping the /opt tmpfs because we still want /opt/runtime to exist (neat!)
{Entry: current_v1.Entries[2], Action: update.Keep},
// We are adding a new bind mount for /opt/runtime
{Entry: desired_v2.Entries[0], Action: update.Mount},
// We also adding the updated path of the content interface (for revision x2)
{Entry: desired_v2.Entries[1], Action: update.Mount},
})
// After performing all those changes this is the profile we observe.
current_v2 := mustReadProfile(
"tmpfs /opt tmpfs x-snapd.synthetic,x-snapd.needed-by=/opt/runtime,mode=0755,uid=0,gid=0 0 0\n" +
"/snap/app/x2/runtime /opt/runtime none rbind,rw,x-snapd.origin=layout 0 0\n" +
"/snap/runtime/x1/opt/runtime /snap/app/x2/runtime none bind,ro 0 0\n")
// So far so good. To trigger the issue we now revert or refresh to v1
// again. Let's see what happens here. The desired profiles are already
// known so let's see what the algorithm thinks now.
changes = update.NeededChanges(current_v2, desired_v1)
c.Assert(changes, DeepEquals, []*update.Change{
// We are, again, dropping the content interface bind mount because app changed revision
{Entry: current_v2.Entries[2], Action: update.Unmount},
// We are also dropping the bind mount from /opt/runtime since we want a symlink instead
{Entry: current_v2.Entries[1], Action: update.Unmount},
// Again, we reuse the tmpfs.
{Entry: current_v2.Entries[0], Action: update.Keep},
// We are providing a symlink /opt/runtime -> to $SNAP/runtime.
{Entry: desired_v1.Entries[0], Action: update.Mount},
// We are bind mounting the runtime from another snap into $SNAP/runtime
{Entry: desired_v1.Entries[1], Action: update.Mount},
})
// The problem is that the tmpfs contains leftovers from the things we
// created and those prevent the execution of this mount profile.
}
// ########################################
// Topic: mounting & unmounting filesystems
// ########################################
// Change.Perform returns errors from os.Lstat (apart from ErrNotExist)
func (s *changeSuite) TestPerformFilesystemMountLstatError(c *C) {
s.sys.InsertFault(`lstat "/target"`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, E: errTesting},
})
}
// Change.Perform wants to mount a filesystem.
func (s *changeSuite) TestPerformFilesystemMount(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `mount "device" "/target" "type" 0 ""`},
})
}
// Change.Perform wants to mount a filesystem but it fails.
func (s *changeSuite) TestPerformFilesystemMountWithError(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
s.sys.InsertFault(`mount "device" "/target" "type" 0 ""`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, Equals, errTesting)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `mount "device" "/target" "type" 0 ""`, E: errTesting},
})
}
// Change.Perform wants to mount a filesystem but the mount point isn't there.
func (s *changeSuite) TestPerformFilesystemMountWithoutMountPoint(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "target" 0755`},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 0 0`},
{C: `close 4`},
{C: `close 3`},
{C: `mount "device" "/target" "type" 0 ""`},
})
}
// Change.Perform wants to create a filesystem but the mount point isn't there and cannot be created.
func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointWithErrors(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 3 "target" 0755`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot create directory "/target": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "target" 0755`, E: errTesting},
{C: `close 3`},
})
}
// Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only.
func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBase(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST)
s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try
s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS, nil) // works on 2nd try
s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755})
s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty.
s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT)
s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{})
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, DeepEquals, []*update.Change{
{Action: update.Mount, Entry: osutil.MountEntry{
Name: "tmpfs", Dir: "/rofs", Type: "tmpfs",
Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}},
},
})
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
// sniff mount target
{C: `lstat "/rofs/target"`, E: syscall.ENOENT},
// /rofs/target is missing, create it
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `mkdirat 4 "target" 0755`, E: syscall.EROFS},
{C: `close 4`},
// error, read only filesystem, create a mimic
{C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}},
{C: `readdir "/rofs"`, R: []os.FileInfo(nil)},
{C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "tmp" 0755`},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 0 0`},
{C: `mkdirat 4 ".snap" 0755`},
{C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5},
{C: `fchown 5 0 0`},
{C: `close 4`},
{C: `close 3`},
{C: `mkdirat 5 "rofs" 0755`},
{C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 5`},
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6},
{C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7},
{C: `fstat 7 `, R: syscall.Stat_t{}},
{C: `close 6`},
{C: `close 5`},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`},
{C: `close 7`},
{C: `close 4`},
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`},
{C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 5`},
{C: `close 4`},
{C: `close 3`},
{C: `fstatfs 6 `, R: syscall.Statfs_t{}},
{C: `remove "/tmp/.snap/rofs"`},
{C: `close 6`},
// mimic ready, re-try initial mkdir
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `mkdirat 4 "target" 0755`},
{C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 4`},
// mount the filesystem
{C: `mount "device" "/rofs/target" "type" 0 ""`},
})
}
// Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only and mimic fails during planning.
func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBaseErrorWhilePlanning(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST)
s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS)
s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT)
s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir)
s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755})
s.sys.InsertReadDirResult(`readdir "/rofs"`, nil)
s.sys.InsertFault(`readdir "/rofs"`, errTesting) // make the writable mimic fail
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot create writable mimic over "/rofs": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
// sniff mount target
{C: `lstat "/rofs/target"`, E: syscall.ENOENT},
// /rofs/target is missing, create it
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `mkdirat 4 "target" 0755`, E: syscall.EROFS},
{C: `close 4`},
// error, read only filesystem, create a mimic
{C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}},
{C: `readdir "/rofs"`, E: errTesting},
// cannot create mimic, that's it
})
}
// Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only and mimic fails during execution.
func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBaseErrorWhileExecuting(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST)
s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS)
s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT)
s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir)
s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755})
s.sys.InsertReadDirResult(`readdir "/rofs"`, nil)
s.sys.InsertFault(`mkdirat 4 ".snap" 0755`, errTesting) // make the writable mimic fail
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot create writable mimic over "/rofs": cannot create directory "/tmp/.snap": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
// sniff mount target
{C: `lstat "/rofs/target"`, E: syscall.ENOENT},
// /rofs/target is missing, create it
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `mkdirat 4 "target" 0755`, E: syscall.EROFS},
{C: `close 4`},
// error, read only filesystem, create a mimic
{C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}},
{C: `readdir "/rofs"`, R: []os.FileInfo(nil)},
{C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "tmp" 0755`},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 0 0`},
{C: `mkdirat 4 ".snap" 0755`, E: errTesting},
{C: `close 4`},
{C: `close 3`},
// cannot create mimic, that's it
})
}
// Change.Perform wants to mount a filesystem but there's a symlink in mount point.
func (s *changeSuite) TestPerformFilesystemMountWithSymlinkInMountPoint(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoSymlink},
})
}
// Change.Perform wants to mount a filesystem but there's a file in mount point.
func (s *changeSuite) TestPerformFilesystemMountWithFileInMountPoint(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoFile},
})
}
// Change.Perform wants to unmount a filesystem.
func (s *changeSuite) TestPerformFilesystemUnmount(c *C) {
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{})
chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `unmount "/target" UMOUNT_NOFOLLOW`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{}},
{C: `remove "/target"`},
{C: `close 4`},
})
c.Assert(synth, HasLen, 0)
}
// Change.Perform wants to detach a bind mount.
func (s *changeSuite) TestPerformFilesystemDetch(c *C) {
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{})
chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/something", Dir: "/target", Options: []string{"x-snapd.detach"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `unmount "/target" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{}},
{C: `remove "/target"`},
{C: `close 4`},
})
c.Assert(synth, HasLen, 0)
}
// Change.Perform wants to unmount a filesystem but it fails.
func (s *changeSuite) TestPerformFilesystemUnmountError(c *C) {
s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting)
chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}
synth, err := chg.Perform(s.as)
c.Assert(err, Equals, errTesting)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting},
})
c.Assert(synth, HasLen, 0)
}
// Change.Perform passes non-flag options to the kernel.
func (s *changeSuite) TestPerformFilesystemMountWithOptions(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"ro", "funky"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `mount "device" "/target" "type" MS_RDONLY "funky"`},
})
}
// Change.Perform doesn't pass snapd-specific options to the kernel.
func (s *changeSuite) TestPerformFilesystemMountWithSnapdSpecificOptions(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"ro", "x-snapd.funky"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `mount "device" "/target" "type" MS_RDONLY ""`},
})
}
// ###############################################
// Topic: bind-mounting and unmounting directories
// ###############################################
// Change.Perform wants to bind mount a directory but the target cannot be stat'ed.
func (s *changeSuite) TestPerformDirectoryBindMountTargetLstatError(c *C) {
s.sys.InsertFault(`lstat "/target"`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, E: errTesting},
})
}
// Change.Perform wants to bind mount a directory but the source cannot be stat'ed.
func (s *changeSuite) TestPerformDirectoryBindMountSourceLstatError(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
s.sys.InsertFault(`lstat "/source"`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot inspect "/source": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `lstat "/source"`, E: errTesting},
})
}
// Change.Perform wants to bind mount a directory.
func (s *changeSuite) TestPerformDirectoryBindMount(c *C) {
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir)
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `lstat "/source"`, R: testutil.FileInfoDir},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`},
{C: `close 5`},
{C: `close 4`},
})
}
// Change.Perform wants to bind mount a directory but it fails.
func (s *changeSuite) TestPerformDirectoryBindMountWithError(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
s.sys.InsertFault(`mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, Equals, errTesting)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `lstat "/source"`, R: testutil.FileInfoDir},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, E: errTesting},
{C: `close 5`},
{C: `close 4`},
})
}
// Change.Perform wants to bind mount a directory but the mount point isn't there.
func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPoint(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir)
s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "target" 0755`},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 0 0`},
{C: `close 4`},
{C: `close 3`},
{C: `lstat "/source"`, R: testutil.FileInfoDir},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`},
{C: `close 5`},
{C: `close 4`},
})
}
// Change.Perform wants to bind mount a directory but the mount source isn't there.
func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSource(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT)
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `lstat "/source"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "source" 0755`},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 0 0`},
{C: `close 4`},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`},
{C: `close 5`},
{C: `close 4`},
})
}
// Change.Perform wants to create a directory bind mount but the mount point isn't there and cannot be created.
func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPointWithErrors(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 3 "target" 0755`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot create directory "/target": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "target" 0755`, E: errTesting},
{C: `close 3`},
})
}
// Change.Perform wants to create a directory bind mount but the mount source isn't there and cannot be created.
func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceWithErrors(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 3 "source" 0755`, errTesting)
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot create directory "/source": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `lstat "/source"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "source" 0755`, E: errTesting},
{C: `close 3`},
})
}
// Change.Perform wants to bind mount a directory but the mount point isn't there and the parent is read-only.
func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPointAndReadOnlyBase(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST)
s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try
s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS, nil) // works on 2nd try
s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755})
s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty.
s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT)
s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir)
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{})
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/rofs/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, DeepEquals, []*update.Change{
{Action: update.Mount, Entry: osutil.MountEntry{
Name: "tmpfs", Dir: "/rofs", Type: "tmpfs",
Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}},
},
})
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
// sniff mount target
{C: `lstat "/rofs/target"`, E: syscall.ENOENT},
// /rofs/target is missing, create it
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `mkdirat 4 "target" 0755`, E: syscall.EROFS},
{C: `close 4`},
// error, read only filesystem, create a mimic
{C: `lstat "/rofs" `, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}},
{C: `readdir "/rofs"`, R: []os.FileInfo(nil)},
{C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "tmp" 0755`},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 0 0`},
{C: `mkdirat 4 ".snap" 0755`},
{C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5},
{C: `fchown 5 0 0`},
{C: `close 4`},
{C: `close 3`},
{C: `mkdirat 5 "rofs" 0755`},
{C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 5`},
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6},
{C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7},
{C: `fstat 7 `, R: syscall.Stat_t{}},
{C: `close 6`},
{C: `close 5`},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`},
{C: `close 7`},
{C: `close 4`},
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`},
{C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 5`},
{C: `close 4`},
{C: `close 3`},
{C: `fstatfs 6 `, R: syscall.Statfs_t{}},
{C: `remove "/tmp/.snap/rofs"`},
{C: `close 6`},
// mimic ready, re-try initial mkdir
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `mkdirat 4 "target" 0755`},
{C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 4`},
// sniff mount source
{C: `lstat "/source"`, R: testutil.FileInfoDir},
// mount the filesystem
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 5`},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/6" "" MS_BIND ""`},
{C: `close 6`},
{C: `close 4`},
})
}
// Change.Perform wants to bind mount a directory but the mount source isn't there and the parent is read-only.
func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceAndReadOnlyBase(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir)
s.sys.InsertFault(`lstat "/rofs/source"`, syscall.ENOENT)
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST)
s.sys.InsertFault(`mkdirat 4 "source" 0755`, syscall.EROFS)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/rofs/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `lstat "/rofs/source"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `mkdirat 4 "source" 0755`, E: syscall.EROFS},
{C: `close 4`},
})
}
// Change.Perform wants to bind mount a directory but the mount source isn't there and the parent is read-only but this is for a layout.
func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceAndReadOnlyBaseForLayout(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
s.sys.InsertFault(`lstat "/rofs/source"`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST)
s.sys.InsertFault(`openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try
s.sys.InsertFault(`mkdirat 4 "source" 0755`, syscall.EROFS, nil) // works on 2nd try
s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty.
s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT)
s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir)
s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{})
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/rofs/source", Dir: "/target", Options: []string{"bind", "x-snapd.origin=layout"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Check(synth, DeepEquals, []*update.Change{
{Action: update.Mount, Entry: osutil.MountEntry{
Name: "tmpfs", Dir: "/rofs", Type: "tmpfs",
Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/source", "mode=0755", "uid=0", "gid=0"}},
},
})
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
// sniff mount target and source
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `lstat "/rofs/source"`, E: syscall.ENOENT},
// /rofs/source is missing, create it
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `mkdirat 4 "source" 0755`, E: syscall.EROFS},
{C: `close 4`},
// error /rofs is a read-only filesystem, create a mimic
{C: `lstat "/rofs" `, R: syscall.Stat_t{Mode: 0755}},
{C: `readdir "/rofs"`, R: []os.FileInfo(nil)},
{C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "tmp" 0755`},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 0 0`},
{C: `mkdirat 4 ".snap" 0755`},
{C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5},
{C: `fchown 5 0 0`},
{C: `close 4`},
{C: `close 3`},
{C: `mkdirat 5 "rofs" 0755`},
{C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 5`},
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6},
{C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7},
{C: `fstat 7 `, R: syscall.Stat_t{}},
{C: `close 6`},
{C: `close 5`},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`},
{C: `close 7`},
{C: `close 4`},
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`},
{C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 5`},
{C: `close 4`},
{C: `close 3`},
{C: `fstatfs 6 `, R: syscall.Statfs_t{}},
{C: `remove "/tmp/.snap/rofs"`},
{C: `close 6`},
// /rofs/source was missing (we checked earlier), create it
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `mkdirat 4 "source" 0755`},
{C: `openat 4 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 4`},
// bind mount /rofs/source -> /target
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 4`},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `mount "/proc/self/fd/5" "/proc/self/fd/4" "" MS_BIND ""`},
{C: `close 4`},
{C: `close 5`},
})
}
// Change.Perform wants to bind mount a directory but there's a symlink in mount point.
func (s *changeSuite) TestPerformDirectoryBindMountWithSymlinkInMountPoint(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoSymlink},
})
}
// Change.Perform wants to bind mount a directory but there's a file in mount mount.
func (s *changeSuite) TestPerformDirectoryBindMountWithFileInMountPoint(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoFile},
})
}
// Change.Perform wants to bind mount a directory but there's a symlink in source.
func (s *changeSuite) TestPerformDirectoryBindMountWithSymlinkInMountSource(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoSymlink)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a directory`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `lstat "/source"`, R: testutil.FileInfoSymlink},
})
}
// Change.Perform wants to bind mount a directory but there's a file in source.
func (s *changeSuite) TestPerformDirectoryBindMountWithFileInMountSource(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a directory`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `lstat "/source"`, R: testutil.FileInfoFile},
})
}
// Change.Perform wants to unmount a directory bind mount.
func (s *changeSuite) TestPerformDirectoryBindUnmount(c *C) {
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{})
chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `unmount "/target" UMOUNT_NOFOLLOW`},
// Perform clean up after the unmount operation.
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{}},
{C: `remove "/target"`},
{C: `close 4`},
})
c.Assert(synth, HasLen, 0)
}
// Change.Perform wants to unmount a directory bind mount but it fails.
func (s *changeSuite) TestPerformDirectoryBindUnmountError(c *C) {
s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting)
chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, Equals, errTesting)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting},
})
c.Assert(synth, HasLen, 0)
}
// #########################################
// Topic: bind-mounting and unmounting files
// #########################################
// Change.Perform wants to bind mount a file but the target cannot be stat'ed.
func (s *changeSuite) TestPerformFileBindMountTargetLstatError(c *C) {
s.sys.InsertFault(`lstat "/target"`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, E: errTesting},
})
}
// Change.Perform wants to bind mount a file but the source cannot be stat'ed.
func (s *changeSuite) TestPerformFileBindMountSourceLstatError(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile)
s.sys.InsertFault(`lstat "/source"`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot inspect "/source": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoFile},
{C: `lstat "/source"`, E: errTesting},
})
}
// Change.Perform wants to bind mount a file.
func (s *changeSuite) TestPerformFileBindMount(c *C) {
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile)
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoFile},
{C: `lstat "/source"`, R: testutil.FileInfoFile},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`},
{C: `close 5`},
{C: `close 4`},
})
}
// Change.Perform wants to bind mount a file but it fails.
func (s *changeSuite) TestPerformFileBindMountWithError(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile)
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
s.sys.InsertFault(`mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, Equals, errTesting)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoFile},
{C: `lstat "/source"`, R: testutil.FileInfoFile},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, E: errTesting},
{C: `close 5`},
{C: `close 4`},
})
}
// Change.Perform wants to bind mount a file but the mount point isn't there.
func (s *changeSuite) TestPerformFileBindMountWithoutMountPoint(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile)
s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4},
{C: `fchown 4 0 0`},
{C: `close 4`},
{C: `close 3`},
{C: `lstat "/source"`, R: testutil.FileInfoFile},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`},
{C: `close 5`},
{C: `close 4`},
})
}
// Change.Perform wants to create a directory bind mount but the mount point isn't there and cannot be created.
func (s *changeSuite) TestPerformFileBindMountWithoutMountPointWithErrors(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT)
s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot open file "/target": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: errTesting},
{C: `close 3`},
})
}
// Change.Perform wants to bind mount a file but the mount source isn't there.
func (s *changeSuite) TestPerformFileBindMountWithoutMountSource(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT)
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 5 `, syscall.Stat_t{})
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoFile},
{C: `lstat "/source"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4},
{C: `fchown 4 0 0`},
{C: `close 4`},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5},
{C: `fstat 5 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`},
{C: `close 5`},
{C: `close 4`},
})
}
// Change.Perform wants to create a file bind mount but the mount source isn't there and cannot be created.
func (s *changeSuite) TestPerformFileBindMountWithoutMountSourceWithErrors(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT)
s.sys.InsertFault(`openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting)
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot open file "/source": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoFile},
{C: `lstat "/source"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: errTesting},
{C: `close 3`},
})
}
// Change.Perform wants to bind mount a file but the mount point isn't there and the parent is read-only.
func (s *changeSuite) TestPerformFileBindMountWithoutMountPointAndReadOnlyBase(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST)
s.sys.InsertFault(`openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EROFS, nil) // works on 2nd try
s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755})
s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty.
s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT)
s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir)
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{})
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/rofs/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, DeepEquals, []*update.Change{
{Action: update.Mount, Entry: osutil.MountEntry{
Name: "tmpfs", Dir: "/rofs", Type: "tmpfs",
Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}},
},
})
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
// sniff mount target
{C: `lstat "/rofs/target"`, E: syscall.ENOENT},
// /rofs/target is missing, create it
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EROFS},
{C: `close 4`},
// error, read only filesystem, create a mimic
{C: `lstat "/rofs" `, R: syscall.Stat_t{Mode: 0755}},
{C: `readdir "/rofs"`, R: []os.FileInfo(nil)},
{C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "tmp" 0755`},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 0 0`},
{C: `mkdirat 4 ".snap" 0755`},
{C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5},
{C: `fchown 5 0 0`},
{C: `close 4`},
{C: `close 3`},
{C: `mkdirat 5 "rofs" 0755`},
{C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 5`},
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6},
{C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7},
{C: `fstat 7 `, R: syscall.Stat_t{}},
{C: `close 6`},
{C: `close 5`},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`},
{C: `close 7`},
{C: `close 4`},
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`},
{C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 5`},
{C: `close 4`},
{C: `close 3`},
{C: `fstatfs 6 `, R: syscall.Statfs_t{}},
{C: `remove "/tmp/.snap/rofs"`},
{C: `close 6`},
// mimic ready, re-try initial mkdir
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
{C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 4`},
// sniff mount source
{C: `lstat "/source"`, R: testutil.FileInfoFile},
// mount the filesystem
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 5`},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/6" "" MS_BIND ""`},
{C: `close 6`},
{C: `close 4`},
})
}
// Change.Perform wants to bind mount a file but there's a symlink in mount point.
func (s *changeSuite) TestPerformFileBindMountWithSymlinkInMountPoint(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a regular file`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoSymlink},
})
}
// Change.Perform wants to bind mount a file but there's a directory in mount point.
func (s *changeSuite) TestPerformBindMountFileWithDirectoryInMountPoint(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a regular file`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
})
}
// Change.Perform wants to bind mount a file but there's a symlink in source.
func (s *changeSuite) TestPerformFileBindMountWithSymlinkInMountSource(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile)
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoSymlink)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a regular file`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoFile},
{C: `lstat "/source"`, R: testutil.FileInfoSymlink},
})
}
// Change.Perform wants to bind mount a file but there's a directory in source.
func (s *changeSuite) TestPerformFileBindMountWithDirectoryInMountSource(c *C) {
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile)
s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a regular file`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoFile},
{C: `lstat "/source"`, R: testutil.FileInfoDir},
})
}
// Change.Perform wants to unmount a file bind mount made on empty squashfs placeholder.
func (s *changeSuite) TestPerformFileBindUnmountOnSquashfs(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.SquashfsMagic})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 0})
chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `unmount "/target" UMOUNT_NOFOLLOW`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}},
{C: `close 4`},
})
c.Assert(synth, HasLen, 0)
}
// Change.Perform wants to unmount a file bind mount made on non-empty ext4 placeholder.
func (s *changeSuite) TestPerformFileBindUnmountOnExt4NonEmpty(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.Ext4Magic})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 1})
chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `unmount "/target" UMOUNT_NOFOLLOW`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{Size: 1}},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.Ext4Magic}},
{C: `fstat 4 `, R: syscall.Stat_t{Size: 1}},
{C: `close 4`},
})
c.Assert(synth, HasLen, 0)
}
// Change.Perform wants to unmount a file bind mount made on empty tmpfs placeholder.
func (s *changeSuite) TestPerformFileBindUnmountOnTmpfsEmpty(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 0})
chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `unmount "/target" UMOUNT_NOFOLLOW`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{Size: 0}},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}},
{C: `fstat 4 `, R: syscall.Stat_t{Size: 0}},
{C: `remove "/target"`},
{C: `close 4`},
})
c.Assert(synth, HasLen, 0)
}
// Change.Perform wants to unmount a file bind mount made on empty tmpfs placeholder but it is busy!.
func (s *changeSuite) TestPerformFileBindUnmountOnTmpfsEmptyButBusy(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic})
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 0})
s.sys.InsertFault(`remove "/target"`, syscall.EBUSY)
chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, "device or resource busy")
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `unmount "/target" UMOUNT_NOFOLLOW`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{Size: 0}},
{C: `close 3`},
{C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}},
{C: `fstat 4 `, R: syscall.Stat_t{Size: 0}},
{C: `remove "/target"`, E: syscall.EBUSY},
{C: `close 4`},
})
c.Assert(synth, HasLen, 0)
}
// Change.Perform wants to unmount a file bind mount but it fails.
func (s *changeSuite) TestPerformFileBindUnmountError(c *C) {
s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting)
chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, Equals, errTesting)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting},
})
c.Assert(synth, HasLen, 0)
}
// #############################################################
// Topic: handling mounts with the x-snapd.ignore-missing option
// #############################################################
func (s *changeSuite) TestPerformMountWithIgnoredMissingMountSource(c *C) {
s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT)
s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.ignore-missing"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, Equals, update.ErrIgnoredMissingMount)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, R: testutil.FileInfoDir},
{C: `lstat "/source"`, E: syscall.ENOENT},
})
}
func (s *changeSuite) TestPerformMountWithIgnoredMissingMountPoint(c *C) {
s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.ignore-missing"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, Equals, update.ErrIgnoredMissingMount)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/target"`, E: syscall.ENOENT},
})
}
// ########################
// Topic: creating symlinks
// ########################
// Change.Perform wants to create a symlink but name cannot be stat'ed.
func (s *changeSuite) TestPerformCreateSymlinkNameLstatError(c *C) {
s.sys.InsertFault(`lstat "/name"`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot inspect "/name": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/name"`, E: errTesting},
})
}
// Change.Perform wants to create a symlink.
func (s *changeSuite) TestPerformCreateSymlink(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/name"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `symlinkat "/oldname" 3 "name"`},
{C: `close 3`},
})
}
// Change.Perform wants to create a symlink but it fails.
func (s *changeSuite) TestPerformCreateSymlinkWithError(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT)
s.sys.InsertFault(`symlinkat "/oldname" 3 "name"`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot create symlink "/name": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/name"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `symlinkat "/oldname" 3 "name"`, E: errTesting},
{C: `close 3`},
})
}
// Change.Perform wants to create a symlink but the target is empty.
func (s *changeSuite) TestPerformCreateSymlinkWithNoTargetError(c *C) {
s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink="}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot create symlink with empty target: "/name"`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/name"`, E: syscall.ENOENT},
})
}
// Change.Perform wants to create a symlink but the base directory isn't there.
func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDir(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/base/name"`, syscall.ENOENT)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/base/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/base/name"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "base" 0755`},
{C: `openat 3 "base" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 0 0`},
{C: `close 3`},
{C: `symlinkat "/oldname" 4 "name"`},
{C: `close 4`},
})
}
// Change.Perform wants to create a symlink but the base directory isn't there and cannot be created.
func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDirWithErrors(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/base/name"`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 3 "base" 0755`, errTesting)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/base/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot create directory "/base": testing`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/base/name"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "base" 0755`, E: errTesting},
{C: `close 3`},
})
}
// Change.Perform wants to create a symlink but the base directory isn't there and the parent is read-only.
func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDirAndReadOnlyBase(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertFault(`lstat "/rofs/name"`, syscall.ENOENT)
s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST)
s.sys.InsertFault(`symlinkat "/oldname" 4 "name"`, syscall.EROFS, nil) // works on 2nd try
s.sys.InsertSysLstatResult(`lstat "/rofs" `, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755})
s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty.
s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT)
s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 7 `, syscall.Stat_t{})
s.sys.InsertFstatResult(`fstat 6 `, syscall.Stat_t{})
s.sys.InsertFstatfsResult(`fstatfs 6 `, syscall.Statfs_t{})
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/rofs/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, DeepEquals, []*update.Change{
{Action: update.Mount, Entry: osutil.MountEntry{
Name: "tmpfs", Dir: "/rofs", Type: "tmpfs",
Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/name", "mode=0755", "uid=0", "gid=0"}},
},
})
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
// sniff symlink name
{C: `lstat "/rofs/name"`, E: syscall.ENOENT},
// create base name (/rofs)
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
// create symlink
{C: `symlinkat "/oldname" 4 "name"`, E: syscall.EROFS},
{C: `close 4`},
// error, read only filesystem, create a mimic
{C: `lstat "/rofs" `, R: syscall.Stat_t{Mode: 0755}},
{C: `readdir "/rofs"`, R: []os.FileInfo(nil)},
{C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "tmp" 0755`},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `fchown 4 0 0`},
{C: `mkdirat 4 ".snap" 0755`},
{C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5},
{C: `fchown 5 0 0`},
{C: `close 4`},
{C: `close 3`},
{C: `mkdirat 5 "rofs" 0755`},
{C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `fchown 3 0 0`},
{C: `close 3`},
{C: `close 5`},
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4 `, R: syscall.Stat_t{}},
{C: `close 3`},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6},
{C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7},
{C: `fstat 7 `, R: syscall.Stat_t{}},
{C: `close 6`},
{C: `close 5`},
{C: `close 3`},
{C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`},
{C: `close 7`},
{C: `close 4`},
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`},
{C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3},
{C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4},
{C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5},
{C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6},
{C: `fstat 6 `, R: syscall.Stat_t{}},
{C: `close 5`},
{C: `close 4`},
{C: `close 3`},
{C: `fstatfs 6 `, R: syscall.Statfs_t{}},
{C: `remove "/tmp/.snap/rofs"`},
{C: `close 6`},
// mimic ready, re-try initial base mkdir
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST},
{C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
{C: `close 3`},
// create symlink
{C: `symlinkat "/oldname" 4 "name"`},
{C: `close 4`},
})
}
// Change.Perform wants to create a symlink but there's a file in the way.
func (s *changeSuite) TestPerformCreateSymlinkWithFileInTheWay(c *C) {
s.sys.InsertOsLstatResult(`lstat "/name"`, testutil.FileInfoFile)
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, ErrorMatches, `cannot create symlink in "/name": existing file in the way`)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/name"`, R: testutil.FileInfoFile},
})
}
// Change.Perform wants to create a symlink but a correct symlink is already present.
func (s *changeSuite) TestPerformCreateSymlinkWithGoodSymlinkPresent(c *C) {
defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted.
s.sys.InsertOsLstatResult(`lstat "/name"`, testutil.FileInfoSymlink)
s.sys.InsertFault(`symlinkat "/oldname" 3 "name"`, syscall.EEXIST)
s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Mode: syscall.S_IFLNK})
s.sys.InsertReadlinkatResult(`readlinkat 4 "" `, "/oldname")
chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}}
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(synth, HasLen, 0)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
{C: `lstat "/name"`, R: testutil.FileInfoSymlink},
{C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
{C: `symlinkat "/oldname" 3 "name"`, E: syscall.EEXIST},
{C: `openat 3 "name" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4},
{C: `fstat 4