Skip to content

Commit

Permalink
fix: limit export filename length to 255 bytes
Browse files Browse the repository at this point in the history
fix #15
  • Loading branch information
vzhd1701 committed Aug 20, 2022
1 parent 951055d commit 33cfe1b
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 14 deletions.
41 changes: 30 additions & 11 deletions evernote_backup/note_exporter_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,25 +91,44 @@ def _get_non_existant_name(file_name: str, target_dir: Path) -> str:
def _trim_name(file_name: str, max_len: int = MAX_FILE_NAME_LEN) -> str:
"""Trim file name to 255 characters while maintaining extension
255 characters is max file name length on linux and macOS
Windows has a path limit of 260 characters which includes
the entire path (drive letter, path, and file name)
This does not trim the path length, just the file name
Windows has a filename limit of 260 bytes
Assuming character is one byte
Trimming only stem, extensions stays untouched
max_len: if provided, trims to this length otherwise MAX_FILE_NAME_LEN
Raises: ValueError if the file name is too long and cannot be trimmed
"""
if len(file_name) <= max_len:
if len(file_name.encode("utf-8")) <= max_len:
return file_name

orig = Path(file_name)

drop_chars = len(file_name) - max_len
trimmed_name = orig.stem[:-drop_chars]
if orig.suffix:
max_len_name = max_len - len(orig.suffix.encode("utf-8"))
else:
max_len_name = max_len

trimmed_name = _trim_string(orig.stem, max_len_name)

result_name = f"{trimmed_name}{orig.suffix}"

if len(result_name.encode("utf-8")) > max_len:
raise ValueError(
f"File name is too long but cannot be safely trimmed: {file_name}"
)

return result_name


def _trim_string(string: str, max_len: int) -> str:
if max_len <= 0:
return ""

chars = list(string)

if not orig.suffix:
return trimmed_name
if len(orig.stem) > drop_chars:
return f"{trimmed_name}{orig.suffix}"
while sum(len(c.encode("utf-8")) for c in chars) > max_len:
chars.pop(-1)

raise ValueError(f"File name is too long but cannot be safely trimmed: {file_name}")
return "".join(chars)
25 changes: 22 additions & 3 deletions tests/test_note_exporter_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def test_safe_path_long_file_name(tmp_path):
safe_path = SafePath(test_dir)
result_file_path = safe_path.get_file("test1", long_file_name)

expected_file.touch()
result_file_path.touch()

assert expected_file.is_file()
assert result_file_path == expected_file
Expand All @@ -136,7 +136,7 @@ def test_safe_path_long_file_name_existing(tmp_path):
safe_path = SafePath(test_dir)
result_file_path = safe_path.get_file(long_file_name)

expected_file.touch()
result_file_path.touch()

assert expected_file.is_file()
assert result_file_path == expected_file
Expand All @@ -155,7 +155,26 @@ def test_safe_path_long_file_name_no_ext(tmp_path):
safe_path = SafePath(test_dir)
result_file_path = safe_path.get_file("test1", long_file_name)

expected_file.touch()
result_file_path.touch()

assert expected_file.is_file()
assert result_file_path == expected_file


def test_safe_path_long_file_name_no_ext_unicode(tmp_path):
"""Test that SafePath trims a long file name with no extension"""
test_dir = tmp_path / "test"

slightly_longer_than_supported = MAX_FILE_NAME_LEN * 2

long_file_name = "😁" * slightly_longer_than_supported
expected_file_name = "😁" * (MAX_FILE_NAME_LEN // len("😁".encode("utf-8")))
expected_file = tmp_path / "test" / "test1" / expected_file_name

safe_path = SafePath(test_dir)
result_file_path = safe_path.get_file("test1", long_file_name)

result_file_path.touch()

assert expected_file.is_file()
assert result_file_path == expected_file
Expand Down
31 changes: 31 additions & 0 deletions tests/test_op_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,37 @@ def test_export_single_notes_over_existing_overwrite(
assert book2_existing_path.stat().st_size > 0


@pytest.mark.usefixtures("fake_init_db")
def test_export_single_notes_super_long_name(cli_invoker, fake_storage, tmp_path):
test_out_path = tmp_path / "test_out"

test_long_title = "😁" * 300
expected_note_name = f"{'😁' * 62}.enex"

test_notebooks = [Notebook(guid="nbid1", name="name1", stack=None)]

test_notes = [
Note(
guid="id1",
title=test_long_title,
content="test",
notebookGuid="nbid1",
active=True,
)
]

fake_storage.notebooks.add_notebooks(test_notebooks)

for note in test_notes:
fake_storage.notes.add_note(note)

cli_invoker("export", "--database", "fake_db", "--single-notes", str(test_out_path))

note_path = test_out_path / "name1" / expected_note_name

assert note_path.is_file()


@pytest.mark.usefixtures("fake_init_db")
def test_export_no_trash(cli_invoker, fake_storage, tmp_path):
test_out_path = tmp_path / "test_out"
Expand Down

0 comments on commit 33cfe1b

Please sign in to comment.