blob: 719a4d2eff6e776772529f4ade8374a6bb7f5b99 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package crosdisks provides a series of tests to verify CrosDisks'
// D-Bus API behavior.
package crosdisks
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"chromiumos/tast/errors"
"chromiumos/tast/local/crosdisks"
"chromiumos/tast/testing"
)
// PreparedArchives is a list of data files used in the test.
var PreparedArchives = []string{
"crosdisks/Duplicate Filenames.zip",
"crosdisks/Encrypted Full V4.rar",
"crosdisks/Encrypted Full V5.rar",
"crosdisks/Encrypted Partial V4.rar",
"crosdisks/Encrypted Partial V5.rar",
"crosdisks/Encrypted AES-128.zip",
"crosdisks/Encrypted AES-192.zip",
"crosdisks/Encrypted AES-256.zip",
"crosdisks/Encrypted ZipCrypto.zip",
"crosdisks/Encrypted Various.zip",
"crosdisks/Invalid.rar",
"crosdisks/Invalid.zip",
"crosdisks/Format V4.rar",
"crosdisks/Format V5.rar",
"crosdisks/Multipart Old Style.rar",
"crosdisks/Multipart Old Style.r00",
"crosdisks/Multipart New Style 01.rar",
"crosdisks/Multipart New Style 02.rar",
"crosdisks/Multipart New Style 03.rar",
"crosdisks/Nested.rar",
"crosdisks/Nested.zip",
"crosdisks/Strict Password.zip",
"crosdisks/Symlinks.zip",
"crosdisks/Unicode.zip",
"crosdisks/MacOS UTF-8 Bug 903664.zip",
"crosdisks/SJIS Bug 846195.zip",
}
func withMountedArchiveDo(ctx context.Context, cd *crosdisks.CrosDisks, archivePath, password string, f func(ctx context.Context, mountPath string) error) error {
options := ""
if password != "" {
options = "password=" + password
}
return withMountDo(ctx, cd, archivePath, filepath.Ext(archivePath), options, f)
}
func verifyArchiveContent(ctx context.Context, cd *crosdisks.CrosDisks, archivePath, password string, expectedContent DirectoryContents) error {
return withMountedArchiveDo(ctx, cd, archivePath, password, func(ctx context.Context, mountPath string) error {
return verifyDirectoryContents(ctx, mountPath, expectedContent)
})
}
func verifyEncryptedArchiveContent(ctx context.Context, cd *crosdisks.CrosDisks, archivePath, password string, expectedContent DirectoryContents) error {
// Check that it fails without a password.
if err := verifyMountStatus(ctx, cd, archivePath, filepath.Ext(archivePath), "", crosdisks.MountErrorNeedPassword); err != nil {
return errors.Wrap(err, "verification failed for encrypted archive without password")
}
// Check that it fails with a wrong password.
for _, pw := range []string{"", password + "foo", password[0 : len(password)-1]} {
if err := verifyMountStatus(ctx, cd, archivePath, filepath.Ext(archivePath), "password="+pw, crosdisks.MountErrorNeedPassword); err != nil {
return errors.Wrap(err, "verification failed for encrypted archive with incorrect password")
}
}
return verifyArchiveContent(ctx, cd, archivePath, password, expectedContent)
}
func testInvalidArchives(ctx context.Context, s *testing.State, cd *crosdisks.CrosDisks, dataDir string) {
for _, f := range []string{
"Invalid.rar",
"Invalid.zip",
"Not There.rar",
"Not There.zip",
} {
if err := verifyMountStatus(ctx, cd, filepath.Join(dataDir, f), filepath.Ext(f), "", crosdisks.MountErrorMountProgramFailed); err != nil {
s.Errorf("Unexpected status of mounting invalid archive %q: %v", f, err)
}
}
}
func testMultipartArchives(ctx context.Context, s *testing.State, cd *crosdisks.CrosDisks, dataDir string) {
data := make([]string, 201)
for i := 0; i < 200; i++ {
data[i] = fmt.Sprintf("Line %03d", i+1)
}
expectedContent := DirectoryContents{
"Lines": {Data: []byte(strings.Join(data, "\n"))},
}
for _, archive := range []string{
"Multipart Old Style.rar",
"Multipart New Style 01.rar",
"Multipart New Style 02.rar",
"Multipart New Style 03.rar",
} {
if err := verifyArchiveContent(ctx, cd, filepath.Join(dataDir, archive), "", expectedContent); err != nil {
s.Errorf("Test failed for %q: %v", archive, err)
}
}
}
func testNestedArchives(ctx context.Context, s *testing.State, cd *crosdisks.CrosDisks, dataDir string) {
for _, archive := range []string{"Nested.rar", "Nested.zip"} {
expectedMountPath := filepath.Join("/media/archive", archive)
if err := withMountedArchiveDo(ctx, cd, filepath.Join(dataDir, archive), "", func(ctx context.Context, mountPath string) error {
if mountPath != expectedMountPath {
return errors.Errorf("mount path is different from expected one: got %q, want %q", mountPath, expectedMountPath)
}
return verifyUnicodeArchives(ctx, cd, mountPath)
}); err != nil {
s.Errorf("Test failed for %q: %v", archive, err)
}
}
}
func verifyUnicodeArchives(ctx context.Context, cd *crosdisks.CrosDisks, archiveDir string) error {
// Test RAR V4 with Unicode BMP characters in file and directory names.
expectedContent := DirectoryContents{
"File D79F \uD79F.txt": {Data: []byte("Char U+D79F is \uD79F HANGUL SYLLABLE HIC\n")},
" Space Oddity ": {Data: []byte("Mind the gap\n")},
"Level 1/Empty": {Data: []byte{}},
"Level 1/Digits": {Data: []byte("0123456789")},
"Level 1/Small": {Data: []byte("Small file\n")},
"Level 1/Level 2/Big": {Data: []byte(strings.Repeat("a", 65536))},
}
if err := verifyArchiveContent(ctx, cd, filepath.Join(archiveDir, "Format V4.rar"), "", expectedContent); err != nil {
return err
}
// Test RAR v5 and ZIP with both Unicode BMP and non-BMP characters in file and directory names.
expectedContent["Dir 1F601 \U0001F601/File 1F602 \U0001F602.txt"] = FileItem{Data: []byte("Char U+1F602 is \U0001F602 FACE WITH TEARS OF JOY\n")}
expectedContent["File 1F600 \U0001F600.txt"] = FileItem{Data: []byte("Char U+1F600 is \U0001F600 GRINNING FACE\n")}
if err := verifyArchiveContent(ctx, cd, filepath.Join(archiveDir, "Format V5.rar"), "", expectedContent); err != nil {
return err
}
if err := verifyArchiveContent(ctx, cd, filepath.Join(archiveDir, "Unicode.zip"), "", expectedContent); err != nil {
return err
}
return nil
}
func testUnicodeFilenamesInArchives(ctx context.Context, s *testing.State, cd *crosdisks.CrosDisks, dataDir string) {
if err := verifyUnicodeArchives(ctx, cd, dataDir); err != nil {
s.Error("Test failed: ", err)
}
}
func testMacOSUTF8InArchives(ctx context.Context, s *testing.State, cd *crosdisks.CrosDisks, dataDir string) {
expectedContent := DirectoryContents{
"ファイル.dat": {1541735375, []byte("This is a file.\n")},
"日本語フォルダ/新しいテキストドキュメント.txt": {1541735341, []byte("新しいテキストドキュメントです。\n")},
}
if err := verifyArchiveContent(ctx, cd, filepath.Join(dataDir, "MacOS UTF-8 Bug 903664.zip"), "", expectedContent); err != nil {
s.Error("Test failed: ", err)
}
}
// testSJISInArchives tests that filenames encoded in Shift JIS are correctly detected and converted to UTF-8.
// https://crbug.com/846195
// https://crbug.com/834544
func testSJISInArchives(ctx context.Context, s *testing.State, cd *crosdisks.CrosDisks, dataDir string) {
expectedContent := DirectoryContents{
"新しいフォルダ/SJIS_835C_ソ.txt": {Mtime: 347068800},
"新しいフォルダ/新しいテキスト ドキュメント.txt": {Mtime: 1002026088},
}
if err := verifyArchiveContent(ctx, cd, filepath.Join(dataDir, "SJIS Bug 846195.zip"), "", expectedContent); err != nil {
s.Error("Test failed: ", err)
}
}
func testSymlinksDisabledInArchives(ctx context.Context, s *testing.State, cd *crosdisks.CrosDisks, dataDir string) {
expectedContent := DirectoryContents{
"textfile": {1357584423, []byte("sample text\n")},
}
if err := verifyArchiveContent(ctx, cd, filepath.Join(dataDir, "Symlinks.zip"), "", expectedContent); err != nil {
s.Error("Test failed: ", err)
}
}
func testUniformEncryptionInArchives(ctx context.Context, s *testing.State, cd *crosdisks.CrosDisks, dataDir string) {
expectedContent := DirectoryContents{
"Secret.txt": {Data: []byte("This is my little secret\n")},
}
for _, archive := range []string{
"Encrypted Full V4.rar",
"Encrypted Full V5.rar",
"Encrypted Partial V4.rar",
"Encrypted Partial V5.rar",
"Encrypted AES-128.zip",
"Encrypted AES-192.zip",
"Encrypted AES-256.zip",
"Encrypted ZipCrypto.zip",
} {
archivePath := filepath.Join(dataDir, archive)
if err := verifyEncryptedArchiveContent(ctx, cd, archivePath, "password", expectedContent); err != nil {
s.Errorf("Test failed for %q: %v", archivePath, err)
}
}
}
func testMixedEncryptioninArchives(ctx context.Context, s *testing.State, cd *crosdisks.CrosDisks, dataDir string) {
archivePath := filepath.Join(dataDir, "Encrypted Various.zip")
expectedContent := DirectoryContents{
"ClearText.txt": {1598592138, []byte("This is not encrypted.\n")},
"Encrypted AES-128.txt": {1598592200, []byte("This is encrypted with AES-128.\n")},
"Encrypted AES-192.txt": {1598592206, []byte("This is encrypted with AES-192.\n")},
"Encrypted AES-256.txt": {1598592213, []byte("This is encrypted with AES-256.\n")},
"Encrypted ZipCrypto.txt": {1598592187, []byte("This is encrypted with ZipCrypto.\n")},
}
if err := verifyEncryptedArchiveContent(ctx, cd, archivePath, "password", expectedContent); err != nil {
s.Errorf("Test failed for %q: %v", archivePath, err)
}
}
// testStrictPasswordInArchives checks that invalid password is not accidentally accepted. https://crbug.com/1127752
func testStrictPasswordInArchives(ctx context.Context, s *testing.State, cd *crosdisks.CrosDisks, dataDir string) {
archivePath := filepath.Join(dataDir, "Strict Password.zip")
if err := verifyMountStatus(ctx, cd, archivePath, filepath.Ext(archivePath), "password=sample", crosdisks.MountErrorNeedPassword); err != nil {
s.Errorf("Test failed for %q: %v", archivePath, err)
}
}
func testDuplicateFilenamesInArchives(ctx context.Context, s *testing.State, cd *crosdisks.CrosDisks, dataDir string) {
mtime := int64(1600602814)
expectedContent := DirectoryContents{
"Simple.txt": {mtime, []byte("Simple 1\n")},
"Simple (1).txt": {mtime, []byte("Simple 2 \n")},
"Simple (2).txt": {mtime, []byte("Simple 3 \n")},
"Suspense...": {mtime, []byte("Suspense 1\n")},
"Suspense... (1)": {mtime, []byte("Suspense 2 \n")},
"Suspense... (2)": {mtime, []byte("Suspense 3 \n")},
"No Dot": {mtime, []byte("No Dot 1\n")},
"No Dot (1)": {mtime, []byte("No Dot 2 \n")},
"No Dot (2)": {mtime, []byte("No Dot 3 \n")},
".Hidden": {mtime, []byte("Hidden 1\n")},
".Hidden (1)": {mtime, []byte("Hidden 2 \n")},
".Hidden (2)": {mtime, []byte("Hidden 3 \n")},
"Folder/Simple.txt": {mtime, []byte("Simple 1\n")},
"Folder/Simple (1).txt": {mtime, []byte("Simple 2 \n")},
"Folder/Simple (2).txt": {mtime, []byte("Simple 3 \n")},
"Folder/Suspense...": {mtime, []byte("Suspense 1\n")},
"Folder/Suspense... (1)": {mtime, []byte("Suspense 2 \n")},
"Folder/Suspense... (2)": {mtime, []byte("Suspense 3 \n")},
"Folder/No Dot": {mtime, []byte("No Dot 1\n")},
"Folder/No Dot (1)": {mtime, []byte("No Dot 2 \n")},
"Folder/No Dot (2)": {mtime, []byte("No Dot 3 \n")},
"Folder/.Hidden": {mtime, []byte("Hidden 1\n")},
"Folder/.Hidden (1)": {mtime, []byte("Hidden 2 \n")},
"Folder/.Hidden (2)": {mtime, []byte("Hidden 3 \n")},
"With.Dot/Simple.txt": {mtime, []byte("Simple 1\n")},
"With.Dot/Simple (1).txt": {mtime, []byte("Simple 2 \n")},
"With.Dot/Simple (2).txt": {mtime, []byte("Simple 3 \n")},
"With.Dot/Suspense...": {mtime, []byte("Suspense 1\n")},
"With.Dot/Suspense... (1)": {mtime, []byte("Suspense 2 \n")},
"With.Dot/Suspense... (2)": {mtime, []byte("Suspense 3 \n")},
"With.Dot/No Dot": {mtime, []byte("No Dot 1\n")},
"With.Dot/No Dot (1)": {mtime, []byte("No Dot 2 \n")},
"With.Dot/No Dot (2)": {mtime, []byte("No Dot 3 \n")},
"With.Dot/.Hidden": {mtime, []byte("Hidden 1\n")},
"With.Dot/.Hidden (1)": {mtime, []byte("Hidden 2 \n")},
"With.Dot/.Hidden (2)": {mtime, []byte("Hidden 3 \n")},
}
if err := verifyArchiveContent(ctx, cd, filepath.Join(dataDir, "Duplicate Filenames.zip"), "", expectedContent); err != nil {
s.Error("Test failed: ", err)
}
}
// copyFile copies a file. Sadly fsutil.CopyFile is unsuitable for copying into FAT filesystem. This is an adaptation of it.
func copyFile(src, dst string) error {
sf, err := os.Open(src)
if err != nil {
return err
}
defer sf.Close()
fi, err := sf.Stat()
if err != nil {
return err
} else if !fi.Mode().IsRegular() {
return errors.Errorf("source not regular file (mode %s)", fi.Mode())
}
df, err := os.Create(dst)
if err != nil {
return err
}
if _, err := io.Copy(df, sf); err != nil {
df.Close()
os.Remove(df.Name())
return err
}
if err := df.Close(); err != nil {
os.Remove(df.Name())
return err
}
// Try to set mode and owner, but ignore failures, as on some filesystems it always fails.
os.Chmod(df.Name(), fi.Mode())
if os.Geteuid() == 0 {
st := fi.Sys().(*syscall.Stat_t)
os.Chown(df.Name(), int(st.Uid), int(st.Gid))
}
return nil
}
// RunArchiveTests executes a set of tests which mount different archives using CrosDisks.
func RunArchiveTests(ctx context.Context, s *testing.State) {
cd, err := crosdisks.New(ctx)
if err != nil {
s.Fatal("Failed to connect CrosDisks D-Bus service: ", err)
}
defer cd.Close()
// Create a FAT filesystem containing all our test archive files.
err = withLoopbackDeviceDo(ctx, cd, 64*1024*1024, "mkfs.vfat -n ARCHIVES", func(ctx context.Context, ld *crosdisks.LoopbackDevice) (err error) {
// Mounting it through CrosDisks will put the archives where we expect users to have them, so they are already in a permitted location.
return withMountDo(ctx, cd, ld.DevicePath(), "", "rw", func(ctx context.Context, mountPath string) error {
s.Logf("Copying all archives to the loopback device mount %q", mountPath)
for _, name := range PreparedArchives {
s.Logf("Copy %q", name)
if err := copyFile(s.DataPath(name), filepath.Join(mountPath, filepath.Base(name))); err != nil {
return errors.Wrapf(err, "failed to copy data file %q into %q", name, mountPath)
}
}
s.Run(ctx, "InvalidArchives", func(ctx context.Context, state *testing.State) {
testInvalidArchives(ctx, state, cd, mountPath)
})
s.Run(ctx, "Multipart", func(ctx context.Context, state *testing.State) {
testMultipartArchives(ctx, state, cd, mountPath)
})
s.Run(ctx, "Nested", func(ctx context.Context, state *testing.State) {
testNestedArchives(ctx, state, cd, mountPath)
})
s.Run(ctx, "Symlinks", func(ctx context.Context, state *testing.State) {
testSymlinksDisabledInArchives(ctx, state, cd, mountPath)
})
s.Run(ctx, "Unicode", func(ctx context.Context, state *testing.State) {
testUnicodeFilenamesInArchives(ctx, state, cd, mountPath)
})
s.Run(ctx, "MacOS-utf8", func(ctx context.Context, state *testing.State) {
testMacOSUTF8InArchives(ctx, state, cd, mountPath)
})
s.Run(ctx, "SJIS", func(ctx context.Context, state *testing.State) {
testSJISInArchives(ctx, state, cd, mountPath)
})
s.Run(ctx, "UniformEncryption", func(ctx context.Context, state *testing.State) {
testUniformEncryptionInArchives(ctx, state, cd, mountPath)
})
s.Run(ctx, "MixedEncryption", func(ctx context.Context, state *testing.State) {
testMixedEncryptioninArchives(ctx, state, cd, mountPath)
})
s.Run(ctx, "StrictPassword", func(ctx context.Context, state *testing.State) {
testStrictPasswordInArchives(ctx, state, cd, mountPath)
})
s.Run(ctx, "DuplicateFilenames", func(ctx context.Context, state *testing.State) {
testDuplicateFilenamesInArchives(ctx, state, cd, mountPath)
})
return nil
})
})
if err != nil {
s.Fatal("Failed to initialize test suite: ", err)
}
}