diff options
author | Tiago Gomes <tiago.gomes@codethink.co.uk> | 2018-07-31 12:34:25 +0100 |
---|---|---|
committer | Tiago Gomes <tiago.avv@gmail.com> | 2018-08-02 11:24:43 +0000 |
commit | e788bda4969de17178f3facc0d31b36c91121402 (patch) | |
tree | 0c94dc6911bb3efa0ab768a15b3f00120079662e | |
parent | 039d43e43431957a42193f12f06acb7b95c2eae4 (diff) | |
download | buildstream-e788bda4969de17178f3facc0d31b36c91121402.tar.gz |
plugin: bake API to get and validate a project path
A project path is a path relative to a project directory.
A project path can not also refer to the parent directory in the first
path component, or point to symbolic links, fifos, sockets and
block/character devices.
-rw-r--r-- | .pylintrc | 2 | ||||
-rw-r--r-- | buildstream/_exceptions.py | 10 | ||||
-rw-r--r-- | buildstream/_yaml.py | 92 | ||||
-rw-r--r-- | buildstream/plugin.py | 47 |
4 files changed, 147 insertions, 4 deletions
@@ -184,7 +184,7 @@ ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local,contextlib.closing,gi.repository.GLib.GError +ignored-classes=optparse.Values,thread._local,_thread._local,contextlib.closing,gi.repository.GLib.GError,pathlib.PurePath # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime diff --git a/buildstream/_exceptions.py b/buildstream/_exceptions.py index e55d942fd..5187357c5 100644 --- a/buildstream/_exceptions.py +++ b/buildstream/_exceptions.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2016 Codethink Limited +# Copyright (C) 2018 Codethink Limited # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,6 +16,7 @@ # # Authors: # Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> +# Tiago Gomes <tiago.gomes@codethink.co.uk> from enum import Enum @@ -206,6 +207,13 @@ class LoadErrorReason(Enum): # Try to load a directory not a yaml file LOADING_DIRECTORY = 18 + # A project path leads outside of the project directory + PROJ_PATH_INVALID = 19 + + # A project path points to a file of the not right kind (e.g. a + # socket) + PROJ_PATH_INVALID_KIND = 20 + # LoadError # diff --git a/buildstream/_yaml.py b/buildstream/_yaml.py index 0e090e2e7..33ee444aa 100644 --- a/buildstream/_yaml.py +++ b/buildstream/_yaml.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2016 Codethink Limited +# Copyright (C) 2018 Codethink Limited # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -22,6 +22,7 @@ import collections import string from copy import deepcopy from contextlib import ExitStack +from pathlib import Path from ruamel import yaml from ruamel.yaml.representer import SafeRepresenter, RoundTripRepresenter @@ -392,6 +393,95 @@ def node_get(node, expected_type, key, indices=None, default_value=_get_sentinel return value +# node_get_project_path() +# +# Fetches a project path from a dictionary node and validates it +# +# Paths are asserted to never lead to a directory outside of the project +# directory. In addition, paths can not point to symbolic links, fifos, +# sockets and block/character devices. +# +# The `check_is_file` and `check_is_dir` parameters can be used to +# perform additional validations on the path. Note that an exception +# will always be raised if both parameters are set to ``True``. +# +# Args: +# node (dict): A dictionary loaded from YAML +# key (str): The key whose value contains a path to validate +# project_dir (str): The project directory +# check_is_file (bool): If ``True`` an error will also be raised +# if path does not point to a regular file. +# Defaults to ``False`` +# check_is_dir (bool): If ``True`` an error will be also raised +# if path does not point to a directory. +# Defaults to ``False`` +# Returns: +# (str): The project path +# +# Raises: +# (LoadError): In case that the project path is not valid or does not +# exist +# +def node_get_project_path(node, key, project_dir, *, + check_is_file=False, check_is_dir=False): + path_str = node_get(node, str, key) + path = Path(path_str) + project_dir_path = Path(project_dir) + + provenance = node_get_provenance(node, key=key) + + if (project_dir_path / path).is_symlink(): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' must not point to " + "symbolic links " + .format(provenance, path_str)) + + if path.parts and path.parts[0] == '..': + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID, + "{}: Specified path '{}' first component must " + "not be '..'" + .format(provenance, path_str)) + + try: + if sys.version_info[0] == 3 and sys.version_info[1] < 6: + full_resolved_path = (project_dir_path / path).resolve() + else: + full_resolved_path = (project_dir_path / path).resolve(strict=True) + except FileNotFoundError: + raise LoadError(LoadErrorReason.MISSING_FILE, + "{}: Specified path '{}' does not exist" + .format(provenance, path_str)) + + is_inside = project_dir_path in full_resolved_path.parents or ( + full_resolved_path == project_dir_path) + + if path.is_absolute() or not is_inside: + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID, + "{}: Specified path '{}' must not lead outside of the " + "project directory" + .format(provenance, path_str)) + + if full_resolved_path.is_socket() or ( + full_resolved_path.is_fifo() or + full_resolved_path.is_block_device()): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' points to an unsupported " + "file kind" + .format(provenance, path_str)) + + if check_is_file and not full_resolved_path.is_file(): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' is not a regular file" + .format(provenance, path_str)) + + if check_is_dir and not full_resolved_path.is_dir(): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' is not a directory" + .format(provenance, path_str)) + + return path_str + + # node_items() # # A convenience generator for iterating over loaded key/value diff --git a/buildstream/plugin.py b/buildstream/plugin.py index 155a9500e..836b60834 100644 --- a/buildstream/plugin.py +++ b/buildstream/plugin.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2017 Codethink Limited +# Copyright (C) 2018 Codethink Limited # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -335,6 +335,51 @@ class Plugin(): """ return _yaml.node_get(node, expected_type, member_name, default_value=default) + def node_get_project_path(self, node, key, *, + check_is_file=False, check_is_dir=False): + """Fetches a project path from a dictionary node and validates it + + Paths are asserted to never lead to a directory outside of the + project directory. In addition, paths can not point to symbolic + links, fifos, sockets and block/character devices. + + The `check_is_file` and `check_is_dir` parameters can be used to + perform additional validations on the path. Note that an + exception will always be raised if both parameters are set to + ``True``. + + Args: + node (dict): A dictionary loaded from YAML + key (str): The key whose value contains a path to validate + check_is_file (bool): If ``True`` an error will also be raised + if path does not point to a regular file. + Defaults to ``False`` + check_is_dir (bool): If ``True`` an error will also be raised + if path does not point to a directory. + Defaults to ``False`` + + Returns: + (str): The project path + + Raises: + :class:`.LoadError`: In the case that the project path is not + valid or does not exist + + *Since: 1.2* + + **Example:** + + .. code:: python + + path = self.node_get_project_path(node, 'path') + + """ + + return _yaml.node_get_project_path(node, key, + self.__project.directory, + check_is_file=check_is_file, + check_is_dir=check_is_dir) + def node_validate(self, node, valid_keys): """This should be used in :func:`~buildstream.plugin.Plugin.configure` implementations to assert that users have only entered |