mirror of
https://github.com/ostreedev/ostree.git
synced 2024-12-23 21:35:26 +03:00
2c55bc6997
There are use cases for libostree as a local content store for content derived or delivered via other mechanisms (e.g. OCI images, RPMs, etc.). rpm-ostree today imports RPMs into OSTree branches, and puts the RPM header value as commit metadata. Some of these can be quite large because the header includes permissions for each file. Similarly, some OCI metadata is large. Since there's no security issues with this, support committing such content. We still by default limit the size of metadata fetches, although for good measure we make this configurable too via a new `max-metadata-size` value. Closes: https://github.com/ostreedev/ostree/issues/1721 Closes: #1744 Approved by: jlebon
506 lines
17 KiB
C
506 lines
17 KiB
C
/*
|
|
* Copyright (C) 2016 Red Hat, Inc.
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.0+
|
|
*
|
|
* This library 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 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, write to the
|
|
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
|
* Boston, MA 02111-1307, USA.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <gio/gio.h>
|
|
#include <string.h>
|
|
#include <err.h>
|
|
|
|
#include "libglnx.h"
|
|
#include "libostreetest.h"
|
|
|
|
static void
|
|
test_repo_is_not_system (gconstpointer data)
|
|
{
|
|
OstreeRepo *repo = (void*)data;
|
|
g_assert (!ostree_repo_is_system (repo));
|
|
}
|
|
|
|
static GBytes *
|
|
input_stream_to_bytes (GInputStream *input)
|
|
{
|
|
g_autoptr(GOutputStream) mem_out_stream = NULL;
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
if (input == NULL)
|
|
return g_bytes_new (NULL, 0);
|
|
|
|
mem_out_stream = g_memory_output_stream_new (NULL, 0, g_realloc, g_free);
|
|
g_output_stream_splice (mem_out_stream,
|
|
input,
|
|
G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
|
|
NULL,
|
|
&error);
|
|
g_assert_no_error (error);
|
|
|
|
return g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (mem_out_stream));
|
|
}
|
|
|
|
static void
|
|
test_raw_file_to_archive_stream (gconstpointer data)
|
|
{
|
|
OstreeRepo *repo = OSTREE_REPO (data);
|
|
g_autofree gchar *commit_checksum = NULL;
|
|
g_autoptr(GHashTable) reachable = NULL;
|
|
g_autoptr(GError) error = NULL;
|
|
/* branch name of the test repository, see setup_test_repository in libtest.sh */
|
|
const gchar *rev = "test2";
|
|
GHashTableIter iter;
|
|
GVariant *serialized_object;
|
|
guint checks = 0;
|
|
|
|
ostree_repo_resolve_rev (repo,
|
|
rev,
|
|
FALSE,
|
|
&commit_checksum,
|
|
&error);
|
|
g_assert_no_error (error);
|
|
ostree_repo_traverse_commit (repo,
|
|
commit_checksum,
|
|
-1,
|
|
&reachable,
|
|
NULL,
|
|
&error);
|
|
g_assert_no_error (error);
|
|
g_hash_table_iter_init (&iter, reachable);
|
|
while (g_hash_table_iter_next (&iter, (gpointer*)&serialized_object, NULL))
|
|
{
|
|
const gchar *object_checksum;
|
|
OstreeObjectType object_type;
|
|
g_autoptr(GInputStream) input = NULL;
|
|
g_autoptr(GFileInfo) info = NULL;
|
|
g_autoptr(GVariant) xattrs = NULL;
|
|
g_autoptr(GBytes) input_bytes = NULL;
|
|
g_autoptr(GInputStream) mem_input = NULL;
|
|
g_autoptr(GInputStream) zlib_stream = NULL;
|
|
g_autoptr(GBytes) zlib_bytes = NULL;
|
|
g_autoptr(GInputStream) mem_zlib = NULL;
|
|
g_autoptr(GInputStream) input2 = NULL;
|
|
g_autoptr(GFileInfo) info2 = NULL;
|
|
g_autoptr(GVariant) xattrs2 = NULL;
|
|
g_autoptr(GBytes) input2_bytes = NULL;
|
|
|
|
ostree_object_name_deserialize (serialized_object, &object_checksum, &object_type);
|
|
if (object_type != OSTREE_OBJECT_TYPE_FILE)
|
|
continue;
|
|
|
|
ostree_repo_load_file (repo,
|
|
object_checksum,
|
|
&input,
|
|
&info,
|
|
&xattrs,
|
|
NULL,
|
|
&error);
|
|
g_assert_no_error (error);
|
|
|
|
input_bytes = input_stream_to_bytes (input);
|
|
/* This is to simulate NULL input received from
|
|
* ostree_repo_load_file. Instead of creating the mem_input
|
|
* variable, I could also rewind the input stream and pass it to
|
|
* the function below, but this would assume that the input
|
|
* stream implements either the GSeekable or
|
|
* GFileDescriptorBased interface. */
|
|
if (input != NULL)
|
|
mem_input = g_memory_input_stream_new_from_bytes (input_bytes);
|
|
ostree_raw_file_to_archive_z2_stream (mem_input,
|
|
info,
|
|
xattrs,
|
|
&zlib_stream,
|
|
NULL,
|
|
&error);
|
|
g_assert_no_error (error);
|
|
|
|
zlib_bytes = input_stream_to_bytes (zlib_stream);
|
|
mem_zlib = g_memory_input_stream_new_from_bytes (zlib_bytes);
|
|
ostree_content_stream_parse (FALSE,
|
|
mem_zlib,
|
|
g_bytes_get_size (zlib_bytes),
|
|
FALSE,
|
|
&input2,
|
|
&info2,
|
|
&xattrs2,
|
|
NULL,
|
|
&error);
|
|
g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
|
|
g_clear_error (&error);
|
|
|
|
g_seekable_seek (G_SEEKABLE (mem_zlib),
|
|
0,
|
|
G_SEEK_SET,
|
|
NULL,
|
|
&error);
|
|
g_assert_no_error (error);
|
|
|
|
ostree_content_stream_parse (TRUE,
|
|
mem_zlib,
|
|
g_bytes_get_size (zlib_bytes),
|
|
FALSE,
|
|
&input2,
|
|
&info2,
|
|
&xattrs2,
|
|
NULL,
|
|
&error);
|
|
g_assert_no_error (error);
|
|
|
|
input2_bytes = input_stream_to_bytes (input2);
|
|
g_assert_true (g_bytes_equal (input_bytes, input2_bytes));
|
|
g_assert_true (g_variant_equal (xattrs, xattrs2));
|
|
/* TODO: Not sure how to compare fileinfos */
|
|
++checks;
|
|
}
|
|
/* to make sure we really tested the function */
|
|
g_assert_cmpint (checks, >, 0);
|
|
}
|
|
|
|
static gboolean hi_content_stream_new (GInputStream **out_stream,
|
|
guint64 *out_length,
|
|
GError **error)
|
|
{
|
|
static const char hi[] = "hi";
|
|
g_autoptr(GMemoryInputStream) hi_memstream = (GMemoryInputStream*)g_memory_input_stream_new_from_data (hi, sizeof(hi)-1, NULL);
|
|
g_autoptr(GFileInfo) finfo = g_file_info_new ();
|
|
g_file_info_set_attribute_uint32 (finfo, "standard::type", G_FILE_TYPE_REGULAR);
|
|
g_file_info_set_attribute_boolean (finfo, "standard::is-symlink", FALSE);
|
|
g_file_info_set_attribute_uint32 (finfo, "unix::uid", 0);
|
|
g_file_info_set_attribute_uint32 (finfo, "unix::gid", 0);
|
|
g_file_info_set_attribute_uint32 (finfo, "unix::mode", S_IFREG|0644);
|
|
return ostree_raw_file_to_content_stream ((GInputStream*)hi_memstream, finfo, NULL, out_stream, out_length, NULL, error);
|
|
}
|
|
|
|
static void
|
|
test_validate_remotename (void)
|
|
{
|
|
const char *valid[] = {"foo", "hello-world"};
|
|
const char *invalid[] = {"foo/bar", ""};
|
|
for (guint i = 0; i < G_N_ELEMENTS(valid); i++)
|
|
{
|
|
g_autoptr(GError) error = NULL;
|
|
g_assert (ostree_validate_remote_name (valid[i], &error));
|
|
g_assert_no_error (error);
|
|
}
|
|
for (guint i = 0; i < G_N_ELEMENTS(invalid); i++)
|
|
{
|
|
g_autoptr(GError) error = NULL;
|
|
g_assert (!ostree_validate_remote_name (invalid[i], &error));
|
|
g_assert (error != NULL);
|
|
}
|
|
}
|
|
|
|
static void
|
|
test_object_writes (gconstpointer data)
|
|
{
|
|
OstreeRepo *repo = OSTREE_REPO (data);
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
static const char hi_sha256[] = "2301b5923720c3edc1f0467addb5c287fd5559e3e0cd1396e7f1edb6b01be9f0";
|
|
|
|
/* Successful content write */
|
|
{ g_autoptr(GInputStream) hi_memstream = NULL;
|
|
guint64 len;
|
|
hi_content_stream_new (&hi_memstream, &len, &error);
|
|
g_assert_no_error (error);
|
|
g_autofree guchar *csum = NULL;
|
|
(void)ostree_repo_write_content (repo, hi_sha256, hi_memstream, len, &csum,
|
|
NULL, &error);
|
|
g_assert_no_error (error);
|
|
}
|
|
|
|
/* Invalid content write */
|
|
{ g_autoptr(GInputStream) hi_memstream = NULL;
|
|
guint64 len;
|
|
hi_content_stream_new (&hi_memstream, &len, &error);
|
|
g_assert_no_error (error);
|
|
g_autofree guchar *csum = NULL;
|
|
static const char invalid_hi_sha256[] = "cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe";
|
|
g_assert (!ostree_repo_write_content (repo, invalid_hi_sha256, hi_memstream, len, &csum,
|
|
NULL, &error));
|
|
g_assert (error);
|
|
g_assert (strstr (error->message, "Corrupted file object"));
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
impl_test_break_hardlink (int tmp_dfd,
|
|
const char *path,
|
|
GError **error)
|
|
{
|
|
const char *linkedpath = glnx_strjoina (path, ".link");
|
|
struct stat orig_stbuf;
|
|
if (!glnx_fstatat (tmp_dfd, path, &orig_stbuf, AT_SYMLINK_NOFOLLOW, error))
|
|
return FALSE;
|
|
|
|
/* Calling ostree_break_hardlink() should be a noop */
|
|
struct stat stbuf;
|
|
if (!ostree_break_hardlink (tmp_dfd, path, TRUE, NULL, error))
|
|
return FALSE;
|
|
if (!glnx_fstatat (tmp_dfd, path, &stbuf, AT_SYMLINK_NOFOLLOW, error))
|
|
return FALSE;
|
|
|
|
g_assert_cmpint (orig_stbuf.st_dev, ==, stbuf.st_dev);
|
|
g_assert_cmpint (orig_stbuf.st_ino, ==, stbuf.st_ino);
|
|
|
|
if (linkat (tmp_dfd, path, tmp_dfd, linkedpath, 0) < 0)
|
|
return glnx_throw_errno_prefix (error, "linkat");
|
|
|
|
if (!ostree_break_hardlink (tmp_dfd, path, TRUE, NULL, error))
|
|
return FALSE;
|
|
if (!glnx_fstatat (tmp_dfd, path, &stbuf, AT_SYMLINK_NOFOLLOW, error))
|
|
return FALSE;
|
|
/* This file should be different */
|
|
g_assert_cmpint (orig_stbuf.st_dev, ==, stbuf.st_dev);
|
|
g_assert_cmpint (orig_stbuf.st_ino, !=, stbuf.st_ino);
|
|
/* But this one is still the same */
|
|
if (!glnx_fstatat (tmp_dfd, linkedpath, &stbuf, AT_SYMLINK_NOFOLLOW, error))
|
|
return FALSE;
|
|
g_assert_cmpint (orig_stbuf.st_dev, ==, stbuf.st_dev);
|
|
g_assert_cmpint (orig_stbuf.st_ino, ==, stbuf.st_ino);
|
|
|
|
(void) unlinkat (tmp_dfd, path, 0);
|
|
(void) unlinkat (tmp_dfd, linkedpath, 0);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
test_break_hardlink (void)
|
|
{
|
|
int tmp_dfd = AT_FDCWD;
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
/* Regular file */
|
|
const char hello_hardlinked_content[] = "hello hardlinked content";
|
|
glnx_file_replace_contents_at (tmp_dfd, "test-hardlink",
|
|
(guint8*)hello_hardlinked_content,
|
|
strlen (hello_hardlinked_content),
|
|
GLNX_FILE_REPLACE_NODATASYNC,
|
|
NULL, &error);
|
|
g_assert_no_error (error);
|
|
(void)impl_test_break_hardlink (tmp_dfd, "test-hardlink", &error);
|
|
g_assert_no_error (error);
|
|
|
|
/* Symlink */
|
|
if (symlinkat ("some-path", tmp_dfd, "test-symhardlink") < 0)
|
|
err (1, "symlinkat");
|
|
(void)impl_test_break_hardlink (tmp_dfd, "test-symhardlink", &error);
|
|
g_assert_no_error (error);
|
|
}
|
|
|
|
static GVariant*
|
|
xattr_cb (OstreeRepo *repo,
|
|
const char *path,
|
|
GFileInfo *file_info,
|
|
gpointer user_data)
|
|
{
|
|
GVariant *xattr = user_data;
|
|
if (g_str_equal (path, "/baz/cow"))
|
|
return g_variant_ref (xattr);
|
|
return NULL;
|
|
}
|
|
|
|
/* check that using a devino cache doesn't cause us to ignore xattr callbacks */
|
|
static void
|
|
test_devino_cache_xattrs (void)
|
|
{
|
|
g_autoptr(GError) error = NULL;
|
|
gboolean ret = FALSE;
|
|
|
|
g_autoptr(GFile) repo_path = g_file_new_for_path ("repo");
|
|
|
|
/* re-initialize as bare */
|
|
ret = ot_test_run_libtest ("setup_test_repository bare", &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
gboolean can_relabel;
|
|
ret = ot_check_relabeling (&can_relabel, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
gboolean has_user_xattrs;
|
|
ret = ot_check_user_xattrs (&has_user_xattrs, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
/* we need both because we're bare and our tests target user xattrs */
|
|
if (!can_relabel || !has_user_xattrs)
|
|
{
|
|
g_test_skip ("this test requires full xattr support");
|
|
return;
|
|
}
|
|
|
|
g_autoptr(OstreeRepo) repo = ostree_repo_new (repo_path);
|
|
ret = ostree_repo_open (repo, NULL, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
g_autofree char *csum = NULL;
|
|
ret = ostree_repo_resolve_rev (repo, "test2", FALSE, &csum, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
g_autoptr(OstreeRepoDevInoCache) cache = ostree_repo_devino_cache_new ();
|
|
|
|
OstreeRepoCheckoutAtOptions options = {0,};
|
|
options.no_copy_fallback = TRUE;
|
|
options.devino_to_csum_cache = cache;
|
|
ret = ostree_repo_checkout_at (repo, &options, AT_FDCWD, "checkout", csum, NULL, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
g_autoptr(OstreeMutableTree) mtree = ostree_mutable_tree_new ();
|
|
g_autoptr(OstreeRepoCommitModifier) modifier =
|
|
ostree_repo_commit_modifier_new (0, NULL, NULL, NULL);
|
|
ostree_repo_commit_modifier_set_devino_cache (modifier, cache);
|
|
|
|
g_auto(GVariantBuilder) builder;
|
|
g_variant_builder_init (&builder, (GVariantType*)"a(ayay)");
|
|
g_variant_builder_add (&builder, "(@ay@ay)",
|
|
g_variant_new_bytestring ("user.myattr"),
|
|
g_variant_new_bytestring ("data"));
|
|
g_autoptr(GVariant) orig_xattrs = g_variant_ref_sink (g_variant_builder_end (&builder));
|
|
|
|
ret = ostree_repo_prepare_transaction (repo, NULL, NULL, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
ostree_repo_commit_modifier_set_xattr_callback (modifier, xattr_cb, NULL, orig_xattrs);
|
|
ret = ostree_repo_write_dfd_to_mtree (repo, AT_FDCWD, "checkout",
|
|
mtree, modifier, NULL, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
g_autoptr(GFile) root = NULL;
|
|
ret = ostree_repo_write_mtree (repo, mtree, &root, NULL, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
/* now check that the final xattr matches */
|
|
g_autoptr(GFile) baz_child = g_file_get_child (root, "baz");
|
|
g_autoptr(GFile) cow_child = g_file_get_child (baz_child, "cow");
|
|
|
|
g_autoptr(GVariant) xattrs = NULL;
|
|
ret = ostree_repo_file_get_xattrs (OSTREE_REPO_FILE (cow_child), &xattrs, NULL, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
gboolean found_xattr = FALSE;
|
|
gsize n = g_variant_n_children (xattrs);
|
|
for (gsize i = 0; i < n; i++)
|
|
{
|
|
const guint8* name;
|
|
const guint8* value;
|
|
g_variant_get_child (xattrs, i, "(^&ay^&ay)", &name, &value);
|
|
|
|
if (g_str_equal ((const char*)name, "user.myattr"))
|
|
{
|
|
g_assert_cmpstr ((const char*)value, ==, "data");
|
|
found_xattr = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
g_assert (found_xattr);
|
|
|
|
OstreeRepoTransactionStats stats;
|
|
ret = ostree_repo_commit_transaction (repo, &stats, NULL, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
/* we should only have had to checksum /baz/cow */
|
|
g_assert_cmpint (stats.content_objects_written, ==, 1);
|
|
}
|
|
|
|
/* https://github.com/ostreedev/ostree/issues/1721
|
|
* We should be able to commit large metadata objects now.
|
|
*/
|
|
static void
|
|
test_big_metadata (void)
|
|
{
|
|
g_autoptr(GError) error = NULL;
|
|
gboolean ret = FALSE;
|
|
|
|
g_autoptr(GFile) repo_path = g_file_new_for_path ("repo");
|
|
|
|
/* init as bare-user-only so we run everywhere */
|
|
ret = ot_test_run_libtest ("setup_test_repository bare-user-only", &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
g_autoptr(OstreeRepo) repo = ostree_repo_new (repo_path);
|
|
ret = ostree_repo_open (repo, NULL, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
g_autoptr(GFile) object_to_commit = NULL;
|
|
ret = ostree_repo_read_commit (repo, "test2", &object_to_commit, NULL, NULL, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
g_autoptr(OstreeMutableTree) mtree = ostree_mutable_tree_new ();
|
|
ret = ostree_repo_write_directory_to_mtree (repo, object_to_commit, mtree, NULL,
|
|
NULL, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
|
|
const size_t len = 20 * 1024 * 1024;
|
|
g_assert_cmpint (len, >, OSTREE_MAX_METADATA_SIZE);
|
|
g_autofree char *large_buf = g_malloc (len);
|
|
memset (large_buf, 0x42, len);
|
|
g_autoptr(GVariantBuilder) builder =
|
|
g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
|
|
g_autofree char *commit_checksum = NULL;
|
|
g_variant_builder_add (builder, "{sv}", "large-value",
|
|
g_variant_new_fixed_array ((GVariantType*)"y",
|
|
large_buf, len, sizeof (char)));
|
|
ret = ostree_repo_write_commit (repo, NULL, NULL, NULL, g_variant_builder_end (builder),
|
|
OSTREE_REPO_FILE (object_to_commit), &commit_checksum, NULL, &error);
|
|
g_assert_no_error (error);
|
|
g_assert (ret);
|
|
}
|
|
|
|
int main (int argc, char **argv)
|
|
{
|
|
g_autoptr(GError) error = NULL;
|
|
glnx_unref_object OstreeRepo *repo = NULL;
|
|
|
|
g_test_init (&argc, &argv, NULL);
|
|
|
|
repo = ot_test_setup_repo (NULL, &error);
|
|
if (!repo)
|
|
goto out;
|
|
|
|
g_test_add_data_func ("/repo-not-system", repo, test_repo_is_not_system);
|
|
g_test_add_data_func ("/raw-file-to-archive-stream", repo, test_raw_file_to_archive_stream);
|
|
g_test_add_data_func ("/objectwrites", repo, test_object_writes);
|
|
g_test_add_func ("/xattrs-devino-cache", test_devino_cache_xattrs);
|
|
g_test_add_func ("/break-hardlink", test_break_hardlink);
|
|
g_test_add_func ("/remotename", test_validate_remotename);
|
|
g_test_add_func ("/big-metadata", test_big_metadata);
|
|
|
|
return g_test_run();
|
|
out:
|
|
if (error)
|
|
g_error ("%s", error->message);
|
|
return 1;
|
|
}
|