Repository: asottile/babi

Issue:
file containing null byte causes crash
### reproduction
```bash
echo -e '\x00' > bytes.txt && babi bytes.txt
```
Traceback:
```console
Traceback (most recent call last):
  File "/tmp/venv/bin/babi", line 8, in <module>
    sys.exit(main())
  File "/tmp/venv/lib/python3.10/site-packages/babi/main.py", line 176, in main
    return c_main(stdscr, file_infos, stdin, perf)
  File "/tmp/venv/lib/python3.10/site-packages/babi/main.py", line 63, in c_main
    res = _edit(screen, stdin)
  File "/tmp/venv/lib/python3.10/site-packages/babi/main.py", line 25, in _edit
    screen.file.ensure_loaded(screen.status, screen.layout.file, stdin)
  File "/tmp/venv/lib/python3.10/site-packages/babi/file.py", line 276, in ensure_loaded
    self.go_to_line(self.initial_line, dim)
  File "/tmp/venv/lib/python3.10/site-packages/babi/file.py", line 116, in action_inner
    return func(self, *args, **kwargs)
  File "/tmp/venv/lib/python3.10/site-packages/babi/file.py", line 414, in go_to_line
    self.buf.x = 0
  File "/tmp/venv/lib/python3.10/site-packages/babi/buf.py", line 245, in x
    self._x_hint = self._cursor_x
  File "/tmp/venv/lib/python3.10/site-packages/babi/buf.py", line 275, in _cursor_x
    return self.line_positions(self.y)[self.x]
  File "/tmp/venv/lib/python3.10/site-packages/babi/buf.py", line 266, in line_positions
    value = _offsets(self._lines[idx], self.tab_size)
  File "/tmp/venv/lib/python3.10/site-packages/babi/buf.py", line 44, in _offsets
    ret.append(ret[-1] + wcwidth(c))
  File "/tmp/venv/lib/python3.10/site-packages/babi/horizontal_scrolling.py", line 43, in wcwidth
    self._window.addstr(0, 0, c)
ValueError: embedded null character
```
Versions:
```
babi==1.5.3
babi-grammars==0.0.43
cffi==1.15.0
identify==2.5.1
onigurumacffi==1.2.0
pycparser==2.21
```
characters like `\x01` or `\x02` work fine and are displayed as `^A` or `^B`
### context
I encountered this error while trying to open and fix a somewhat corrupted file. While python was writing to this file, the machine crashed and a bunch of null bytes ended up at the end of the file, removing them makes the file usable again.

`nano`, `less` and `vim` display `\x00` as `^@`.



Discussion:

Patch:
diff --git a/babi/file.py b/babi/file.py
index 73182d3..3fa5184 100644
--- a/babi/file.py
+++ b/babi/file.py
@@ -40,11 +40,17 @@ TCallable = TypeVar('TCallable', bound=Callable[..., Any])
 WS_RE = re.compile(r'^\s*')
 
 
+class NullByteError(ValueError):
+    pass
+
+
 def get_lines(sio: IO[str]) -> tuple[list[str], str, bool, str]:
     sha256 = hashlib.sha256()
     lines = []
     newlines = collections.Counter({'\n': 0})  # default to `\n`
     for line in sio:
+        if '\0' in line:
+            raise NullByteError
         sha256.update(line.encode())
         for ending in ('\r\n', '\n'):
             if line.endswith(ending):
@@ -68,6 +74,8 @@ def _load_file(filename: str) -> tuple[list[str], str, bool, str]:
     try:
         with open(filename, encoding='UTF-8', newline='') as f:
             return get_lines(f)
+    except NullByteError:
+        raise OpenError(fr'error! file contains \0 bytes: {filename!r}')
     except UnicodeDecodeError:
         raise OpenError(f'error! not utf-8: {filename!r}')
     except OSError:


Test patch:
diff --git a/tests/features/save_test.py b/tests/features/save_test.py
index b8e2a56..2463d66 100644
--- a/tests/features/save_test.py
+++ b/tests/features/save_test.py
@@ -61,6 +61,14 @@ def test_non_utf8_file(run, tmpdir):
         h.await_text('error! not utf-8:')
 
 
+def test_file_with_null_bytes(run, tmp_path):
+    f = tmp_path.joinpath('f')
+    f.write_bytes(b'hello\0world\n')
+
+    with run(str(f)) as h, and_exit(h):
+        h.await_text(r'error! file contains \0 bytes:')
+
+
 def test_save_no_filename_specified(run, tmpdir):
     f = tmpdir.join('f')