Add path manipulation and validation
This commit is contained in:
parent
8ab8bfba83
commit
00eb91e4cd
70
earthsnake/path.py
Normal file
70
earthsnake/path.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
"""Path handling"""
|
||||||
|
|
||||||
|
from .identity import Identity
|
||||||
|
from .types import ALPHA_LOWER, ALPHA_UPPER, DIGIT
|
||||||
|
|
||||||
|
PATH_PUNCTUATION = "/'()-._~!$&+,:=@%"
|
||||||
|
PATH_CHARACTER = ALPHA_LOWER + ALPHA_UPPER + DIGIT + PATH_PUNCTUATION
|
||||||
|
|
||||||
|
|
||||||
|
class Path:
|
||||||
|
"""A document path"""
|
||||||
|
|
||||||
|
_SEGMENT_PATTERN = f'/[{PATH_CHARACTER}]+'
|
||||||
|
_PATTERN = f'^({_SEGMENT_PATTERN})+$'
|
||||||
|
|
||||||
|
def __init__(self, path: str) -> None:
|
||||||
|
self.validate(path, allow_ephemeral=True)
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(path: str, allow_ephemeral: bool = False) -> None:
|
||||||
|
"""Validate a path"""
|
||||||
|
|
||||||
|
if not 2 <= len(path) <= 512:
|
||||||
|
raise ValueError('Path length must be between 2 and 512')
|
||||||
|
|
||||||
|
if not path.startswith('/'):
|
||||||
|
raise ValueError('Paths must start with a /')
|
||||||
|
|
||||||
|
if path.endswith('/'):
|
||||||
|
raise ValueError('Paths must not end with a /')
|
||||||
|
|
||||||
|
if path.startswith('/@'):
|
||||||
|
raise ValueError('Paths must not start with /@')
|
||||||
|
|
||||||
|
if '//' in path:
|
||||||
|
raise ValueError('Paths must not contain //')
|
||||||
|
|
||||||
|
if path.count('!') > 1:
|
||||||
|
raise ValueError('Only one ! is allowed in paths')
|
||||||
|
|
||||||
|
if '!' in path and not allow_ephemeral:
|
||||||
|
raise ValueError('Only ephemeral paths may contain !')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_shared(self) -> bool:
|
||||||
|
"""Check if the path is shared"""
|
||||||
|
|
||||||
|
return '~' not in self.path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_ephemeral(self) -> bool:
|
||||||
|
"""Check if the path is ephemeral"""
|
||||||
|
|
||||||
|
return '!' in self.path
|
||||||
|
|
||||||
|
def can_write(self, author: Identity) -> bool:
|
||||||
|
"""Check if a specific author has write access over a document"""
|
||||||
|
|
||||||
|
if self.is_shared:
|
||||||
|
return True
|
||||||
|
|
||||||
|
segments = self.path.split('/')
|
||||||
|
|
||||||
|
for segment in segments:
|
||||||
|
for allowed_author in segment.split('~'):
|
||||||
|
if Identity.valid_address(allowed_author) and str(author) == allowed_author:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
@ -2,6 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
ALPHA_LOWER = 'abcdefghijklmnopqrstuvwxyz'
|
ALPHA_LOWER = 'abcdefghijklmnopqrstuvwxyz'
|
||||||
|
ALPHA_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||||
DIGIT = '0123456789'
|
DIGIT = '0123456789'
|
||||||
B32_CHAR = ALPHA_LOWER + '234567'
|
B32_CHAR = ALPHA_LOWER + '234567'
|
||||||
ALPHA_LOWER_OR_DIGIT = ALPHA_LOWER + DIGIT
|
ALPHA_LOWER_OR_DIGIT = ALPHA_LOWER + DIGIT
|
||||||
|
95
tests/test_path.py
Normal file
95
tests/test_path.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"""Tests for path handling
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from earthsnake.identity import Identity
|
||||||
|
from earthsnake.path import Path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'path,error_msg',
|
||||||
|
[
|
||||||
|
pytest.param('/', 'Path length must be between 2 and 512', id='short'),
|
||||||
|
pytest.param('/' + 'a' * 512, 'Path length must be between 2 and 512', id='long'),
|
||||||
|
pytest.param('badstart', 'Paths must start with a /', id='no_slash_start'),
|
||||||
|
pytest.param('/@atstart', 'Paths must not start with /@', id='slashat_start'),
|
||||||
|
pytest.param('/double//slash', 'Paths must not contain //', id='double_slash'),
|
||||||
|
pytest.param('/multiple!excl!', 'Only one ! is allowed in paths', id='multi_excl'),
|
||||||
|
pytest.param('/test/', 'Paths must not end with a /', id='trailing_slash'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_invalid(path: str, error_msg: str) -> None:
|
||||||
|
"""Test if validation fails for invalid paths"""
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as ctx:
|
||||||
|
Path.validate(path)
|
||||||
|
|
||||||
|
assert error_msg in str(ctx.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_non_ephemeral() -> None:
|
||||||
|
"""Test validating ephemeral paths when ephemeral is not allowed"""
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as ctx:
|
||||||
|
Path.validate('/ephemeral!')
|
||||||
|
|
||||||
|
assert 'Only ephemeral paths may contain !' in str(ctx.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_ephemeral() -> None:
|
||||||
|
"""Test validation of ephemeral paths"""
|
||||||
|
|
||||||
|
assert Path.validate('/ephemeral!', allow_ephemeral=True) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_ephemeral() -> None:
|
||||||
|
"""Test the is_ephemeral property"""
|
||||||
|
|
||||||
|
assert Path('/ephemeral!').is_ephemeral
|
||||||
|
assert not Path('/non-ephemeral').is_ephemeral
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_shared() -> None:
|
||||||
|
"""Test the is_shared property"""
|
||||||
|
|
||||||
|
assert Path('/test').is_shared
|
||||||
|
assert not Path('/test/~').is_shared
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'author,path,allowed',
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
'@test.bcz76z52y5dlpohtkmpuj3jsdcvfmebzpcgfmtmhu4u7hlexzreya',
|
||||||
|
'/test',
|
||||||
|
True,
|
||||||
|
id='shared',
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
'@test.bcz76z52y5dlpohtkmpuj3jsdcvfmebzpcgfmtmhu4u7hlexzreya',
|
||||||
|
'/test/~@test.bcz76z52y5dlpohtkmpuj3jsdcvfmebzpcgfmtmhu4u7hlexzreya',
|
||||||
|
True,
|
||||||
|
id='allowed',
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
'@test.bcz76z52y5dlpohtkmpuj3jsdcvfmebzpcgfmtmhu4u7hlexzreya',
|
||||||
|
'/test/~@abob.bcz76z52y5dlpohtkmpuj3jsdcvfmebzpcgfmtmhu4u7hlexzreya',
|
||||||
|
False,
|
||||||
|
id='someone_else',
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
'@test.bcz76z52y5dlpohtkmpuj3jsdcvfmebzpcgfmtmhu4u7hlexzreya',
|
||||||
|
'/test/~',
|
||||||
|
False,
|
||||||
|
id='strictly_forbidden',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_can_write(author: str, path: str, allowed: bool) -> None:
|
||||||
|
"""Test the can_write method"""
|
||||||
|
|
||||||
|
path_obj = Path(path)
|
||||||
|
identity = Identity.from_address(author)
|
||||||
|
|
||||||
|
assert path_obj.can_write(identity) is allowed
|
Loading…
Reference in New Issue
Block a user