d8b7236f58
This should be safe because we dont allow empty directories. So installing a directory into an illegal place would error. The moment a keepdir is done it gets a file in it and then we validate this file instead. So this all works out nicely. Change-Id: I205b21d21a08f0e6afa702f0dc28b8f1d54f047b Reviewed-on: https://galileo.mailstation.de/gerrit/4660 Reviewed-by: Bo Ørsted Andresen <zlin@exherbo.org>
503 lines
16 KiB
C++
503 lines
16 KiB
C++
/* vim: set sw=4 sts=4 et foldmethod=syntax : */
|
|
|
|
/*
|
|
* Copyright (c) 2010, 2011 Ciaran McCreesh
|
|
*
|
|
* This file is part of the Paludis package manager. Paludis is free software;
|
|
* you can redistribute it and/or modify it under the terms of the GNU General
|
|
* Public License version 2, as published by the Free Software Foundation.
|
|
*
|
|
* Paludis 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, write to the Free Software Foundation, Inc., 59 Temple
|
|
* Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#include <paludis/merger.hh>
|
|
#include <paludis/util/stringify.hh>
|
|
#include <paludis/util/exception.hh>
|
|
#include <paludis/util/log.hh>
|
|
#include <paludis/util/pimp-impl.hh>
|
|
#include <paludis/util/timestamp.hh>
|
|
#include <paludis/util/fs_iterator.hh>
|
|
#include <paludis/util/fs_stat.hh>
|
|
#include <paludis/selinux/security_context.hh>
|
|
#include <paludis/environment.hh>
|
|
#include <paludis/hook.hh>
|
|
#include <set>
|
|
#include <istream>
|
|
#include <ostream>
|
|
|
|
using namespace paludis;
|
|
|
|
MergerError::MergerError(const std::string & m) noexcept :
|
|
Exception(m)
|
|
{
|
|
}
|
|
|
|
namespace paludis
|
|
{
|
|
template <>
|
|
struct Imp<Merger>
|
|
{
|
|
MergerParams params;
|
|
bool result;
|
|
bool skip_dir;
|
|
|
|
std::set<FSPath, FSPathComparator> fixed_entries;
|
|
|
|
Imp(const MergerParams & p) :
|
|
params(p),
|
|
result(true),
|
|
skip_dir(false)
|
|
{
|
|
}
|
|
};
|
|
}
|
|
|
|
#include <paludis/merger-se.cc>
|
|
|
|
Merger::Merger(const MergerParams & p) :
|
|
_imp(p)
|
|
{
|
|
}
|
|
|
|
Merger::~Merger() = default;
|
|
|
|
Hook
|
|
Merger::extend_hook(const Hook & h)
|
|
{
|
|
return h;
|
|
}
|
|
|
|
bool
|
|
Merger::check()
|
|
{
|
|
Context context("When checking merge from '" + stringify(_imp->params.image()) + "' to '"
|
|
+ stringify(_imp->params.root()) + "':");
|
|
|
|
if (0 != _imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_check_pre")
|
|
("INSTALL_SOURCE", stringify(_imp->params.image()))
|
|
("INSTALL_DESTINATION", stringify(_imp->params.root()))),
|
|
_imp->params.maybe_output_manager()).max_exit_status())
|
|
make_check_fail();
|
|
|
|
do_dir_recursive(true, _imp->params.image(), _imp->params.root() / _imp->params.install_under());
|
|
|
|
if (0 != _imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_check_post")
|
|
("INSTALL_SOURCE", stringify(_imp->params.image()))
|
|
("INSTALL_DESTINATION", stringify(_imp->params.root()))),
|
|
_imp->params.maybe_output_manager()).max_exit_status())
|
|
make_check_fail();
|
|
|
|
return _imp->result;
|
|
}
|
|
|
|
void
|
|
Merger::merge()
|
|
{
|
|
Context context("When performing merge from '" + stringify(_imp->params.image()) + "' to '"
|
|
+ stringify(_imp->params.root()) + "':");
|
|
|
|
if (0 != _imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_install_pre")
|
|
("INSTALL_SOURCE", stringify(_imp->params.image()))
|
|
("INSTALL_DESTINATION", stringify(_imp->params.root()))),
|
|
_imp->params.maybe_output_manager()).max_exit_status())
|
|
Log::get_instance()->message("merger.pre_hooks.failure", ll_warning, lc_context) <<
|
|
"Merge of '" << _imp->params.image() << "' to '" << _imp->params.root() << "' pre hooks returned non-zero";
|
|
|
|
prepare_install_under();
|
|
|
|
if (! _imp->params.no_chown())
|
|
do_ownership_fixes_recursive(_imp->params.image());
|
|
|
|
do_dir_recursive(false, _imp->params.image(), canonicalise_root_path(_imp->params.root() / _imp->params.install_under()));
|
|
on_done_merge();
|
|
|
|
if (0 != _imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_install_post")
|
|
("INSTALL_SOURCE", stringify(_imp->params.image()))
|
|
("INSTALL_DESTINATION", stringify(_imp->params.root()))),
|
|
_imp->params.maybe_output_manager()).max_exit_status())
|
|
Log::get_instance()->message("merger.post_hooks.failure", ll_warning, lc_context) <<
|
|
"Merge of '" << _imp->params.image() << "' to '" << _imp->params.root() << "' post hooks returned non-zero";
|
|
}
|
|
|
|
|
|
void
|
|
Merger::make_check_fail()
|
|
{
|
|
_imp->result = false;
|
|
}
|
|
|
|
void
|
|
Merger::do_dir_recursive(bool is_check, const FSPath & src, const FSPath & dst)
|
|
{
|
|
Context context("When " + stringify(is_check ? "checking" : "performing") + " merge from '" +
|
|
stringify(src) + "' to '" + stringify(dst) + "':");
|
|
|
|
if (! src.stat().is_directory())
|
|
throw MergerError("Source directory '" + stringify(src) + "' is not a directory");
|
|
|
|
on_enter_dir(is_check, src);
|
|
|
|
FSIterator d(src, { fsio_include_dotfiles, fsio_inode_sort }), d_end;
|
|
|
|
if (is_check)
|
|
{
|
|
if (d == d_end && dst != _imp->params.root().realpath())
|
|
{
|
|
if (_imp->params.options()[mo_allow_empty_dirs])
|
|
Log::get_instance()->message("merger.empty_directory", ll_warning, lc_context) << "Installing empty directory '"
|
|
<< stringify(dst) << "'";
|
|
else
|
|
on_error(is_check, "Attempted to install empty directory '" + stringify(dst) + "'");
|
|
}
|
|
}
|
|
|
|
for ( ; d != d_end ; ++d)
|
|
{
|
|
EntryType m(entry_type(*d));
|
|
switch (m)
|
|
{
|
|
case et_sym:
|
|
on_sym(is_check, *d, dst);
|
|
continue;
|
|
|
|
case et_file:
|
|
on_file(is_check, *d, dst);
|
|
continue;
|
|
|
|
case et_dir:
|
|
on_dir(is_check, *d, dst);
|
|
if (_imp->result)
|
|
{
|
|
if (! _imp->skip_dir)
|
|
do_dir_recursive(is_check, *d,
|
|
is_check ? (dst / d->basename()) : canonicalise_root_path(dst / d->basename()));
|
|
else
|
|
_imp->skip_dir = false;
|
|
}
|
|
continue;
|
|
|
|
case et_misc:
|
|
on_misc(is_check, *d, dst);
|
|
continue;
|
|
|
|
case et_nothing:
|
|
case last_et:
|
|
;
|
|
}
|
|
|
|
throw InternalError(PALUDIS_HERE, "Unexpected entry_type '" + stringify(m) + "'");
|
|
}
|
|
|
|
on_leave_dir(is_check, src);
|
|
}
|
|
|
|
void
|
|
Merger::on_enter_dir(bool, const FSPath)
|
|
{
|
|
}
|
|
|
|
void
|
|
Merger::on_leave_dir(bool, const FSPath)
|
|
{
|
|
}
|
|
|
|
EntryType
|
|
Merger::entry_type(const FSPath & f)
|
|
{
|
|
Context context("When checking type of '" + stringify(f) + "':");
|
|
|
|
FSStat f_stat(f);
|
|
|
|
if (! f_stat.exists())
|
|
return et_nothing;
|
|
|
|
if (f_stat.is_symlink())
|
|
return et_sym;
|
|
|
|
if (f_stat.is_regular_file())
|
|
return et_file;
|
|
|
|
if (f_stat.is_directory())
|
|
return et_dir;
|
|
|
|
return et_misc;
|
|
}
|
|
|
|
void
|
|
Merger::on_file(bool is_check, const FSPath & src, const FSPath & dst)
|
|
{
|
|
Context context("When handling file '" + stringify(src) + "' to '" + stringify(dst) + "':");
|
|
const auto staged(dst / src.basename());
|
|
|
|
if (is_check)
|
|
{
|
|
if (0 != _imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_check_file_pre")
|
|
("INSTALL_SOURCE", stringify(src))
|
|
("INSTALL_DESTINATION", stringify(staged))),
|
|
_imp->params.maybe_output_manager()).max_exit_status())
|
|
make_check_fail();
|
|
|
|
if (! _imp->params.permit_destination()(dst.strip_leading(_imp->params.root().realpath())))
|
|
on_error(is_check, "Not allowed to merge '" + stringify(src) + "' to '" + stringify(dst) + "'");
|
|
}
|
|
|
|
if (! is_check)
|
|
{
|
|
HookResult hr(_imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_install_file_override")
|
|
("INSTALL_SOURCE", stringify(src))
|
|
("INSTALL_DESTINATION", stringify(staged))
|
|
.grab_output(Hook::AllowedOutputValues()("skip"))),
|
|
_imp->params.maybe_output_manager()));
|
|
|
|
if (hr.max_exit_status() != 0)
|
|
Log::get_instance()->message("merger.file.skip_hooks.failure", ll_warning, lc_context) << "Merge of '"
|
|
<< stringify(src) << "' to '" << stringify(dst) << "' skip hooks returned non-zero";
|
|
else if (hr.output() == "skip")
|
|
{
|
|
std::string tidy(stringify(staged.strip_leading(_imp->params.root().realpath())));
|
|
display_override("--- [skp] " + tidy);
|
|
return;
|
|
}
|
|
}
|
|
|
|
FSStat src_stat(src);
|
|
if (is_check && src_stat.mtim() < _imp->params.fix_mtimes_before())
|
|
src.utime(_imp->params.fix_mtimes_before());
|
|
|
|
on_file_main(is_check, FSPath(stringify(src)), dst);
|
|
|
|
if (is_check &&
|
|
0 != _imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_check_file_post")
|
|
("INSTALL_SOURCE", stringify(src))
|
|
("INSTALL_DESTINATION", stringify(staged))),
|
|
_imp->params.maybe_output_manager()).max_exit_status())
|
|
make_check_fail();
|
|
}
|
|
|
|
void
|
|
Merger::on_dir(bool is_check, const FSPath & src, const FSPath & dst)
|
|
{
|
|
Context context("When handling dir '" + stringify(src) + "' to '" + stringify(dst) + "':");
|
|
const auto staged(dst / src.basename());
|
|
|
|
if (is_check &&
|
|
0 != _imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_check_dir_pre")
|
|
("INSTALL_SOURCE", stringify(src))
|
|
("INSTALL_DESTINATION", stringify(staged))),
|
|
_imp->params.maybe_output_manager()).max_exit_status())
|
|
make_check_fail();
|
|
|
|
if (! is_check)
|
|
{
|
|
HookResult hr(_imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_install_dir_override")
|
|
("INSTALL_SOURCE", stringify(src))
|
|
("INSTALL_DESTINATION", stringify(staged))
|
|
.grab_output(Hook::AllowedOutputValues()("skip"))),
|
|
_imp->params.maybe_output_manager()));
|
|
|
|
if (hr.max_exit_status() != 0)
|
|
Log::get_instance()->message("merger.dir.skip_hooks.failure", ll_warning, lc_context) << "Merge of '"
|
|
<< stringify(src) << "' to '" << stringify(dst) << "' skip hooks returned non-zero";
|
|
else if (hr.output() == "skip")
|
|
{
|
|
std::string tidy(stringify(staged.strip_leading(_imp->params.root().realpath())));
|
|
display_override("--- [skp] " + tidy);
|
|
_imp->skip_dir = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
on_dir_main(is_check, src, dst);
|
|
|
|
if (is_check &&
|
|
0 != _imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_check_dir_post")
|
|
("INSTALL_SOURCE", stringify(src))
|
|
("INSTALL_DESTINATION", stringify(staged))),
|
|
_imp->params.maybe_output_manager()).max_exit_status())
|
|
make_check_fail();
|
|
}
|
|
|
|
void
|
|
Merger::on_sym(bool is_check, const FSPath & src, const FSPath & dst)
|
|
{
|
|
Context context("When handling sym '" + stringify(src) + "' to '" + stringify(dst) + "':");
|
|
const auto staged(dst / src.basename());
|
|
|
|
if (is_check)
|
|
{
|
|
if (0 != _imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_check_sym_pre")
|
|
("INSTALL_SOURCE", stringify(src))
|
|
("INSTALL_DESTINATION", stringify(staged))),
|
|
_imp->params.maybe_output_manager()).max_exit_status())
|
|
make_check_fail();
|
|
|
|
if (! _imp->params.permit_destination()(dst.strip_leading(_imp->params.root().realpath())))
|
|
on_error(is_check, "Not allowed to merge '" + stringify(src) + "' to '" + stringify(dst) + "'");
|
|
}
|
|
|
|
if (! is_check)
|
|
{
|
|
HookResult hr(_imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_install_sym_override")
|
|
("INSTALL_SOURCE", stringify(src))
|
|
("INSTALL_DESTINATION", stringify(staged))
|
|
.grab_output(Hook::AllowedOutputValues()("skip"))),
|
|
_imp->params.maybe_output_manager()));
|
|
|
|
if (hr.max_exit_status() != 0)
|
|
Log::get_instance()->message("merger.sym.skip_hooks.failure", ll_warning, lc_context) << "Merge of '"
|
|
<< stringify(src) << "' to '" << stringify(dst) << "' skip hooks returned non-zero";
|
|
else if (hr.output() == "skip")
|
|
{
|
|
std::string tidy(stringify(staged.strip_leading(_imp->params.root().realpath())));
|
|
display_override("--- [skp] " + tidy);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (symlink_needs_rewriting(src))
|
|
{
|
|
if (! _imp->params.options()[mo_rewrite_symlinks])
|
|
on_error(is_check, "Symlink to image detected at: " + stringify(src) + " (" + src.readlink() + ")");
|
|
else if (is_check)
|
|
rewrite_symlink_as_needed(src, dst);
|
|
}
|
|
|
|
on_sym_main(is_check, FSPath(stringify(src)), dst);
|
|
|
|
if (is_check &&
|
|
0 != _imp->params.environment()->perform_hook(extend_hook(
|
|
Hook("merger_check_sym_post")
|
|
("INSTALL_SOURCE", stringify(src))
|
|
("INSTALL_DESTINATION", stringify(staged))),
|
|
_imp->params.maybe_output_manager()).max_exit_status())
|
|
make_check_fail();
|
|
}
|
|
|
|
void
|
|
Merger::on_misc(bool is_check, const FSPath & src, const FSPath & dst)
|
|
{
|
|
Context context("When handling misc '" + stringify(src) + "' to '" + stringify(dst) + "':");
|
|
|
|
on_error(is_check, "Cannot write '" + stringify(src) + "' to '" + stringify(dst) +
|
|
"' because it is not a recognised file type");
|
|
}
|
|
|
|
bool
|
|
Merger::symlink_needs_rewriting(const FSPath & sym)
|
|
{
|
|
std::string target(sym.readlink());
|
|
std::string real_image(stringify(_imp->params.image().realpath()));
|
|
|
|
return (0 == target.compare(0, real_image.length(), real_image));
|
|
}
|
|
|
|
void
|
|
Merger::set_skipped_dir(const bool value)
|
|
{
|
|
_imp->skip_dir = value;
|
|
}
|
|
|
|
void
|
|
Merger::do_ownership_fixes_recursive(const FSPath & dir)
|
|
{
|
|
for (FSIterator d(dir, { fsio_include_dotfiles, fsio_inode_sort }), d_end ; d != d_end ; ++d)
|
|
{
|
|
std::pair<uid_t, gid_t> new_ids(_imp->params.get_new_ids_or_minus_one()(*d));
|
|
if (uid_t(-1) != new_ids.first || gid_t(-1) != new_ids.second)
|
|
{
|
|
FSPath f(*d);
|
|
FSStat f_stat(f);
|
|
f.lchown(new_ids.first, new_ids.second);
|
|
|
|
if (et_sym != entry_type(*d))
|
|
{
|
|
mode_t mode(f_stat.permissions());
|
|
|
|
if (et_dir == entry_type(*d))
|
|
{
|
|
if (uid_t(-1) != new_ids.first)
|
|
mode &= ~S_ISUID;
|
|
if (gid_t(-1) != new_ids.second)
|
|
mode &= ~S_ISGID;
|
|
}
|
|
|
|
f.chmod(mode); /* set*id */
|
|
}
|
|
|
|
_imp->fixed_entries.insert(f);
|
|
}
|
|
|
|
EntryType m(entry_type(*d));
|
|
switch (m)
|
|
{
|
|
case et_sym:
|
|
case et_file:
|
|
continue;
|
|
|
|
case et_dir:
|
|
do_ownership_fixes_recursive(*d);
|
|
continue;
|
|
|
|
case et_misc:
|
|
throw MergerError("Unexpected 'et_misc' entry found at: " + stringify(*d));
|
|
|
|
case et_nothing:
|
|
case last_et:
|
|
break;
|
|
}
|
|
|
|
throw InternalError(PALUDIS_HERE, "Unexpected entry_type '" + stringify(m) + "'");
|
|
}
|
|
}
|
|
|
|
bool
|
|
Merger::fixed_ownership_for(const FSPath & f)
|
|
{
|
|
return _imp->fixed_entries.end() != _imp->fixed_entries.find(f);
|
|
}
|
|
|
|
void
|
|
Merger::on_done_merge()
|
|
{
|
|
}
|
|
|
|
void
|
|
Merger::rewrite_symlink_as_needed(const FSPath & src, const FSPath & dst_dir)
|
|
{
|
|
if (! symlink_needs_rewriting(src))
|
|
return;
|
|
|
|
FSCreateCon createcon(MatchPathCon::get_instance()->match(stringify(dst_dir / src.basename()), S_IFLNK));
|
|
|
|
FSPath real_image(_imp->params.image().realpath());
|
|
FSPath dst(src.readlink());
|
|
std::string fixed_dst(stringify(dst.strip_leading(real_image)));
|
|
|
|
Log::get_instance()->message("merger.rewriting_symlink", ll_qa, lc_context) << "Rewriting bad symlink: "
|
|
<< src << " -> " << dst << " to " << fixed_dst;
|
|
|
|
FSPath s(src);
|
|
s.unlink();
|
|
s.symlink(fixed_dst);
|
|
}
|
|
|