Repository navigation
fix(wal): reject non-string keys and values on the write path #345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,23 +41,10 @@ def from_dict(data: dict) -> "WalEntry": | |
| op = data.get("op") | ||
| try: | ||
| if op == "set": | ||
| key = data["key"] | ||
| value = data["value"] | ||
|
|
||
| if not isinstance(key, str): | ||
| raise TypeError("key must be a string") | ||
| if not isinstance(value, str): | ||
| raise TypeError("value must be a string") | ||
|
|
||
| return SetEntry(key=key, value=value) | ||
| return SetEntry(key=data["key"], value=data["value"]) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thats a lot simpler, I do like it |
||
|
|
||
| if op == "delete": | ||
| key = data["key"] | ||
|
|
||
| if not isinstance(key, str): | ||
| raise TypeError("key must be a string") | ||
|
|
||
| return DeleteEntry(key=key) | ||
| return DeleteEntry(key=data["key"]) | ||
|
|
||
| except KeyError as e: | ||
| raise ValueError(f"missing required field: {e}") from e | ||
|
|
@@ -72,6 +59,13 @@ class SetEntry(WalEntry): | |
| key: str | ||
| value: str | ||
|
|
||
| def __post_init__(self) -> None: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great |
||
| """Reject keys and values that are not strings.""" | ||
| if not isinstance(self.key, str): | ||
| raise TypeError("key must be a string") | ||
| if not isinstance(self.value, str): | ||
| raise TypeError("value must be a string") | ||
|
|
||
| def to_dict(self) -> dict: | ||
| """Return the JSON-serializable representation of this entry.""" | ||
| return {"op": "set", "key": self.key, "value": self.value} | ||
|
|
@@ -87,6 +81,11 @@ class DeleteEntry(WalEntry): | |
|
|
||
| key: str | ||
|
|
||
| def __post_init__(self) -> None: | ||
| """Reject keys that are not strings.""" | ||
| if not isinstance(self.key, str): | ||
| raise TypeError("key must be a string") | ||
|
|
||
| def to_dict(self) -> dict: | ||
| """Return the JSON-serializable representation of this entry.""" | ||
| return {"op": "delete", "key": self.key} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,6 +72,7 @@ def test_replay_drops_and_truncates_a_torn_last_entry(wal, wal_path): | |
| b'{"op": "set", "key": null, "value": "b"}\n', # Key is null | ||
| b'{"op": "set", "key": 5, "value": "b"}\n', # Key is not a string | ||
| b'{"op": "set", "key": "b", "value": [1, 2]}\n', # Value is not a string | ||
| b'{"op": "delete", "key": 5}\n', # Delete key is not a string | ||
| ], | ||
| ) | ||
| def test_replay_recovers_from_malformed_entry(wal, wal_path, malformed): | ||
|
|
@@ -95,6 +96,16 @@ def test_replay_recovers_from_malformed_entry(wal, wal_path, malformed): | |
| ] | ||
|
|
||
|
|
||
| def test_entries_reject_non_string_key_or_value(): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess that enough |
||
| """Reject a key or value that is not a string.""" | ||
| with pytest.raises(TypeError): | ||
| SetEntry(key=5, value="b") # ty: ignore[invalid-argument-type] | ||
| with pytest.raises(TypeError): | ||
| SetEntry(key="b", value=[1, 2]) # ty: ignore[invalid-argument-type] | ||
| with pytest.raises(TypeError): | ||
| DeleteEntry(key=5) # ty: ignore[invalid-argument-type] | ||
|
|
||
|
|
||
| def test_append_after_repaired_truncation_stays_valid(wal, wal_path): | ||
| wal.append(SetEntry(key="a", value="1")) | ||
| wal.append(SetEntry(key="b", value="2")) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats good