From 34d082c010ce14168d6a2e2c396f0a4bfbac2262 Mon Sep 17 00:00:00 2001 From: Mohit Gupta Date: Tue, 1 Sep 2026 13:52:35 +0530 Subject: [PATCH] fix(schema): validate description UTF-8 byte length Signed-off-by: Mohit Gupta --- src/skillevaluator/constants.py | 3 ++- src/skillevaluator/models/skill.py | 9 ++++++++- tests/validators/test_schema.py | 22 +++++++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/skillevaluator/constants.py b/src/skillevaluator/constants.py index 5926ba41..2373418f 100644 --- a/src/skillevaluator/constants.py +++ b/src/skillevaluator/constants.py @@ -98,13 +98,14 @@ # SHARED CONSTANTS # ============================================================================= -# Field length constraints per SkillEvaluator specification +# Field limits for serialized metadata NAME_MIN_LENGTH = 1 NAME_MAX_LENGTH = 64 TITLE_MIN_LENGTH = 1 TITLE_MAX_LENGTH = 256 DESCRIPTION_MIN_LENGTH = 1 DESCRIPTION_MAX_LENGTH = 1024 +DESCRIPTION_MAX_BYTES = 1024 COMPATIBILITY_MAX_LENGTH = 500 # Maximum recommended line counts diff --git a/src/skillevaluator/models/skill.py b/src/skillevaluator/models/skill.py index 8ac6a96c..2991c132 100644 --- a/src/skillevaluator/models/skill.py +++ b/src/skillevaluator/models/skill.py @@ -14,6 +14,7 @@ from skillevaluator.constants import ( COMPATIBILITY_MAX_LENGTH, + DESCRIPTION_MAX_BYTES, DESCRIPTION_MAX_LENGTH, DESCRIPTION_MIN_LENGTH, FORBIDDEN_SKILL_FIELDS, @@ -161,7 +162,13 @@ def validate_name_format(cls, v: str) -> str: @field_validator("description") @classmethod def validate_description_content(cls, v: str) -> str: - """Reject descriptions that contain XML tags.""" + """Reject descriptions that exceed the serialized limit or contain XML tags.""" + byte_length = len(v.encode("utf-8")) + if byte_length > DESCRIPTION_MAX_BYTES: + raise ValueError( + f"Description must be at most {DESCRIPTION_MAX_BYTES} UTF-8 bytes " + f"(got {byte_length} bytes)" + ) if XML_TAG_RE.search(v): raise ValueError("Skill description must not contain XML tags") return v diff --git a/tests/validators/test_schema.py b/tests/validators/test_schema.py index 78db3ad6..4e86ee36 100644 --- a/tests/validators/test_schema.py +++ b/tests/validators/test_schema.py @@ -8,6 +8,9 @@ from pathlib import Path +import pytest + +from skillevaluator.models.skill import SkillFrontmatter from skillevaluator.validators.schema import SchemaValidator @@ -184,7 +187,7 @@ def test_consecutive_hyphens_rejected(self, tmp_path: Path): assert any("consecutive" in err.lower() or "hyphen" in err.lower() for err in result.errors) def test_description_length_constraints(self, tmp_path: Path): - """Test validation enforces description length constraints (1-1024 chars).""" + """Test validation enforces description length constraints (1-1024 UTF-8 bytes).""" skill_dir = tmp_path / "long-description" skill_dir.mkdir() @@ -206,6 +209,23 @@ def test_description_length_constraints(self, tmp_path: Path): assert not result.passed assert any("1024" in err or "description" in err.lower() for err in result.errors) + def test_description_accepts_exact_utf8_byte_limit(self): + description = "a" * 1021 + "€" + + frontmatter = SkillFrontmatter(name="exact-byte-limit", description=description) + + assert len(description.encode("utf-8")) == 1024 + assert frontmatter.description == description + + def test_description_rejects_utf8_byte_overflow(self): + description = "a" * 1022 + "€" + + with pytest.raises(ValueError, match=r"at most 1024 UTF-8 bytes \(got 1025 bytes\)"): + SkillFrontmatter(name="byte-overflow", description=description) + + assert len(description) == 1023 + assert len(description.encode("utf-8")) == 1025 + def test_metadata_author_validation(self, tmp_path: Path): """Test author format fails for malformed (no email) author under default profile.""" from skillevaluator.models.result import Severity