# Copyright © 2007-2011 Raphaël Hertzog # Copyright © 2011 Linaro Limited # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program 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, see . package Dpkg::Path; use strict; use warnings; our $VERSION = '1.05'; our @EXPORT_OK = qw( canonpath resolve_symlink check_files_are_the_same check_directory_traversal find_command find_build_file get_control_path get_pkg_root_dir guess_pkg_root_dir relative_to_pkg_root ); use Exporter qw(import); use Errno qw(ENOENT); use File::Spec; use File::Find; use Cwd qw(realpath); use Dpkg::ErrorHandling; use Dpkg::Gettext; use Dpkg::Arch qw(get_host_arch debarch_to_debtuple); use Dpkg::IPC; =encoding utf8 =head1 NAME Dpkg::Path - some common path handling functions =head1 DESCRIPTION It provides some functions to handle various path. =head1 FUNCTIONS =over 8 =item get_pkg_root_dir($file) This function will scan upwards the hierarchy of directory to find out the directory which contains the "DEBIAN" sub-directory and it will return its path. This directory is the root directory of a package being built. If no DEBIAN subdirectory is found, it will return undef. =cut sub get_pkg_root_dir($) { my $file = shift; $file =~ s{/+$}{}; $file =~ s{/+[^/]+$}{} if not -d $file; while ($file) { return $file if -d "$file/DEBIAN"; last if $file !~ m{/}; $file =~ s{/+[^/]+$}{}; } return; } =item relative_to_pkg_root($file) Returns the filename relative to get_pkg_root_dir($file). =cut sub relative_to_pkg_root($) { my $file = shift; my $pkg_root = get_pkg_root_dir($file); if (defined $pkg_root) { $pkg_root .= '/'; return $file if ($file =~ s/^\Q$pkg_root\E//); } return; } =item guess_pkg_root_dir($file) This function tries to guess the root directory of the package build tree. It will first use get_pkg_root_dir(), but it will fallback to a more imprecise check: namely it will use the parent directory that is a sub-directory of the debian directory. It can still return undef if a file outside of the debian sub-directory is provided. =cut sub guess_pkg_root_dir($) { my $file = shift; my $root = get_pkg_root_dir($file); return $root if defined $root; $file =~ s{/+$}{}; $file =~ s{/+[^/]+$}{} if not -d $file; my $parent = $file; while ($file) { $parent =~ s{/+[^/]+$}{}; last if not -d $parent; return $file if check_files_are_the_same('debian', $parent); $file = $parent; last if $file !~ m{/}; } return; } =item check_files_are_the_same($file1, $file2, $resolve_symlink) This function verifies that both files are the same by checking that the device numbers and the inode numbers returned by stat()/lstat() are the same. If $resolve_symlink is true then stat() is used, otherwise lstat() is used. =cut sub check_files_are_the_same($$;$) { my ($file1, $file2, $resolve_symlink) = @_; return 1 if $file1 eq $file2; return 0 if ((! -e $file1) || (! -e $file2)); my (@stat1, @stat2); if ($resolve_symlink) { @stat1 = stat($file1); @stat2 = stat($file2); } else { @stat1 = lstat($file1); @stat2 = lstat($file2); } my $result = ($stat1[0] == $stat2[0]) && ($stat1[1] == $stat2[1]); return $result; } =item canonpath($file) This function returns a cleaned path. It simplifies double //, and remove /./ and /../ intelligently. For /../ it simplifies the path only if the previous element is not a symlink. Thus it should only be used on real filenames. =cut sub canonpath($) { my $path = shift; $path = File::Spec->canonpath($path); my ($v, $dirs, $file) = File::Spec->splitpath($path); my @dirs = File::Spec->splitdir($dirs); my @new; foreach my $d (@dirs) { if ($d eq '..') { if (scalar(@new) > 0 and $new[-1] ne '..') { next if $new[-1] eq ''; # Root directory has no parent my $parent = File::Spec->catpath($v, File::Spec->catdir(@new), ''); if (not -l $parent) { pop @new; } else { push @new, $d; } } else { push @new, $d; } } else { push @new, $d; } } return File::Spec->catpath($v, File::Spec->catdir(@new), $file); } =item $newpath = resolve_symlink($symlink) Return the filename of the file pointed by the symlink. The new name is canonicalized by canonpath(). =cut sub resolve_symlink($) { my $symlink = shift; my $content = readlink($symlink); return unless defined $content; if (File::Spec->file_name_is_absolute($content)) { return canonpath($content); } else { my ($link_v, $link_d, $link_f) = File::Spec->splitpath($symlink); my ($cont_v, $cont_d, $cont_f) = File::Spec->splitpath($content); my $new = File::Spec->catpath($link_v, $link_d . '/' . $cont_d, $cont_f); return canonpath($new); } } =item check_directory_traversal($basedir, $dir) This function verifies that the directory $dir does not contain any symlink that goes beyond $basedir (which should be either equal or a parent of $dir). =cut sub check_directory_traversal { my ($basedir, $dir) = @_; my $canon_basedir = realpath($basedir); # On Solaris /dev/null points to /devices/pseudo/mm@0:null. my $canon_devnull = realpath('/dev/null'); my $check_symlinks = sub { my $canon_pathname = realpath($_); if (not defined $canon_pathname) { return if $! == ENOENT; syserr(g_("pathname '%s' cannot be canonicalized"), $_); } return if $canon_pathname eq $canon_devnull; return if $canon_pathname eq $canon_basedir; return if $canon_pathname =~ m{^\Q$canon_basedir/\E}; error(g_("pathname '%s' points outside source root (to '%s')"), $_, $canon_pathname); }; find({ wanted => $check_symlinks, no_chdir => 1, follow => 1, follow_skip => 2, }, $dir); return; } =item $cmdpath = find_command($command) Return the path of the command if defined and available on an absolute or relative path or on the $PATH, undef otherwise. =cut sub find_command($) { my $cmd = shift; return if not $cmd; if ($cmd =~ m{/}) { return "$cmd" if -x "$cmd"; } else { foreach my $dir (split(/:/, $ENV{PATH})) { return "$dir/$cmd" if -x "$dir/$cmd"; } } return; } =item $control_file = get_control_path($pkg, $filetype) Return the path of the control file of type $filetype for the given package. =item @control_files = get_control_path($pkg) Return the path of all available control files for the given package. =cut sub get_control_path($;$) { my ($pkg, $filetype) = @_; my $control_file; my @exec = ('dpkg-query', '--control-path', $pkg); push @exec, $filetype if defined $filetype; spawn(exec => \@exec, wait_child => 1, to_string => \$control_file); chomp($control_file); if (defined $filetype) { return if $control_file eq ''; return $control_file; } return () if $control_file eq ''; return split(/\n/, $control_file); } =item $file = find_build_file($basename) Selects the right variant of the given file: the arch-specific variant ("$basename.$arch") has priority over the OS-specific variant ("$basename.$os") which has priority over the default variant ("$basename"). If none of the files exists, then it returns undef. =item @files = find_build_file($basename) Return the available variants of the given file. Returns an empty list if none of the files exists. =cut sub find_build_file($) { my $base = shift; my $host_arch = get_host_arch(); my ($abi, $libc, $host_os, $cpu) = debarch_to_debtuple($host_arch); my @files; foreach my $f ("$base.$host_arch", "$base.$host_os", "$base") { push @files, $f if -f $f; } return @files if wantarray; return $files[0] if scalar @files; return; } =back =head1 CHANGES =head2 Version 1.05 (dpkg 1.20.4) New function: check_directory_traversal(). =head2 Version 1.04 (dpkg 1.17.11) Update semantics: find_command() now handles an empty or undef argument. =head2 Version 1.03 (dpkg 1.16.1) New function: find_build_file() =head2 Version 1.02 (dpkg 1.16.0) New function: get_control_path() =head2 Version 1.01 (dpkg 1.15.8) New function: find_command() =head2 Version 1.00 (dpkg 1.15.6) Mark the module as public. =cut 1;