# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2015-2021 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Implement the tar source handler."""
import os
import re
import tarfile
from pathlib import Path
from typing import Iterator, List, Optional
from overrides import overrides
from craft_parts.dirs import ProjectDirs
from . import errors
from .base import FileSourceHandler
[docs]class TarSource(FileSourceHandler):
"""The tar source handler."""
# pylint: disable=too-many-arguments
def __init__(
self,
source: str,
part_src_dir: Path,
*,
cache_dir: Path,
project_dirs: ProjectDirs,
source_tag: Optional[str] = None,
source_commit: Optional[str] = None,
source_branch: Optional[str] = None,
source_depth: Optional[int] = None,
source_checksum: Optional[str] = None,
source_submodules: Optional[List[str]] = None,
ignore_patterns: Optional[List[str]] = None,
):
super().__init__(
source,
part_src_dir,
cache_dir=cache_dir,
source_tag=source_tag,
source_commit=source_commit,
source_branch=source_branch,
source_depth=source_depth,
source_checksum=source_checksum,
source_submodules=source_submodules,
project_dirs=project_dirs,
ignore_patterns=ignore_patterns,
)
if source_tag:
raise errors.InvalidSourceOption(source_type="tar", option="source-tag")
if source_commit:
raise errors.InvalidSourceOption(source_type="tar", option="source-commit")
if source_branch:
raise errors.InvalidSourceOption(source_type="tar", option="source-branch")
if source_depth:
raise errors.InvalidSourceOption(source_type="tar", option="source-depth")
# pylint: enable=too-many-arguments
[docs] @overrides
def provision(
self,
dst: Path,
keep: bool = False,
src: Optional[Path] = None,
) -> None:
"""Extract tarball contents to the part source dir."""
if src:
tarball = src
else:
tarball = self.part_src_dir / os.path.basename(self.source)
_extract(tarball, dst)
if not keep:
os.remove(tarball)
def _extract(tarball: Path, dst: Path) -> None:
with tarfile.open(tarball) as tar:
def filter_members(tar: tarfile.TarFile) -> Iterator[tarfile.TarInfo]:
"""Strip common prefix and ban dangerous names."""
members = tar.getmembers()
common = os.path.commonprefix([m.name for m in members])
# commonprefix() works a character at a time and will
# consider "d/ab" and "d/abc" to have common prefix "d/ab";
# check all members either start with common dir
for member in members:
if not (
member.name.startswith(common + "/")
or member.isdir()
and member.name == common
):
# commonprefix() didn't return a dir name; go up one
# level
common = os.path.dirname(common)
break
for member in members:
if member.name == common:
continue
_strip_prefix(common, member)
# We mask all files to be writable to be able to easily
# extract on top.
member.mode = member.mode | 0o200
yield member
# ignore type, members expect List but we're providing Generator
tar.extractall(members=filter_members(tar), path=dst) # type: ignore
def _strip_prefix(common: str, member: tarfile.TarInfo) -> None:
if member.name.startswith(common + "/"):
member.name = member.name[len(common + "/") :]
# strip leading '/', './' or '../' as many times as needed
member.name = re.sub(r"^(\.{0,2}/)*", r"", member.name)
# do the same for linkname if this is a hardlink
if member.islnk() and not member.issym():
if member.linkname.startswith(common + "/"):
member.linkname = member.linkname[len(common + "/") :]
member.linkname = re.sub(r"^(\.{0,2}/)*", r"", member.linkname)