/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright (C) 2014 James Antil * * 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 of the licence 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 #include #include #include #include "rpmostree-builtins.h" #include "rpmostree-treepkgdiff.h" #include "rpmostree-util.h" /* FIXME: */ #define OSTREE_GIO_FAST_QUERYINFO ("standard::name,standard::type,standard::size,standard::is-symlink,standard::symlink-target," \ "unix::device,unix::inode,unix::mode,unix::uid,unix::gid,unix::rdev") #include "libgsystem.h" #include #include #include #include static char *opt_repo; static char *opt_rpmdbdir; static GOptionEntry option_entries[] = { { "repo", 'r', 0, G_OPTION_ARG_STRING, &opt_repo, "Path to OSTree repository", "REPO" }, { "rpmdbdir", 0, 0, G_OPTION_ARG_STRING, &opt_rpmdbdir, "Working directory", "WORKDIR" }, { NULL } }; static void header_free_p (gpointer data) { headerFree (data); } static int header_name_cmp (Header h1, Header h2) { const char* n1 = headerGetString (h1, RPMTAG_NAME); const char* n2 = headerGetString (h2, RPMTAG_NAME); int cmp = strcmp (n1, n2); /* printf("%s <=> %s = %u\n", n1, n2, cmp); */ return cmp; } static int header_cmp_p (gconstpointer gph1, gconstpointer gph2) { const Header *ph1 = gph1; const Header *ph2 = gph2; Header h1 = *ph1; Header h2 = *ph2; int cmp = header_name_cmp (h1, h2); if (!cmp) cmp = rpmVersionCompare (h1, h2); return cmp; } /* generate pkg nevra/etc. strings from Header objects */ static char * pkg_envra_strdup (Header h1) { const char* name = headerGetString (h1, RPMTAG_NAME); uint64_t epoch = headerGetNumber (h1, RPMTAG_EPOCH); const char* version = headerGetString (h1, RPMTAG_VERSION); const char* release = headerGetString (h1, RPMTAG_RELEASE); const char* arch = headerGetString (h1, RPMTAG_ARCH); char *envra = NULL; if (!epoch) envra = g_strdup_printf ("%s-%s-%s.%s", name, version, release, arch); else { unsigned long long ullepoch = epoch; envra = g_strdup_printf ("%llu:%s-%s-%s.%s", ullepoch, name, version, release, arch); } return envra; } static char * pkg_nevra_strdup (Header h1) { const char* name = headerGetString (h1, RPMTAG_NAME); uint64_t epoch = headerGetNumber (h1, RPMTAG_EPOCH); const char* version = headerGetString (h1, RPMTAG_VERSION); const char* release = headerGetString (h1, RPMTAG_RELEASE); const char* arch = headerGetString (h1, RPMTAG_ARCH); char *nevra = NULL; if (!epoch) nevra = g_strdup_printf ("%s-%s-%s.%s", name, version, release, arch); else { unsigned long long ullepoch = epoch; nevra = g_strdup_printf ("%s-%llu:%s-%s.%s", name, ullepoch, version, release, arch); } return nevra; } static char * pkg_na_strdup (Header h1) { const char* name = headerGetString (h1, RPMTAG_NAME); const char* arch = headerGetString (h1, RPMTAG_ARCH); return g_strdup_printf ("%s.%s", name, arch); } static char * pkg_nvr_strdup (Header h1) { const char* name = headerGetString (h1, RPMTAG_NAME); const char* version = headerGetString (h1, RPMTAG_VERSION); const char* release = headerGetString (h1, RPMTAG_RELEASE); return g_strdup_printf ("%s-%s-%s", name, version, release); } struct RpmHeaders { rpmts ts; /* rpm transaction set the headers belong to */ GPtrArray *hs; /* list of rpm header objects from = Header */ }; #define CASENCMP_EQ(x, y, n) (g_ascii_strncasecmp (x, y, n) == 0) #define CASEFNMATCH_EQ(x, y) (fnmatch (x, y, FNM_CASEFOLD) == 0) /* find a common prefix length that doesn't need fnmatch */ static size_t pat_fnmatch_prefix (GPtrArray *patterns) { gsize ret = G_MAXSIZE; int num = 0; if (!patterns) return 0; for (num = 0; num < patterns->len; num++) { const char *pat = patterns->pdata[num]; gsize prefix = 0; while (*pat) { if (*pat == ':') break; if (*pat == '-') break; if (*pat == '*') break; if (*pat == '?') break; if (*pat == '.') break; if (*pat == '[') break; ++prefix; ++pat; } ret = MIN(ret, prefix); } return ret; } static gboolean pat_fnmatch_match (Header pkg, const char *name, gsize patprefixlen, GPtrArray *patterns) { int num = 0; gs_free char *pkg_na = NULL; gs_free char *pkg_nevra = NULL; gs_free char *pkg_nvr = NULL; if (!patterns) return TRUE; for (num = 0; num < patterns->len; num++) { const char *pattern = patterns->pdata[num]; if (patprefixlen && !CASENCMP_EQ (name, pattern, patprefixlen)) continue; if (!pkg_na) { pkg_nevra = pkg_nevra_strdup (pkg); pkg_na = pkg_na_strdup (pkg); pkg_nvr = pkg_nvr_strdup (pkg); } if (CASEFNMATCH_EQ (pattern, name) || CASEFNMATCH_EQ (pattern, pkg_nevra) || CASEFNMATCH_EQ (pattern, pkg_na) || CASEFNMATCH_EQ (pattern, pkg_nvr) || FALSE) return TRUE; } return FALSE; } static struct RpmHeaders * rpmhdrs_new (const char *root, GPtrArray *patterns) { rpmts ts = rpmtsCreate(); int status = -1; rpmdbMatchIterator iter; Header h1; GPtrArray *hs = NULL; struct RpmHeaders *ret = NULL; gsize patprefixlen = pat_fnmatch_prefix (patterns);; rpmtsSetVSFlags (ts, _RPMVSF_NODIGESTS | _RPMVSF_NOSIGNATURES); status = rpmtsSetRootDir (ts, root); if (status) exit (2); // FIXME: better fail. /* iter = rpmtsInitIterator (ts, RPMTAG_NAME, "yum", 0); */ iter = rpmtsInitIterator (ts, RPMDBI_PACKAGES, NULL, 0); hs = g_ptr_array_new_with_free_func (header_free_p); while ((h1 = rpmdbNextIterator (iter))) { const char* name = headerGetString (h1, RPMTAG_NAME); if (g_str_equal (name, "gpg-pubkey")) continue; /* rpmdb abstraction leak */ if (!pat_fnmatch_match (h1, name, patprefixlen, patterns)) continue; h1 = headerLink (h1); g_ptr_array_add (hs, h1); } iter = rpmdbFreeIterator (iter); g_ptr_array_sort (hs, header_cmp_p); ret = g_malloc0 (sizeof (struct RpmHeaders)); ret->ts = ts; ret->hs = hs; return ret; } static void rpmhdrs_free (struct RpmHeaders *l1) { g_ptr_array_free (l1->hs, TRUE); l1->hs = NULL; l1->ts = rpmtsFree (l1->ts); g_free (l1); } static char * pkg_yumdb_relpath (Header h1) { const char* name = headerGetString (h1, RPMTAG_NAME); const char* version = headerGetString (h1, RPMTAG_VERSION); const char* release = headerGetString (h1, RPMTAG_RELEASE); const char* arch = headerGetString (h1, RPMTAG_ARCH); const char* pkgid = headerGetString (h1, RPMTAG_SHA1HEADER); char *path = NULL; g_assert (name[0]); // FIXME: sanitize name ... remove '/' and '~' // FIXME: If pkgid doesn't exist use: name.buildtime path = g_strdup_printf ("%c/%s-%s-%s-%s-%s", name[0], pkgid, name, version, release, arch); return path; } static GInputStream * pkg_yumdb_file_read (GFile *root, Header pkg, const char *yumdb_key, GCancellable *cancellable, GError **error) { gs_unref_object GFile *f = NULL; gs_free char *pkgpath = pkg_yumdb_relpath (pkg); gs_free char *path = g_strconcat ("/var/lib/yum/yumdb/", pkgpath, "/", yumdb_key, NULL); f = g_file_resolve_relative_path (root, path); return (GInputStream*)g_file_read (f, cancellable, error); } static char * pkg_yumdb_strdup (GFile *root, Header pkg, const char *yumdb_key, GCancellable *cancellable, GError **error) { gs_unref_object GFile *f = NULL; gs_free char *pkgpath = pkg_yumdb_relpath (pkg); gs_free char *path = g_strconcat ("/var/lib/yum/yumdb/", pkgpath, "/", yumdb_key, NULL); char *ret = NULL; f = g_file_resolve_relative_path (root, path); if (!_rpmostree_file_load_contents_utf8_allow_noent (f, &ret, cancellable, error)) return g_strdup (""); return ret; } static gsize _console_get_width(int fd) { struct winsize w; ioctl (STDOUT_FILENO, TIOCGWINSZ, &w); return w.ws_col; } static gsize _console_get_width_stdout_cached(void) { static gsize align = 0; if (!align) align = _console_get_width (1); return align; } static void pkg_print (GFile *root, Header pkg, GCancellable *cancellable, GError **error) { gs_free char *nevra = pkg_nevra_strdup (pkg); gs_free char *from_repo = pkg_yumdb_strdup (root, pkg, "from_repo", cancellable, error); gsize align = _console_get_width_stdout_cached (); if (*from_repo) { if (align) { gsize plen = strlen (nevra); gsize rlen = strlen (from_repo) + 1; --align; // hacky ... for leading spaces. if (align > (plen + rlen)) printf ("%s%*s@%s\n", nevra, align - (plen + rlen), "", from_repo); else align = 0; } if (!align) printf ("%s @%s\n", nevra, from_repo); } else printf("%s\n", nevra); } static void rpmhdrs_list (GFile *root, struct RpmHeaders *l1, GCancellable *cancellable, GError **error) { int num = 0; while (num < l1->hs->len) { Header h1 = l1->hs->pdata[num++]; printf (" "); pkg_print (root, h1, cancellable, error); } } static char * rpmhdrs_rpmdbv (GFile *root, struct RpmHeaders *l1, GCancellable *cancellable, GError **error) { GChecksum *checksum = g_checksum_new (G_CHECKSUM_SHA1); gs_free char *checksum_cstr = NULL; int num = 0; while (num < l1->hs->len) { Header pkg = l1->hs->pdata[num++]; gs_unref_object GInputStream *tin = NULL; gs_unref_object GInputStream *din = NULL; char tbuf[1024]; char dbuf[1024]; gs_free char *envra = pkg_envra_strdup (pkg); gsize tbytes_read = 0; gsize dbytes_read = 0; g_checksum_update (checksum, (guint8*)envra, strlen(envra)); tin = pkg_yumdb_file_read (root, pkg, "checksum_type", cancellable,error); if (!tin) continue; din = pkg_yumdb_file_read (root, pkg, "checksum_data", cancellable,error); if (!din) continue; if (!g_input_stream_read_all (tin, tbuf, sizeof(tbuf), &tbytes_read, cancellable, error)) continue; if (!g_input_stream_read_all (din, dbuf, sizeof(dbuf), &dbytes_read, cancellable, error)) continue; if (tbytes_read >= 512) continue; // should be == len(md5) or len(sha256) etc. if (dbytes_read >= 1024) continue; // should be digest size of md5/sha256/sha512/etc. g_checksum_update (checksum, (guint8*)tbuf, tbytes_read); g_checksum_update (checksum, (guint8*)dbuf, dbytes_read); } checksum_cstr = g_strdup (g_checksum_get_string (checksum)); g_checksum_free (checksum); return g_strdup_printf ("%u:%s", num, checksum_cstr); } static void rpmhdrs_diff (GFile *root1, struct RpmHeaders *l1, GFile *root2, struct RpmHeaders *l2, GCancellable *cancellable, GError **error) { int n1 = 0; int n2 = 0; while (n1 < l1->hs->len) { Header h1 = l1->hs->pdata[n1]; if (n2 >= l2->hs->len) { printf ("-"); pkg_print (root1, h1, cancellable, error); ++n1; } else { Header h2 = l2->hs->pdata[n2]; int cmp = header_name_cmp (h1, h2); if (cmp > 0) { printf("+"); pkg_print (root2, h2, cancellable, error); ++n2; } else if (cmp < 0) { printf("-"); pkg_print (root1, h1, cancellable, error); ++n1; } else { cmp = rpmVersionCompare (h1, h2); if (!cmp) { ++n1; ++n2; continue; } printf("!<= "); pkg_print (root1, h1, cancellable, error); ++n1; printf("!>= "); pkg_print (root2, h2, cancellable, error); ++n2; } } } while (n2 < l2->hs->len) { Header h2 = l2->hs->pdata[n2]; printf("+"); pkg_print (root2, h2, cancellable, error); ++n2; } } /* data needed to extract rpm/yum data from a commit revision */ struct RpmRevisionData { struct RpmHeaders *rpmdb; GFile *root; char *commit; }; static void rpmrev_free (struct RpmRevisionData *ptr) { gs_unref_object GFile *root = NULL; gs_free char *commit = NULL; if (!ptr) return; rpmhdrs_free (ptr->rpmdb); ptr->rpmdb = NULL; root = ptr->root; ptr->root = NULL; commit = ptr->commit; ptr->commit = NULL; g_free (ptr); } _RPMOSTREE_DEFINE_TRIVIAL_CLEANUP_FUNC(struct RpmRevisionData *, rpmrev_free); #define _cleanup_rpmrev_ __attribute__((cleanup(rpmrev_freep))) static struct RpmRevisionData * rpmrev_new (OstreeRepo *repo, GFile *rpmdbdir, const char *rev, GPtrArray *patterns, GCancellable *cancellable, GError **error) { gs_unref_object GFile *subtree = NULL; gs_unref_object GFileInfo *file_info = NULL; gs_unref_object GFile *targetp = NULL; gs_unref_object GFile *target = NULL; gs_free char *targetp_path = NULL; gs_free char *target_path = NULL; gs_unref_object GFile *revdir = NULL; GError *tmp_error = NULL; struct RpmRevisionData *rpmrev = NULL; gs_unref_object GFile *root = NULL; gs_free char *commit = NULL; if (!ostree_repo_read_commit (repo, rev, &root, &commit, NULL, error)) goto out; subtree = g_file_resolve_relative_path (root, "/var/lib/rpm"); if (!g_file_query_exists (subtree, cancellable)) { g_object_unref (subtree); subtree = g_file_resolve_relative_path (root, "/usr/share/rpm"); } file_info = g_file_query_info (subtree, OSTREE_GIO_FAST_QUERYINFO, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable, &tmp_error); if (!file_info) // g_file_query_exists (subtree, cancellable)) { g_propagate_error (error, tmp_error); // g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, // "No rpmdb directory found"); goto out; } revdir = g_file_resolve_relative_path (rpmdbdir, commit); targetp_path = g_strconcat (commit, "/var/lib", NULL); targetp = g_file_resolve_relative_path (rpmdbdir, targetp_path); target_path = g_strconcat (commit, "/var/lib/rpm", NULL); target = g_file_resolve_relative_path (rpmdbdir, target_path); if (!g_file_query_exists (target, cancellable) && (!gs_file_ensure_directory (targetp, TRUE, cancellable, error) || !ostree_repo_checkout_tree (repo, OSTREE_REPO_CHECKOUT_MODE_NONE, OSTREE_REPO_CHECKOUT_OVERWRITE_NONE, target, OSTREE_REPO_FILE (subtree), file_info, cancellable, error))) goto out; rpmrev = g_malloc0 (sizeof(struct RpmRevisionData)); rpmrev->root = root; root = NULL; rpmrev->commit = commit; commit = NULL; rpmrev->rpmdb = rpmhdrs_new (gs_file_get_path_cached (revdir), patterns); out: return rpmrev; } gboolean rpmostree_builtin_rpm (int argc, char **argv, GCancellable *cancellable, GError **error) { gboolean ret = FALSE; gs_unref_ptrarray GPtrArray *deployments = NULL; // list of all depoyments GOptionContext *context = g_option_context_new ("- Run rpm commands on systems"); gs_unref_object OstreeRepo *repo = NULL; gs_unref_object GFile *rpmdbdir = NULL; gboolean rpmdbdir_is_tmp = FALSE; const char *cmd = NULL; int argnum = 2; g_option_context_add_main_entries (context, option_entries, NULL); if (!g_option_context_parse (context, &argc, &argv, error)) goto out; if (argc < 3) { g_printerr ("usage: rpm-ostree rpm diff|list|version COMMIT...\n"); g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Argument processing failed"); goto out; } cmd = argv[1]; if ((g_str_equal (cmd, "diff")) && (argc != 4)) { g_printerr ("usage: rpm-ostree rpm diff COMMIT COMMIT\n"); g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Argument processing failed"); goto out; } if (!opt_repo) { gs_unref_object OstreeSysroot *sysroot = NULL; sysroot = ostree_sysroot_new_default (); if (!ostree_sysroot_load (sysroot, cancellable, error)) goto out; if (!ostree_sysroot_get_repo (sysroot, &repo, cancellable, error)) goto out; } else { gs_unref_object GFile *repo_path = NULL; repo_path = g_file_new_for_path (opt_repo); repo = ostree_repo_new (repo_path); if (!ostree_repo_open (repo, cancellable, error)) goto out; } if (rpmReadConfigFiles (NULL, NULL)) exit (2); // FIXME: Better fail if (opt_rpmdbdir) { rpmdbdir = g_file_new_for_path (opt_rpmdbdir); } else { gs_free char *tmpd = g_mkdtemp (g_strdup ("/var/tmp/rpm-ostree.XXXXXX")); rpmdbdir = g_file_new_for_path (tmpd); rpmdbdir_is_tmp = TRUE; } if (FALSE) {} else if (g_str_equal(cmd, "version")) for (; argnum < argc; ++argnum) { const char *rev = argv[argnum]; _cleanup_rpmrev_ struct RpmRevisionData *rpmrev = NULL; gs_free char *rpmdbv = NULL; rpmrev = rpmrev_new (repo, rpmdbdir, rev, NULL, cancellable, error); if (!rpmrev) goto out; rpmdbv = rpmhdrs_rpmdbv (rpmrev->root, rpmrev->rpmdb, cancellable, error); // FIXME: g_console if (!g_str_equal (rev, rpmrev->commit)) printf ("ostree commit: %s (%s)\n", rev, rpmrev->commit); else printf ("ostree commit: %s\n", rev); printf (" rpmdbv is: %24s%s\n", "", rpmdbv); } else if (g_str_equal (cmd, "diff")) { _cleanup_rpmrev_ struct RpmRevisionData *rpmrev1 = NULL; _cleanup_rpmrev_ struct RpmRevisionData *rpmrev2 = NULL; if (!(rpmrev1 = rpmrev_new (repo, rpmdbdir, argv[2], NULL, cancellable, error))) goto out; if (!(rpmrev2 = rpmrev_new (repo, rpmdbdir, argv[3], NULL, cancellable, error))) goto out; if (!g_str_equal (argv[2], rpmrev1->commit)) printf ("ostree diff commit old: %s (%s)\n", argv[2], rpmrev1->commit); else printf ("ostree diff commit old: %s\n", argv[2]); if (!g_str_equal (argv[3], rpmrev2->commit)) printf ("ostree diff commit new: %s (%s)\n", argv[3], rpmrev2->commit); else printf ("ostree diff commit new: %s\n", argv[3]); rpmhdrs_diff (rpmrev1->root, rpmrev1->rpmdb, rpmrev2->root,rpmrev2->rpmdb, cancellable, error); } else if (g_str_equal (cmd, "list")) { gs_unref_ptrarray GPtrArray *patterns = NULL; int listnum = argc - 3; char **listargv = argv + 2; char *commit; // Find first commit arg. ... all before it are list args. while ((listnum > 0) && ostree_repo_resolve_rev (repo, listargv[listnum-1], TRUE, &commit, NULL) && commit) { g_free (commit); // FiXME: broken allow_noent? listnum--; } argc -= listnum; argv += listnum; if (listnum) { patterns = g_ptr_array_new (); while (listnum--) g_ptr_array_add (patterns, *listargv++); } for (; argnum < argc; ++argnum) { const char *rev = argv[argnum]; _cleanup_rpmrev_ struct RpmRevisionData *rpmrev = NULL; rpmrev = rpmrev_new (repo, rpmdbdir, rev, patterns, cancellable, error); if (!rpmrev) goto out; if (!g_str_equal (rev, rpmrev->commit)) printf ("ostree commit: %s (%s)\n", rev, rpmrev->commit); else printf ("ostree commit: %s\n", rev); rpmhdrs_list (rpmrev->root, rpmrev->rpmdb, cancellable, error); } } else { g_printerr ("rpm-ostree rpm SUB-COMMANDS:\n"); g_printerr (" diff COMMIT COMMIT\n"); g_printerr (" list [prefix-pkgname...] COMMIT...\n"); g_printerr (" version COMMIT...\n"); g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Command processing failed"); goto out; } ret = TRUE; out: if (rpmdbdir_is_tmp) (void) gs_shutil_rm_rf (rpmdbdir, NULL, NULL); if (context) g_option_context_free (context); return ret; }