Repository: scikit-hep/awkward

Issue:
`ak.from_buffers` fails for n-dim `NumpyArray` with size 0
### Version of Awkward Array

1.4.0

### Description and code to reproduce

The following code
```python3
>>> array = ak.from_numpy(np.zeros((1, 0), dtype=np.int32))
>>> ak.from_buffers(*ak.to_buffers(array))
```
fails with a `ZXeroDivisionError`:
```pytb
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
/tmp/ipykernel_10330/380633746.py in <module>
----> 1 ak.from_buffers(*ak.to_buffers(array))

/opt/texat-venv/lib/python3.9/site-packages/awkward/operations/convert.py in from_buffers(form, length, container, partition_start, key_format, lazy, lazy_cache, lazy_cache_key, highlevel, behavior)
   5055 
   5056         else:
-> 5057             out = _form_to_layout(*(args + (None, None)))
   5058 
   5059     elif isinstance(length, Iterable):

/opt/texat-venv/lib/python3.9/site-packages/awkward/operations/convert.py in _form_to_layout(form, container, partnum, key_format, length, lazy_cache, lazy_cache_key)
   4712 
   4713         if length is not None:
-> 4714             actual = len(raw_array) // dtype_inner_shape.itemsize
   4715             if length > actual:
   4716                 raise ValueError(

ZeroDivisionError: integer division or modulo by zero
```


Discussion:

Resolving patch:
diff --git a/src/awkward/operations/convert.py b/src/awkward/operations/convert.py
--- a/src/awkward/operations/convert.py
+++ b/src/awkward/operations/convert.py
@@ -4710,7 +4710,9 @@ def _form_to_layout(
         else:
             dtype, inner_shape = dtype_inner_shape.subdtype
 
-        if length is not None:
+        # If the buffer is required to have a length, check that it's not too short.
+        # If the array has no itemsize, then the buffer can never be "too short"!
+        if (length is not None) and (dtype_inner_shape.itemsize > 0):
             actual = len(raw_array) // dtype_inner_shape.itemsize
             if length > actual:
                 raise ValueError(
@@ -4719,7 +4721,21 @@ def _form_to_layout(
                     + ak._util.exception_suffix(__file__)
                 )
 
-        array = raw_array.view(dtype).reshape((-1,) + inner_shape)
+        # NumPy can only infer the length of the array from the buffer
+        # if the inner shape has a nonzero item-size.
+        if dtype_inner_shape.itemsize:
+            leading_shape = (-1,)
+        elif length is not None:
+            leading_shape = (length,)
+        # Given that `length=None` only occurs when reading legacy data
+        # This already failed, so we can fail if `length` is not given
+        else:
+            raise ValueError(
+                "buffer is empty, and no length was given. It is not possible to "
+                "determine the correct size of the array from the buffer alone."
+            )
+
+        array = raw_array.view(dtype).reshape(leading_shape + inner_shape)
 
         return ak.layout.NumpyArray(array, identities, parameters)
 


Test patch:
diff --git a/tests/test_1007-from_buffers-empty-ndarray.py b/tests/test_1007-from_buffers-empty-ndarray.py
new file mode 100644
--- /dev/null
+++ b/tests/test_1007-from_buffers-empty-ndarray.py
@@ -0,0 +1,15 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+from __future__ import absolute_import
+
+import pytest  # noqa: F401
+import numpy as np  # noqa: F401
+import awkward as ak  # noqa: F401
+
+
+def test():
+    array = ak.from_numpy(np.zeros((3, 0), dtype=np.int32))
+    buffs = ak.to_buffers(array)
+    new_array = ak.from_buffers(*buffs)
+
+    assert ak.to_list(new_array) == [[], [], []]