diff --git a/meson.build b/meson.build index 89efe5df86e2375f527a38b447d09c2b05c7bafa..74f6430d2a1c8a6122fcfa34d5db4a3f12bf9f41 100644 --- a/meson.build +++ b/meson.build @@ -136,6 +136,7 @@ scripts = [ ] tests = [ + 'cheap-copy.py', 'mypy.sh', 'pycodestyle.sh', 'pyflakes.sh', @@ -149,10 +150,16 @@ test_env.set('G_TEST_BUILDDIR', meson.current_build_dir()) test_env.set('PRESSURE_VESSEL_UNINSTALLED', 'yes') foreach test_name : tests + test_args = ['-v', files('tests/' + test_name)] + + if test_name.endswith('.py') + test_args += ['-e', python.path()] + endif + if prove.found() test( test_name, prove, - args : ['-v', files('tests/' + test_name)], + args : test_args, env : test_env, ) endif @@ -276,6 +283,28 @@ executable( install_dir : get_option('bindir'), ) +executable( + 'test-cheap-copy', + sources : [ + 'src/flatpak-utils-base.c', + 'src/flatpak-utils-base-private.h', + 'src/glib-backports.c', + 'src/glib-backports.h', + 'src/utils.c', + 'src/utils.h', + 'tests/cheap-copy.c', + ], + c_args : [ + '-Wno-unused-local-typedefs', + ], + dependencies : [ + dependency('gio-unix-2.0', required : true), + subproject('libglnx').get_variable('libglnx_dep'), + ], + include_directories : project_include_dirs, + install : false, +) + if get_option('man') pandoc = find_program('pandoc', required : true) diff --git a/src/utils.c b/src/utils.c index d51faa3b08935f44d7896f1b25636f56e2c9961e..2a144f3a9cfe5a80d3eea972b2e36b3c812c1d5f 100644 --- a/src/utils.c +++ b/src/utils.c @@ -22,6 +22,8 @@ #include "utils.h" +#include <ftw.h> + #include <glib.h> #include <glib/gstdio.h> #include <gio/gio.h> @@ -250,3 +252,124 @@ pv_hash_table_get_arbitrary_key (GHashTable *table) else return NULL; } + +/* nftw() doesn't have a user_data argument so we need to use a global + * variable :-( */ +static struct +{ + gchar *source_root; + gchar *dest_root; + GError *error; +} nftw_data; + +static int +copy_tree_helper (const char *fpath, + const struct stat *sb, + int typeflag, + struct FTW *ftwbuf) +{ + size_t len; + const char *suffix; + g_autofree gchar *dest = NULL; + g_autofree gchar *target = NULL; + GError **error = &nftw_data.error; + + g_return_val_if_fail (g_str_has_prefix (fpath, nftw_data.source_root), 1); + + if (strcmp (fpath, nftw_data.source_root) == 0) + { + if (typeflag != FTW_D) + { + glnx_throw (error, "\"%s\" is not a directory", fpath); + return 1; + } + + if (!glnx_shutil_mkdir_p_at (-1, nftw_data.dest_root, 0700, NULL, + error)) + return 1; + + return 0; + } + + len = strlen (nftw_data.source_root); + g_return_val_if_fail (fpath[len] == '/', 1); + suffix = &fpath[len + 1]; + dest = g_build_filename (nftw_data.dest_root, suffix, NULL); + + switch (typeflag) + { + case FTW_D: + /* For now we assume the permissions are not significant */ + if (!glnx_shutil_mkdir_p_at (-1, dest, 0755, NULL, error)) + return 1; + break; + + case FTW_SL: + target = glnx_readlinkat_malloc (-1, fpath, NULL, error); + + if (target == NULL) + return 1; + + if (symlink (target, dest) != 0) + { + glnx_throw_errno_prefix (error, + "Unable to create symlink at \"%s\"", + dest); + return 1; + } + break; + + case FTW_F: + /* TODO: If creating a hard link doesn't work, fall back to + * copying */ + if (link (fpath, dest) != 0) + { + glnx_throw_errno_prefix (error, + "Unable to create hard link from \"%s\" to \"%s\"", + fpath, dest); + return 1; + } + break; + + default: + glnx_throw (&nftw_data.error, + "Don't know how to handle ftw type flag %d at %s", + typeflag, fpath); + return 1; + } + + return 0; +} + +gboolean +pv_cheap_tree_copy (const char *source_root, + const char *dest_root, + GError **error) +{ + int res; + + /* Can't run concurrently */ + g_return_val_if_fail (nftw_data.source_root == NULL, FALSE); + + nftw_data.source_root = flatpak_canonicalize_filename (source_root); + nftw_data.dest_root = flatpak_canonicalize_filename (dest_root); + nftw_data.error = NULL; + + res = nftw (nftw_data.source_root, copy_tree_helper, 100, FTW_PHYS); + + if (res == -1) + { + g_assert (nftw_data.error == NULL); + glnx_throw_errno_prefix (error, "Unable to copy \"%s\" to \"%s\"", + source_root, dest_root); + } + else if (res != 0) + { + g_propagate_error (error, g_steal_pointer (&nftw_data.error)); + } + + g_clear_pointer (&nftw_data.source_root, g_free); + g_clear_pointer (&nftw_data.dest_root, g_free); + g_assert (nftw_data.error == NULL); + return (res == 0); +} diff --git a/src/utils.h b/src/utils.h index 31014575b470919ac0ee1fc14a22ef32c11e63f5..11172fd542cb9797b909b69f221b4a7fc32e828f 100644 --- a/src/utils.h +++ b/src/utils.h @@ -42,3 +42,7 @@ gchar *pv_capture_output (const char * const * argv, GError **error); gpointer pv_hash_table_get_arbitrary_key (GHashTable *table); + +gboolean pv_cheap_tree_copy (const char *source_root, + const char *dest_root, + GError **error); diff --git a/tests/cheap-copy.c b/tests/cheap-copy.c new file mode 100644 index 0000000000000000000000000000000000000000..9a5bf686c8a1183b48a58cbd2ad761c3942bc73d --- /dev/null +++ b/tests/cheap-copy.c @@ -0,0 +1,78 @@ +/* + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" +#include "subprojects/libglnx/config.h" + +#include <locale.h> +#include <sysexits.h> + +#include "libglnx/libglnx.h" + +#include "glib-backports.h" +#include "utils.h" + +static GOptionEntry options[] = +{ + { NULL } +}; + +int +main (int argc, + char *argv[]) +{ + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GError) local_error = NULL; + GError **error = &local_error; + int ret = EX_USAGE; + + setlocale (LC_ALL, ""); + pv_avoid_gvfs (); + + context = g_option_context_new ("SOURCE DEST"); + g_option_context_add_main_entries (context, options, NULL); + + if (!g_option_context_parse (context, &argc, &argv, error)) + goto out; + + if (argc >= 2 && strcmp (argv[1], "--") == 0) + { + argv++; + argc--; + } + + if (argc != 3) + { + g_printerr ("Usage: %s SOURCE DEST\n", g_get_prgname ()); + goto out; + } + + ret = EX_UNAVAILABLE; + + if (!pv_cheap_tree_copy (argv[1], argv[2], error)) + goto out; + + ret = 0; + +out: + if (local_error != NULL) + g_warning ("%s", local_error->message); + + return ret; +} diff --git a/tests/cheap-copy.py b/tests/cheap-copy.py new file mode 100755 index 0000000000000000000000000000000000000000..c01f4ffe1e936001fa190d7c57a8e51d4abe9e72 --- /dev/null +++ b/tests/cheap-copy.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# Copyright 2020 Collabora Ltd. +# +# SPDX-License-Identifier: MIT + +import os +import subprocess +import tempfile +import unittest + + +try: + import typing + typing # placate pyflakes +except ImportError: + pass + + +class TestCheapCopy(unittest.TestCase): + def setUp(self) -> None: + self.G_TEST_SRCDIR = os.getenv( + 'G_TEST_SRCDIR', + os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir), + ), + ) + self.G_TEST_BUILDDIR = os.getenv( + 'G_TEST_BUILDDIR', + os.path.abspath('_build'), + ) + self.cheap_copy = os.path.join(self.G_TEST_BUILDDIR, 'test-cheap-copy') + + def assert_tree_is_superset(self, superset, subset): + for path, dirs, files in os.walk(subset): + equivalent = os.path.join(superset, os.path.relpath(path, subset)) + + for d in dirs: + if not os.path.isdir(os.path.join(equivalent, d)): + raise AssertionError( + '%r should be a directory', equivalent) + + for f in files: + in_subset = os.path.join(path, f) + if ( + os.path.islink(in_subset) + or not os.path.exists(in_subset) + ): + target = os.readlink(in_subset) + target2 = os.readlink(os.path.join(equivalent, f)) + self.assertEqual(target, target2) + else: + info = os.stat(in_subset) + info2 = os.stat(os.path.join(equivalent, f)) + # they should be hard links + self.assertEqual(info.st_ino, info2.st_ino) + self.assertEqual(info.st_dev, info2.st_dev) + + def assert_tree_is_same(self, left, right): + self.assert_tree_is_superset(left, right) + self.assert_tree_is_superset(right, left) + + def test_empty(self) -> None: + with tempfile.TemporaryDirectory( + ) as source, tempfile.TemporaryDirectory( + ) as dest: + subprocess.run( + [ + self.cheap_copy, + source, + dest, + ], + check=True, + ) + self.assert_tree_is_same(source, dest) + + def test_create(self) -> None: + with tempfile.TemporaryDirectory( + ) as source, tempfile.TemporaryDirectory( + ) as parent: + dest = os.path.join(parent, 'dest') + subprocess.run( + [ + self.cheap_copy, + source, + dest, + ], + check=True, + ) + self.assert_tree_is_same(source, dest) + + def test_populated(self) -> None: + with tempfile.TemporaryDirectory( + ) as source, tempfile.TemporaryDirectory( + ) as parent: + os.makedirs(os.path.join(source, 'a', 'b', 'c')) + os.makedirs(os.path.join(source, 'files')) + + with open(os.path.join(source, 'x'), 'w') as writer: + writer.write('hello') + + with open(os.path.join(source, 'files', 'y'), 'w') as writer: + writer.write('hello') + + os.symlink('y', os.path.join(source, 'files', 'exists')) + os.symlink('/dev', os.path.join(source, 'files', 'dev')) + os.symlink('no', os.path.join(source, 'files', 'not here')) + + dest = os.path.join(parent, 'dest') + subprocess.run( + [ + self.cheap_copy, + source, + dest, + ], + check=True, + ) + self.assert_tree_is_same(source, dest) + + def tearDown(self) -> None: + pass + + +if __name__ == '__main__': + try: + from tap.runner import TAPTestRunner + except ImportError: + TAPTestRunner = None # type: ignore + + if TAPTestRunner is not None: + runner = TAPTestRunner() + runner.set_stream(True) + unittest.main(testRunner=runner) + else: + print('1..1') + program = unittest.main(exit=False) + if program.result.wasSuccessful(): + print( + 'ok 1 - %r (tap module not available)' + % program.result + ) + else: + print( + 'not ok 1 - %r (tap module not available)' + % program.result + ) + +# vi: set sw=4 sts=4 et: