diff --git a/CHANGELOG.md b/CHANGELOG.md index 0497ac3508..5ac0fe57f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ This document records all notable changes to [HTTPie](https://httpie.io). This project adheres to [Semantic Versioning](https://semver.org/). +## Unreleased + +- Fix update-check lock files for paths whose Base64 encoding contains a slash, + including some non-ASCII configuration paths. + ## [3.2.4](https://github.com/httpie/cli/compare/3.2.3...3.2.4) (2024-11-01) - Fix default certs loading and unpin `requests`. ([#1596](https://github.com/httpie/cli/issues/1596)) diff --git a/httpie/utils.py b/httpie/utils.py index 4735b2be5d..155f7f925f 100644 --- a/httpie/utils.py +++ b/httpie/utils.py @@ -273,7 +273,7 @@ class LockFileError(ValueError): @contextmanager def open_with_lockfile(file: Path, *args, **kwargs) -> Generator[IO[Any], None, None]: - file_id = base64.b64encode(os.fsencode(file)).decode() + file_id = base64.urlsafe_b64encode(os.fsencode(file)).decode() target_file = Path(tempfile.gettempdir()) / file_id # Have an atomic-like touch here, so we'll tighten the possibility of diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000000..2b8a2f605a --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,39 @@ +from pathlib import Path + +import pytest + +from httpie.utils import LockFileError, open_with_lockfile + + +@pytest.mark.parametrize('filename', ['version_info.json', '\u00ff\u00ff\u00ff.json']) +def test_open_with_lockfile_unicode_path(tmp_path, monkeypatch, filename): + monkeypatch.chdir(tmp_path) + lock_dir = tmp_path / 'locks' + lock_dir.mkdir() + monkeypatch.setattr('httpie.utils.tempfile.gettempdir', lambda: str(lock_dir)) + target = Path(filename) + + with open_with_lockfile(target, 'w') as stream: + stream.write('saved') + assert len(list(lock_dir.iterdir())) == 1 + with pytest.raises(LockFileError): + with open_with_lockfile(target, 'w'): + pass + + assert target.read_text() == 'saved' + assert list(lock_dir.iterdir()) == [] + with open_with_lockfile(target) as stream: + assert stream.read() == 'saved' + + +def test_open_with_lockfile_releases_after_error(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + lock_dir = tmp_path / 'locks' + lock_dir.mkdir() + monkeypatch.setattr('httpie.utils.tempfile.gettempdir', lambda: str(lock_dir)) + + with pytest.raises(ValueError, match='failed'): + with open_with_lockfile(Path('\u00ff\u00ff\u00ff.json'), 'w'): + raise ValueError('failed') + + assert list(lock_dir.iterdir()) == []