From 9fb06e8968e3a72c72e0d8633630c42b7f565ce3 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 14 Jul 2022 17:52:47 +0100 Subject: [PATCH 1/5] Add an MXCUri class for representing media uri's in matrix --- src/matrix_common/types/__init__.py | 0 src/matrix_common/types/mxc_uri.py | 88 +++++++++++++++++++++++++++ tests/types/__init__.py | 0 tests/types/test_mxc_uri.py | 94 +++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+) create mode 100644 src/matrix_common/types/__init__.py create mode 100644 src/matrix_common/types/mxc_uri.py create mode 100644 tests/types/__init__.py create mode 100644 tests/types/test_mxc_uri.py diff --git a/src/matrix_common/types/__init__.py b/src/matrix_common/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/matrix_common/types/mxc_uri.py b/src/matrix_common/types/mxc_uri.py new file mode 100644 index 0000000..d35c5a9 --- /dev/null +++ b/src/matrix_common/types/mxc_uri.py @@ -0,0 +1,88 @@ +# Copyright 2022 The Matrix.org Foundation C.I.C. +# +# 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. +from typing import Type, TypeVar +from urllib.parse import urlparse + +import attr + +MU = TypeVar("MU", bound="MXCUri") + + +@attr.s(frozen=True, slots=True, auto_attribs=True) +class MXCUri: + """Represents a URI that points to a media resource in matrix. + + MXC URIs take the form 'mxc://server_name/media_id'. + """ + + server_name: str + media_id: str + + @classmethod + def from_str(cls: Type[MU], mxc_uri_str: str) -> MU: + """ + Given a str in the form "mxc:///", return an equivalent MXCUri. + + Args: + mxc_uri_str: The MXC Uri as a str. + + Returns: + An MXCUri object with matching attributes. + + Raises: + ValueError: If the str was not a valid MXC Uri. + """ + # Attempt to parse the given URI. This will raise a ValueError if the uri is + # particularly malformed. + parsed_mxc_uri = urlparse(mxc_uri_str) + + # MXC Uri's are pretty bare bones. The scheme must be "mxc", and we don't allow + # any fragments, query parameters or other features. + if ( + # The scheme must be "mxc". + parsed_mxc_uri.scheme != "mxc" + # There must be a host and path provided. + or not parsed_mxc_uri.netloc + or not parsed_mxc_uri.path + or not parsed_mxc_uri.path.startswith("/") + or len(parsed_mxc_uri.path) <= 1 + # There cannot be any fragments, queries or parameters. + or parsed_mxc_uri.fragment + or parsed_mxc_uri.query + or parsed_mxc_uri.params + ): + raise ValueError( + f"Found invalid structure when parsing MXC Uri: {mxc_uri_str}" + ) + + # We use the parsed 'network location' as the server name + server_name = parsed_mxc_uri.netloc + + # urlparse adds a '/' to the beginning of the path, so let's remove that and use + # it as the media_id + media_id = parsed_mxc_uri.path[1:] + + # The media ID should not contain a '/' + if "/" in media_id: + raise ValueError( + f"Found invalid character in media ID portion of MXC Uri: {mxc_uri_str}" + ) + + return cls(server_name, media_id) + + def to_string(self) -> str: + """Convert an MXCUri object to a str.""" + return f"mxc://{self.server_name}/{self.media_id}" + + __str__ = to_string diff --git a/tests/types/__init__.py b/tests/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/types/test_mxc_uri.py b/tests/types/test_mxc_uri.py new file mode 100644 index 0000000..64162ab --- /dev/null +++ b/tests/types/test_mxc_uri.py @@ -0,0 +1,94 @@ +# Copyright 2022 The Matrix.org Foundation C.I.C. +# +# 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. +from unittest import TestCase + +from matrix_common.types.mxc_uri import MXCUri + + +class MXCUriTestCase(TestCase): + def test_valid_mxc_uris(self) -> None: + """Tests that a series of valid mxc uris are parsed correctly.""" + # Converting an MXCUri to its str representation + mxc_0 = MXCUri(server_name="example.com", media_id="84n8493hnfsjkbcu") + self.assertEqual(mxc_0.to_string(), "mxc://example.com/84n8493hnfsjkbcu") + + mxc_1 = MXCUri( + server_name="192.168.1.17:8008", media_id="bajkad89h31ausdhoqqasd" + ) + self.assertEqual( + mxc_1.to_string(), "mxc://192.168.1.17:8008/bajkad89h31ausdhoqqasd" + ) + + mxc_2 = MXCUri(server_name="123.123.123.123", media_id="000000000000") + self.assertEqual(mxc_2.to_string(), "mxc://123.123.123.123/000000000000") + + # Converting a str to its MXCUri representation + mxcuri_0 = MXCUri.from_str("mxc://example.com/g12789g890ajksjk") + self.assertEqual(mxcuri_0.server_name, "example.com") + self.assertEqual(mxcuri_0.media_id, "g12789g890ajksjk") + + mxcuri_1 = MXCUri.from_str("mxc://localhost:8448/abcdefghijklmnopqrstuvwxyz") + self.assertEqual(mxcuri_1.server_name, "localhost:8448") + self.assertEqual(mxcuri_1.media_id, "abcdefghijklmnopqrstuvwxyz") + + mxcuri_2 = MXCUri.from_str("mxc://[::1]/abcdefghijklmnopqrstuvwxyz") + self.assertEqual(mxcuri_2.server_name, "[::1]") + self.assertEqual(mxcuri_2.media_id, "abcdefghijklmnopqrstuvwxyz") + + mxcuri_3 = MXCUri.from_str("mxc://123.123.123.123:32112/12893y81283781023") + self.assertEqual(mxcuri_3.server_name, "123.123.123.123:32112") + self.assertEqual(mxcuri_3.media_id, "12893y81283781023") + + mxcuri_4 = MXCUri.from_str("mxc://domain/abcdefg") + self.assertEqual(mxcuri_4.server_name, "domain") + self.assertEqual(mxcuri_4.media_id, "abcdefg") + + def test_invalid_mxc_uris(self) -> None: + """Tests that a series of invalid mxc uris are appropriately rejected.""" + # Converting a str to its MXCUri representation + with self.assertRaises(ValueError): + MXCUri.from_str("http://example.com/abcdef") + + with self.assertRaises(ValueError): + MXCUri.from_str("mxc:///example.com/abcdef") + + with self.assertRaises(ValueError): + MXCUri.from_str("mxc://example.com//abcdef") + + with self.assertRaises(ValueError): + MXCUri.from_str("mxc://example.com/abcdef/") + + with self.assertRaises(ValueError): + MXCUri.from_str("mxc://example.com/abc/abcdef") + + with self.assertRaises(ValueError): + MXCUri.from_str("mxc://example.com/abc/abcdef") + + with self.assertRaises(ValueError): + MXCUri.from_str("mxc:///abcdef") + + with self.assertRaises(ValueError): + MXCUri.from_str("mxc://example.com") + + with self.assertRaises(ValueError): + MXCUri.from_str("mxc://example.com/") + + with self.assertRaises(ValueError): + MXCUri.from_str("mxc:///") + + with self.assertRaises(ValueError): + MXCUri.from_str("") + + with self.assertRaises(ValueError): + MXCUri.from_str(None) # type: ignore From f35bdf7d2cdc05528cad3fb46da5c56e03e1ea58 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 8 Sep 2022 14:56:38 +0100 Subject: [PATCH 2/5] Seperate valid mxc test case into two --- tests/types/test_mxc_uri.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/types/test_mxc_uri.py b/tests/types/test_mxc_uri.py index 64162ab..20b0147 100644 --- a/tests/types/test_mxc_uri.py +++ b/tests/types/test_mxc_uri.py @@ -17,8 +17,8 @@ class MXCUriTestCase(TestCase): - def test_valid_mxc_uris(self) -> None: - """Tests that a series of valid mxc uris are parsed correctly.""" + def test_valid_mxc_uris_to_str(self) -> None: + """Tests that a series of valid mxc are converted to a str correctly.""" # Converting an MXCUri to its str representation mxc_0 = MXCUri(server_name="example.com", media_id="84n8493hnfsjkbcu") self.assertEqual(mxc_0.to_string(), "mxc://example.com/84n8493hnfsjkbcu") @@ -33,6 +33,8 @@ def test_valid_mxc_uris(self) -> None: mxc_2 = MXCUri(server_name="123.123.123.123", media_id="000000000000") self.assertEqual(mxc_2.to_string(), "mxc://123.123.123.123/000000000000") + def test_valid_mxc_uris_from_str(self) -> None: + """Tests that a series of valid mxc uris strs are parsed correctly.""" # Converting a str to its MXCUri representation mxcuri_0 = MXCUri.from_str("mxc://example.com/g12789g890ajksjk") self.assertEqual(mxcuri_0.server_name, "example.com") @@ -54,9 +56,9 @@ def test_valid_mxc_uris(self) -> None: self.assertEqual(mxcuri_4.server_name, "domain") self.assertEqual(mxcuri_4.media_id, "abcdefg") - def test_invalid_mxc_uris(self) -> None: + def test_invalid_mxc_uris_from_str(self) -> None: """Tests that a series of invalid mxc uris are appropriately rejected.""" - # Converting a str to its MXCUri representation + # Converting invalid MXC URI strs to MXCUri representations with self.assertRaises(ValueError): MXCUri.from_str("http://example.com/abcdef") From 1b6457abc621ec877024691ad60cc203b92b6e64 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 8 Sep 2022 15:15:56 +0100 Subject: [PATCH 3/5] Modify conditional for parsing an mxc str --- src/matrix_common/types/mxc_uri.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix_common/types/mxc_uri.py b/src/matrix_common/types/mxc_uri.py index d35c5a9..28065e2 100644 --- a/src/matrix_common/types/mxc_uri.py +++ b/src/matrix_common/types/mxc_uri.py @@ -56,7 +56,7 @@ def from_str(cls: Type[MU], mxc_uri_str: str) -> MU: or not parsed_mxc_uri.netloc or not parsed_mxc_uri.path or not parsed_mxc_uri.path.startswith("/") - or len(parsed_mxc_uri.path) <= 1 + or len(parsed_mxc_uri.path) == 1 # if the path is only '/', aka no Media ID # There cannot be any fragments, queries or parameters. or parsed_mxc_uri.fragment or parsed_mxc_uri.query From 75d2a1d886d4a8573285986e46f16b4e7e5f3f2b Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 8 Sep 2022 15:25:54 +0100 Subject: [PATCH 4/5] x.to_string() -> str(x) --- src/matrix_common/types/mxc_uri.py | 4 ++-- tests/types/test_mxc_uri.py | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/matrix_common/types/mxc_uri.py b/src/matrix_common/types/mxc_uri.py index 28065e2..ae4e61f 100644 --- a/src/matrix_common/types/mxc_uri.py +++ b/src/matrix_common/types/mxc_uri.py @@ -81,8 +81,8 @@ def from_str(cls: Type[MU], mxc_uri_str: str) -> MU: return cls(server_name, media_id) - def to_string(self) -> str: + def _to_string(self) -> str: """Convert an MXCUri object to a str.""" return f"mxc://{self.server_name}/{self.media_id}" - __str__ = to_string + __str__ = _to_string diff --git a/tests/types/test_mxc_uri.py b/tests/types/test_mxc_uri.py index 20b0147..76d31c9 100644 --- a/tests/types/test_mxc_uri.py +++ b/tests/types/test_mxc_uri.py @@ -21,17 +21,15 @@ def test_valid_mxc_uris_to_str(self) -> None: """Tests that a series of valid mxc are converted to a str correctly.""" # Converting an MXCUri to its str representation mxc_0 = MXCUri(server_name="example.com", media_id="84n8493hnfsjkbcu") - self.assertEqual(mxc_0.to_string(), "mxc://example.com/84n8493hnfsjkbcu") + self.assertEqual(str(mxc_0), "mxc://example.com/84n8493hnfsjkbcu") mxc_1 = MXCUri( server_name="192.168.1.17:8008", media_id="bajkad89h31ausdhoqqasd" ) - self.assertEqual( - mxc_1.to_string(), "mxc://192.168.1.17:8008/bajkad89h31ausdhoqqasd" - ) + self.assertEqual(str(mxc_1), "mxc://192.168.1.17:8008/bajkad89h31ausdhoqqasd") mxc_2 = MXCUri(server_name="123.123.123.123", media_id="000000000000") - self.assertEqual(mxc_2.to_string(), "mxc://123.123.123.123/000000000000") + self.assertEqual(str(mxc_2), "mxc://123.123.123.123/000000000000") def test_valid_mxc_uris_from_str(self) -> None: """Tests that a series of valid mxc uris strs are parsed correctly.""" From 0623463758f9ccded871a0f6c8e4b4fa13d86b9b Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 12 Sep 2022 17:39:28 +0100 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Patrick Cloke --- src/matrix_common/types/mxc_uri.py | 4 +--- tests/types/test_mxc_uri.py | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/matrix_common/types/mxc_uri.py b/src/matrix_common/types/mxc_uri.py index ae4e61f..894a8f7 100644 --- a/src/matrix_common/types/mxc_uri.py +++ b/src/matrix_common/types/mxc_uri.py @@ -81,8 +81,6 @@ def from_str(cls: Type[MU], mxc_uri_str: str) -> MU: return cls(server_name, media_id) - def _to_string(self) -> str: + def __str__(self) -> str: """Convert an MXCUri object to a str.""" return f"mxc://{self.server_name}/{self.media_id}" - - __str__ = _to_string diff --git a/tests/types/test_mxc_uri.py b/tests/types/test_mxc_uri.py index 76d31c9..353d48f 100644 --- a/tests/types/test_mxc_uri.py +++ b/tests/types/test_mxc_uri.py @@ -87,6 +87,9 @@ def test_invalid_mxc_uris_from_str(self) -> None: with self.assertRaises(ValueError): MXCUri.from_str("mxc:///") + with self.assertRaises(ValueError): + MXCUri.from_str("example.com/abc") + with self.assertRaises(ValueError): MXCUri.from_str("")