Manifest: transform path before checking against the zip file list #171

Merged
Oleg-Komarov merged 1 commits from manifest-canonical-path into main 2024-06-06 14:48:14 +02:00
3 changed files with 110 additions and 6 deletions

View File

@ -102,7 +102,7 @@ EXPECTED_VALIDATION_ERRORS = {
'invalid-theme-multiple-xmls.zip': {'source': ['A theme should have exactly one XML file.']}, 'invalid-theme-multiple-xmls.zip': {'source': ['A theme should have exactly one XML file.']},
'invalid-missing-wheels.zip': { 'invalid-missing-wheels.zip': {
'source': [ 'source': [
'A declared wheel is missing in the zip file, expected path: addon/./wheels/test-wheel-whatever.whl' 'A declared wheel is missing in the zip file, expected path: addon/wheels/test-wheel-whatever.whl'
] ]
}, },
} }

View File

@ -1,5 +1,6 @@
from pathlib import Path from pathlib import Path
from unittest.mock import patch, ANY from unittest.mock import patch, ANY
import dataclasses
import tempfile import tempfile
from django.test import TestCase from django.test import TestCase
@ -11,6 +12,7 @@ from files.utils import (
find_path_by_name, find_path_by_name,
get_thumbnail_upload_to, get_thumbnail_upload_to,
make_thumbnails, make_thumbnails,
validate_file_list,
) )
# Reusing test files from the extensions app # Reusing test files from the extensions app
@ -171,3 +173,84 @@ class UtilsTest(TestCase):
mock_ffmpeg.return_value.option.return_value.input.assert_any_call( mock_ffmpeg.return_value.option.return_value.input.assert_any_call(
'path/to/source/video.mp4' 'path/to/source/video.mp4'
) )
def test_validate_file_list(self):
@dataclasses.dataclass
class TestParams:
name: str
toml_content: dict
manifest_filepath: dict
file_list: list
expected: list
for test in [
TestParams(
name='valid add-on',
toml_content={'type': 'add-on'},
manifest_filepath='blender_manifest.toml',
file_list=['__init__.py'],
expected=[],
),
TestParams(
name='add-on missing init',
toml_content={'type': 'add-on'},
manifest_filepath='blender_manifest.toml',
file_list=[],
expected=['invalid_missing_init'],
),
TestParams(
name='valid theme',
toml_content={'type': 'theme'},
manifest_filepath='blender_manifest.toml',
file_list=['theme.xml'],
expected=[],
),
TestParams(
name='missing theme file',
toml_content={'type': 'theme'},
manifest_filepath='blender_manifest.toml',
file_list=[],
expected=['missing_or_multiple_theme_xml'],
),
TestParams(
name='multiple theme files',
toml_content={'type': 'theme'},
manifest_filepath='blender_manifest.toml',
file_list=['theme.xml', 'theme/2.xml'],
expected=['missing_or_multiple_theme_xml'],
),
TestParams(
name='valid nested wheels',
toml_content={'type': 'add-on', 'wheels': ['wheels/1.whl']},
manifest_filepath='addon/blender_manifest.toml',
file_list=['addon/__init__.py', 'addon/wheels/1.whl'],
expected=[],
),
TestParams(
name='valid nested wheels with ./',
toml_content={'type': 'add-on', 'wheels': ['./wheels/1.whl']},
manifest_filepath='blender_manifest.toml',
file_list=['__init__.py', 'wheels/1.whl'],
expected=[],
),
TestParams(
name='missing wheel',
toml_content={'type': 'add-on', 'wheels': ['./wheels/1.whl', './wheels/2.whl']},
manifest_filepath='blender_manifest.toml',
file_list=['__init__.py', 'wheels/1.whl'],
expected=[{'code': 'missing_wheel', 'params': {'path': 'wheels/2.whl'}}],
),
TestParams(
name='incorrect wheel nesting',
toml_content={'type': 'add-on', 'wheels': ['./wheels/1.whl']},
manifest_filepath='add-on/blender_manifest.toml',
file_list=['add-on/__init__.py', 'wheels/1.whl'],
expected=[{'code': 'missing_wheel', 'params': {'path': 'add-on/wheels/1.whl'}}],
),
]:
with self.subTest(**dataclasses.asdict(test)):
self.assertEqual(
test.expected,
validate_file_list(test.toml_content, test.manifest_filepath, test.file_list),
test.name,
)

View File

@ -147,7 +147,14 @@ def read_manifest_from_zip(archive_path):
error_codes.append('invalid_manifest_toml') error_codes.append('invalid_manifest_toml')
return None, error_codes return None, error_codes
# If manifest was parsed successfully, do additional type-specific validation file_list_error_codes = validate_file_list(toml_content, manifest_filepath, file_list)
error_codes.extend(file_list_error_codes)
return toml_content, error_codes
def validate_file_list(toml_content, manifest_filepath, file_list):
"""Check the files in in the archive against manifest."""
error_codes = []
type_slug = toml_content['type'] type_slug = toml_content['type']
if type_slug == 'theme': if type_slug == 'theme':
theme_xmls = filter_paths_by_ext(file_list, '.xml') theme_xmls = filter_paths_by_ext(file_list, '.xml')
@ -155,22 +162,36 @@ def read_manifest_from_zip(archive_path):
error_codes.append('missing_or_multiple_theme_xml') error_codes.append('missing_or_multiple_theme_xml')
elif type_slug == 'add-on': elif type_slug == 'add-on':
# __init__.py is expected to be next to the manifest # __init__.py is expected to be next to the manifest
expected_init_path = os.path.join(os.path.dirname(manifest_filepath), '__init__.py') expected_init_path = _canonical_path('__init__.py', manifest_filepath)
init_filepath = find_exact_path(file_list, expected_init_path) init_filepath = find_exact_path(file_list, expected_init_path)
if not init_filepath: if not init_filepath:
error_codes.append('invalid_missing_init') error_codes.append('invalid_missing_init')
wheels = toml_content.get('wheels') wheels = toml_content.get('wheels')
if wheels: if wheels:
for wheel in wheels: for wheel in wheels:
expected_wheel_path = os.path.join(os.path.dirname(manifest_filepath), wheel) expected_wheel_path = _canonical_path(wheel, manifest_filepath)
wheel_filepath = find_exact_path(file_list, expected_wheel_path) wheel_filepath = find_exact_path(file_list, expected_wheel_path)
if not wheel_filepath: if not wheel_filepath:
error_codes.append( error_codes.append(
{'code': 'missing_wheel', 'params': {'path': expected_wheel_path}} {'code': 'missing_wheel', 'params': {'path': expected_wheel_path}}
) )
return error_codes
return toml_content, error_codes
def _canonical_path(path, manifest_filepath):
"""Transform path before checking against the zip file list.
We expect to support other manifest fields (e.g. in the [build] section) that will potentially
point to directories, including the "current" directory, which has to be denoted as "./".
To avoid inconsistencies in file path notations supported for different fields, we process all
paths values in manifest in a uniform way, allowing the leading "./" in all file paths.
All paths mentioned in manifest are treated as relative for the directory that contains
manifest_filepath.
"""
if path.startswith('./'):
path = path[2:]
return os.path.join(os.path.dirname(manifest_filepath), path)
def guess_mimetype_from_ext(file_name: str) -> str: def guess_mimetype_from_ext(file_name: str) -> str: