Files
platformio-core/platformio/package/manifest/schema.py
T

283 lines
8.8 KiB
Python
Raw Normal View History

2019-10-17 00:17:16 +03:00
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
2019-12-29 14:18:43 +02:00
# pylint: disable=too-many-ancestors
import json
import re
import marshmallow
2019-10-17 00:17:16 +03:00
import requests
import semantic_version
from marshmallow import Schema, ValidationError, fields, validate, validates
2020-08-22 17:48:49 +03:00
from platformio.clients.http import fetch_remote_content
2019-10-17 00:17:16 +03:00
from platformio.package.exception import ManifestValidationError
from platformio.util import memoized
class BaseSchema(Schema):
class Meta(object): # pylint: disable=no-init
unknown = marshmallow.EXCLUDE # pylint: disable=no-member
def load_manifest(self, data):
return self.load(data)
def handle_error(self, error, data, **_): # pylint: disable=arguments-differ
raise ManifestValidationError(
error.messages,
data,
error.valid_data if hasattr(error, "valid_data") else error.data,
)
class StrictSchema(BaseSchema):
2019-12-29 14:18:43 +02:00
def handle_error(self, error, data, **_): # pylint: disable=arguments-differ
2019-10-17 00:17:16 +03:00
# skip broken records
if self.many:
error.valid_data = [
2019-10-17 00:17:16 +03:00
item for idx, item in enumerate(data) if idx not in error.messages
]
else:
error.valid_data = None
2019-10-17 00:17:16 +03:00
raise error
class StrictListField(fields.List):
2019-12-29 14:18:43 +02:00
def _deserialize( # pylint: disable=arguments-differ
self, value, attr, data, **kwargs
):
2019-10-17 00:17:16 +03:00
try:
2022-04-15 14:44:30 +03:00
return super()._deserialize(value, attr, data, **kwargs)
2019-10-17 00:17:16 +03:00
except ValidationError as exc:
if exc.data:
exc.data = [item for item in exc.data if item is not None]
2019-10-17 00:17:16 +03:00
raise exc
class AuthorSchema(StrictSchema):
name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
2019-10-17 00:17:16 +03:00
email = fields.Email(validate=validate.Length(min=1, max=50))
maintainer = fields.Bool(dump_default=False)
2019-10-17 00:17:16 +03:00
url = fields.Url(validate=validate.Length(min=1, max=255))
class RepositorySchema(StrictSchema):
type = fields.Str(
required=True,
validate=validate.OneOf(
["git", "hg", "svn"],
error="Invalid repository type, please use one of [git, hg, svn]",
),
)
url = fields.Str(required=True, validate=validate.Length(min=1, max=255))
branch = fields.Str(validate=validate.Length(min=1, max=50))
class DependencySchema(StrictSchema):
owner = fields.Str(validate=validate.Length(min=1, max=100))
name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
version = fields.Str(validate=validate.Length(min=1, max=100))
authors = StrictListField(fields.Str(validate=validate.Length(min=1, max=50)))
platforms = StrictListField(
fields.Str(
validate=[
validate.Length(min=1, max=50),
validate.Regexp(
r"^([a-z\d\-_]+|\*)$", error="Only [a-z0-9-_*] chars are allowed"
),
]
)
)
frameworks = StrictListField(
fields.Str(
validate=[
validate.Length(min=1, max=50),
validate.Regexp(
r"^([a-z\d\-_]+|\*)$", error="Only [a-z0-9-_*] chars are allowed"
),
]
)
)
class ExportSchema(BaseSchema):
2019-10-17 00:17:16 +03:00
include = StrictListField(fields.Str)
exclude = StrictListField(fields.Str)
class ExampleSchema(StrictSchema):
name = fields.Str(
required=True,
validate=[
validate.Length(min=1, max=255),
2019-10-17 00:17:16 +03:00
validate.Regexp(
2020-11-22 22:32:03 +02:00
r"^[a-zA-Z\d\-\_/\. ]+$",
error="Only [a-zA-Z0-9-_/. ] chars are allowed",
2019-10-17 00:17:16 +03:00
),
],
)
base = fields.Str(required=True)
files = StrictListField(fields.Str, required=True)
# Fields
class ScriptField(fields.Field):
def _deserialize(self, value, attr, data, **kwargs):
if isinstance(value, (str, list)):
return value
raise ValidationError(
"Script value must be a command (string) or list of arguments"
)
# Scheme
class ManifestSchema(BaseSchema):
2019-10-17 00:17:16 +03:00
# Required fields
name = fields.Str(
required=True,
validate=[
validate.Length(min=1, max=100),
validate.Regexp(
r"^[^:;/,@\<\>]+$", error="The next chars [:;/,@<>] are not allowed"
),
],
)
2019-10-17 00:17:16 +03:00
version = fields.Str(required=True, validate=validate.Length(min=1, max=50))
# Optional fields
authors = fields.Nested(AuthorSchema, many=True)
description = fields.Str(validate=validate.Length(min=1, max=1000))
homepage = fields.Url(validate=validate.Length(min=1, max=255))
license = fields.Str(validate=validate.Length(min=1, max=255))
repository = fields.Nested(RepositorySchema)
dependencies = fields.Nested(DependencySchema, many=True)
scripts = fields.Dict(
keys=fields.Str(validate=validate.OneOf(["postinstall", "preuninstall"])),
values=ScriptField(),
)
# library.json
2019-10-17 00:17:16 +03:00
export = fields.Nested(ExportSchema)
examples = fields.Nested(ExampleSchema, many=True)
downloadUrl = fields.Url(validate=validate.Length(min=1, max=255))
2019-10-17 00:17:16 +03:00
keywords = StrictListField(
fields.Str(
validate=[
validate.Length(min=1, max=50),
validate.Regexp(
r"^[a-z\d\-\+\. ]+$", error="Only [a-z0-9-+. ] chars are allowed"
),
]
)
)
platforms = StrictListField(
fields.Str(
validate=[
validate.Length(min=1, max=50),
validate.Regexp(
r"^([a-z\d\-_]+|\*)$", error="Only [a-z0-9-_*] chars are allowed"
),
]
)
)
frameworks = StrictListField(
fields.Str(
validate=[
validate.Length(min=1, max=50),
validate.Regexp(
r"^([a-z\d\-_]+|\*)$", error="Only [a-z0-9-_*] chars are allowed"
),
]
)
)
headers = StrictListField(
fields.Str(
validate=[
validate.Length(min=1, max=255),
]
)
)
2019-10-17 00:17:16 +03:00
# platform.json specific
title = fields.Str(validate=validate.Length(min=1, max=100))
# package.json specific
system = StrictListField(
fields.Str(
validate=[
validate.Length(min=1, max=50),
validate.Regexp(
r"^[a-z\d\-_]+$", error="Only [a-z0-9-_] chars are allowed"
),
]
)
)
@validates("version")
def validate_version(self, value): # pylint: disable=no-self-use
try:
value = str(value)
assert "." in value
# check leading zeros
try:
semantic_version.Version(value)
except ValueError as exc:
if "Invalid leading zero" in str(exc):
raise exc
2019-10-17 00:17:16 +03:00
semantic_version.Version.coerce(value)
except (AssertionError, ValueError):
raise ValidationError(
"Invalid semantic versioning format, see https://semver.org/"
)
@validates("license")
def validate_license(self, value):
try:
spdx = self.load_spdx_licenses()
except requests.exceptions.RequestException:
raise ValidationError("Could not load SPDX licenses for validation")
known_ids = set(item.get("licenseId") for item in spdx.get("licenses", []))
if value in known_ids:
return True
# parse license expression
# https://spdx.github.io/spdx-spec/SPDX-license-expressions/
package_ids = [
item.strip()
for item in re.sub(r"(\s+(?:OR|AND|WITH)\s+|[\(\)])", " ", value).split(" ")
if item.strip()
]
if known_ids >= set(package_ids):
return True
2019-10-17 00:17:16 +03:00
raise ValidationError(
"Invalid SPDX license identifier. See valid identifiers at "
"https://spdx.org/licenses/"
)
@staticmethod
@memoized(expire="1h")
def load_spdx_licenses():
2022-05-09 10:08:08 +03:00
version = "3.17"
2021-03-03 21:31:42 +02:00
spdx_data_url = (
2021-03-04 18:52:02 +02:00
"https://raw.githubusercontent.com/spdx/license-list-data/"
2021-03-03 21:31:42 +02:00
"v%s/json/licenses.json" % version
)
2020-08-22 17:48:49 +03:00
return json.loads(fetch_remote_content(spdx_data_url))