Browse Source

Convert _objects.c => _objects.rs

Jelmer Vernooij 1 year ago
parent
commit
4832e1b18d
7 changed files with 467 additions and 314 deletions
  1. 280 0
      Cargo.lock
  2. 12 0
      Cargo.toml
  3. 3 0
      NEWS
  4. 0 309
      dulwich/_objects.c
  5. 166 0
      dulwich/_objects.rs
  6. 4 4
      dulwich/tests/test_objects.py
  7. 2 1
      setup.py

+ 280 - 0
Cargo.lock

@@ -0,0 +1,280 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "indoc"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "lock_api"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "memchr"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f478948fd84d9f8e86967bf432640e46adfb5a4bd4f14ef7e864ab38220534ae"
+
+[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "objects-py"
+version = "3.4.0"
+dependencies = [
+ "memchr",
+ "pyo3",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38"
+dependencies = [
+ "cfg-if",
+ "indoc",
+ "libc",
+ "memoffset",
+ "parking_lot",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+ "unindent",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5"
+dependencies = [
+ "once_cell",
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "smallvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+
+[[package]]
+name = "unindent"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c"
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"

+ 12 - 0
Cargo.toml

@@ -0,0 +1,12 @@
+[package]
+name = "objects-py"
+version = "3.4.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+path = "dulwich/_objects.rs"
+
+[dependencies]
+pyo3 = { version = ">=0.19", features = ["extension-module"]}
+memchr = "2"

+ 3 - 0
NEWS

@@ -16,6 +16,9 @@
 
 0.21.6	2023-09-02
 
+ * Convert _objects.c to rust.
+   (Jelmer Vernooij)
+
  * index: Handle different stages of conflicted paths.
    (Kevin Hendricks, Jelmer Vernooij)
 

+ 0 - 309
dulwich/_objects.c

@@ -1,309 +0,0 @@
-/*
- * Copyright (C) 2009 Jelmer Vernooij <jelmer@jelmer.uk>
- *
- * Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
- * General Public License as public by the Free Software Foundation; version 2.0
- * or (at your option) any later version. You can redistribute it and/or
- * modify it under the terms of either of these two licenses.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * You should have received a copy of the licenses; if not, see
- * <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
- * and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
- * License, Version 2.0.
- */
-
-#define PY_SSIZE_T_CLEAN
-#include <Python.h>
-#include <stdlib.h>
-#include <sys/stat.h>
-
-#if defined(__MINGW32_VERSION) || defined(__APPLE__)
-size_t rep_strnlen(char *text, size_t maxlen);
-size_t rep_strnlen(char *text, size_t maxlen)
-{
-	const char *last = memchr(text, '\0', maxlen);
-	return last ? (size_t) (last - text) : maxlen;
-}
-#define strnlen rep_strnlen
-#endif
-
-#define bytehex(x) (((x)<0xa)?('0'+(x)):('a'-0xa+(x)))
-
-static PyObject *tree_entry_cls;
-static PyObject *object_format_exception_cls;
-
-static PyObject *sha_to_pyhex(const unsigned char *sha)
-{
-	char hexsha[41];
-	int i;
-	for (i = 0; i < 20; i++) {
-		hexsha[i*2] = bytehex((sha[i] & 0xF0) >> 4);
-		hexsha[i*2+1] = bytehex(sha[i] & 0x0F);
-	}
-
-	return PyBytes_FromStringAndSize(hexsha, 40);
-}
-
-static PyObject *py_parse_tree(PyObject *self, PyObject *args, PyObject *kw)
-{
-	char *text, *start, *end;
-	Py_ssize_t len; int strict;
-	size_t namelen;
-	PyObject *ret, *item, *name, *sha, *py_strict = NULL;
-	static char *kwlist[] = {"text", "strict", NULL};
-
-	if (!PyArg_ParseTupleAndKeywords(args, kw, "y#|O", kwlist,
-	                                 &text, &len, &py_strict))
-		return NULL;
-	strict = py_strict ?  PyObject_IsTrue(py_strict) : 0;
-	/* TODO: currently this returns a list; if memory usage is a concern,
-	 * consider rewriting as a custom iterator object */
-	ret = PyList_New(0);
-	if (ret == NULL) {
-		return NULL;
-	}
-	start = text;
-	end = text + len;
-	while (text < end) {
-		long mode;
-		if (strict && text[0] == '0') {
-			PyErr_SetString(object_format_exception_cls,
-			                "Illegal leading zero on mode");
-			Py_DECREF(ret);
-			return NULL;
-		}
-		mode = strtol(text, &text, 8);
-		if (*text != ' ') {
-			PyErr_SetString(PyExc_ValueError, "Expected space");
-			Py_DECREF(ret);
-			return NULL;
-		}
-		text++;
-		namelen = strnlen(text, len - (text - start));
-		name = PyBytes_FromStringAndSize(text, namelen);
-		if (name == NULL) {
-			Py_DECREF(ret);
-			return NULL;
-		}
-		if (text + namelen + 20 >= end) {
-			PyErr_SetString(PyExc_ValueError, "SHA truncated");
-			Py_DECREF(ret);
-			Py_DECREF(name);
-			return NULL;
-		}
-		sha = sha_to_pyhex((unsigned char *)text+namelen+1);
-		if (sha == NULL) {
-			Py_DECREF(ret);
-			Py_DECREF(name);
-			return NULL;
-		}
-		item = Py_BuildValue("(NlN)", name, mode, sha);
-		if (item == NULL) {
-			Py_DECREF(ret);
-			Py_DECREF(sha);
-			Py_DECREF(name);
-			return NULL;
-		}
-		if (PyList_Append(ret, item) == -1) {
-			Py_DECREF(ret);
-			Py_DECREF(item);
-			return NULL;
-		}
-		Py_DECREF(item);
-		text += namelen+21;
-	}
-	return ret;
-}
-
-struct tree_item {
-	const char *name;
-	int mode;
-	PyObject *tuple;
-};
-
-/* Not all environments define S_ISDIR */
-#if !defined(S_ISDIR) && defined(S_IFMT) && defined(S_IFDIR)
-#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
-#endif
-
-int cmp_tree_item(const void *_a, const void *_b)
-{
-	const struct tree_item *a = _a, *b = _b;
-	const char *remain_a, *remain_b;
-	int ret;
-	size_t common;
-	if (strlen(a->name) > strlen(b->name)) {
-		common = strlen(b->name);
-		remain_a = a->name + common;
-		remain_b = (S_ISDIR(b->mode)?"/":"");
-	} else if (strlen(b->name) > strlen(a->name)) {
-		common = strlen(a->name);
-		remain_a = (S_ISDIR(a->mode)?"/":"");
-		remain_b = b->name + common;
-	} else { /* strlen(a->name) == strlen(b->name) */
-		common = 0;
-		remain_a = a->name;
-		remain_b = b->name;
-	}
-	ret = strncmp(a->name, b->name, common);
-	if (ret != 0)
-		return ret;
-	return strcmp(remain_a, remain_b);
-}
-
-int cmp_tree_item_name_order(const void *_a, const void *_b) {
-	const struct tree_item *a = _a, *b = _b;
-	return strcmp(a->name, b->name);
-}
-
-static PyObject *py_sorted_tree_items(PyObject *self, PyObject *args)
-{
-	struct tree_item *qsort_entries = NULL;
-	int name_order, n = 0, i;
-	PyObject *entries, *py_name_order, *ret, *key, *value, *py_mode, *py_sha;
-	Py_ssize_t pos = 0, num_entries;
-	int (*cmp)(const void *, const void *);
-
-	if (!PyArg_ParseTuple(args, "OO", &entries, &py_name_order))
-		goto error;
-
-	if (!PyDict_Check(entries)) {
-		PyErr_SetString(PyExc_TypeError, "Argument not a dictionary");
-		goto error;
-	}
-
-	name_order = PyObject_IsTrue(py_name_order);
-	if (name_order == -1)
-		goto error;
-	cmp = name_order ? cmp_tree_item_name_order : cmp_tree_item;
-
-	num_entries = PyDict_Size(entries);
-	if (PyErr_Occurred())
-		goto error;
-	qsort_entries = PyMem_New(struct tree_item, num_entries);
-	if (!qsort_entries) {
-		PyErr_NoMemory();
-		goto error;
-	}
-
-	while (PyDict_Next(entries, &pos, &key, &value)) {
-		if (!PyBytes_Check(key)) {
-			PyErr_SetString(PyExc_TypeError, "Name is not a string");
-			goto error;
-		}
-
-		if (PyTuple_Size(value) != 2) {
-			PyErr_SetString(PyExc_ValueError, "Tuple has invalid size");
-			goto error;
-		}
-
-		py_mode = PyTuple_GET_ITEM(value, 0);
-		if (!PyLong_Check(py_mode)) {
-			PyErr_SetString(PyExc_TypeError, "Mode is not an integral type");
-			goto error;
-		}
-
-		py_sha = PyTuple_GET_ITEM(value, 1);
-		if (!PyBytes_Check(py_sha)) {
-			PyErr_SetString(PyExc_TypeError, "SHA is not a string");
-			goto error;
-		}
-		qsort_entries[n].name = PyBytes_AS_STRING(key);
-		qsort_entries[n].mode = PyLong_AsLong(py_mode);
-
-		qsort_entries[n].tuple = PyObject_CallFunctionObjArgs(
-		                tree_entry_cls, key, py_mode, py_sha, NULL);
-		if (qsort_entries[n].tuple == NULL)
-			goto error;
-		n++;
-	}
-
-	qsort(qsort_entries, num_entries, sizeof(struct tree_item), cmp);
-
-	ret = PyList_New(num_entries);
-	if (ret == NULL) {
-		PyErr_NoMemory();
-		goto error;
-	}
-
-	for (i = 0; i < num_entries; i++) {
-		PyList_SET_ITEM(ret, i, qsort_entries[i].tuple);
-	}
-	PyMem_Free(qsort_entries);
-	return ret;
-
-error:
-	for (i = 0; i < n; i++) {
-		Py_XDECREF(qsort_entries[i].tuple);
-	}
-	PyMem_Free(qsort_entries);
-	return NULL;
-}
-
-static PyMethodDef py_objects_methods[] = {
-	{ "parse_tree", (PyCFunction)py_parse_tree, METH_VARARGS | METH_KEYWORDS,
-	  NULL },
-	{ "sorted_tree_items", py_sorted_tree_items, METH_VARARGS, NULL },
-	{ NULL, NULL, 0, NULL }
-};
-
-static PyObject *
-moduleinit(void)
-{
-	PyObject *m, *objects_mod, *errors_mod;
-
-	static struct PyModuleDef moduledef = {
-		PyModuleDef_HEAD_INIT,
-		"_objects",         /* m_name */
-		NULL,               /* m_doc */
-		-1,                 /* m_size */
-		py_objects_methods, /* m_methods */
-		NULL,               /* m_reload */
-		NULL,               /* m_traverse */
-		NULL,               /* m_clear*/
-		NULL,               /* m_free */
-	};
-	m = PyModule_Create(&moduledef);
-	if (m == NULL) {
-		return NULL;
-	}
-
-	errors_mod = PyImport_ImportModule("dulwich.errors");
-	if (errors_mod == NULL) {
-		return NULL;
-	}
-
-	object_format_exception_cls = PyObject_GetAttrString(
-		errors_mod, "ObjectFormatException");
-	Py_DECREF(errors_mod);
-	if (object_format_exception_cls == NULL) {
-		return NULL;
-	}
-
-	/* This is a circular import but should be safe since this module is
-	 * imported at at the very bottom of objects.py. */
-	objects_mod = PyImport_ImportModule("dulwich.objects");
-	if (objects_mod == NULL) {
-		return NULL;
-	}
-
-	tree_entry_cls = PyObject_GetAttrString(objects_mod, "TreeEntry");
-	Py_DECREF(objects_mod);
-	if (tree_entry_cls == NULL) {
-		return NULL;
-	}
-
-	return m;
-}
-
-PyMODINIT_FUNC
-PyInit__objects(void)
-{
-	return moduleinit();
-}

+ 166 - 0
dulwich/_objects.rs

@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2009 Jelmer Vernooij <jelmer@jelmer.uk>
+ *
+ * Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+ * General Public License as public by the Free Software Foundation; version 2.0
+ * or (at your option) any later version. You can redistribute it and/or
+ * modify it under the terms of either of these two licenses.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * You should have received a copy of the licenses; if not, see
+ * <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+ * and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+ * License, Version 2.0.
+ */
+
+use memchr::memchr;
+use std::borrow::Cow;
+
+use pyo3::exceptions::PyTypeError;
+use pyo3::import_exception;
+use pyo3::prelude::*;
+use pyo3::types::{PyBytes, PyDict};
+
+import_exception!(dulwich.errors, ObjectFormatException);
+
+const S_IFDIR: u32 = 0o40000;
+
+fn bytehex(byte: u8) -> u8 {
+    match byte {
+        0..=9 => byte + b'0',
+        10..=15 => byte - 10 + b'a',
+        _ => unreachable!(),
+    }
+}
+
+fn sha_to_pyhex(py: Python, sha: &[u8]) -> PyResult<PyObject> {
+    let mut hexsha = Vec::new();
+    for c in sha {
+        hexsha.push(bytehex((c & 0xF0) >> 4));
+        hexsha.push(bytehex(c & 0x0F));
+    }
+
+    Ok(PyBytes::new(py, hexsha.as_slice()).into())
+}
+
+#[pyfunction]
+fn parse_tree(
+    py: Python,
+    mut text: &[u8],
+    strict: Option<bool>,
+) -> PyResult<Vec<(PyObject, u32, PyObject)>> {
+    let mut entries = Vec::new();
+    let strict = strict.unwrap_or(false);
+    while !text.is_empty() {
+        let mode_end = match memchr(b' ', text) {
+            Some(e) => e,
+            None => {
+                return Err(ObjectFormatException::new_err((
+                    "Missing terminator for mode",
+                )));
+            }
+        };
+        let text_str = String::from_utf8_lossy(&text[..mode_end]).to_string();
+        let mode = match u32::from_str_radix(text_str.as_str(), 8) {
+            Ok(m) => m,
+            Err(e) => {
+                return Err(ObjectFormatException::new_err((format!(
+                    "invalid mode: {}",
+                    e
+                ),)));
+            }
+        };
+        if strict && text[0] == b'0' {
+            return Err(ObjectFormatException::new_err((
+                "Illegal leading zero on mode",
+            )));
+        }
+        text = &text[mode_end + 1..];
+        let namelen = match memchr(b'\0', text) {
+            Some(nl) => nl,
+            None => {
+                return Err(ObjectFormatException::new_err(("Missing trailing \\0",)));
+            }
+        };
+        let name = &text[..namelen];
+        if namelen + 20 >= text.len() {
+            return Err(ObjectFormatException::new_err(("SHA truncated",)));
+        }
+        text = &text[namelen + 1..];
+        let sha = &text[..20];
+        entries.push((
+            PyBytes::new(py, name).to_object(py),
+            mode,
+            sha_to_pyhex(py, sha)?,
+        ));
+        text = &text[20..];
+    }
+    Ok(entries)
+}
+
+fn name_with_suffix(mode: u32, name: &[u8]) -> Cow<[u8]> {
+    if mode & S_IFDIR != 0 {
+        let mut v = name.to_vec();
+        v.push(b'/');
+        v.into()
+    } else {
+        name.into()
+    }
+}
+
+/// Iterate over a tree entries dictionary.
+///
+/// # Arguments
+///
+///   name_order: If True, iterate entries in order of their name. If
+///        False, iterate entries in tree order, that is, treat subtree entries as
+///        having '/' appended.
+///      entries: Dictionary mapping names to (mode, sha) tuples
+///
+/// # Returns: Iterator over (name, mode, hexsha)
+#[pyfunction]
+fn sorted_tree_items(py: Python, entries: &PyDict, name_order: bool) -> PyResult<Vec<PyObject>> {
+    let mut qsort_entries = Vec::new();
+    for (name, e) in entries.iter() {
+        let (mode, sha): (u32, Vec<u8>) = match e.extract() {
+            Ok(o) => o,
+            Err(e) => {
+                return Err(PyTypeError::new_err((format!("invalid type: {}", e),)));
+            }
+        };
+        qsort_entries.push((name.extract::<Vec<u8>>().unwrap(), mode, sha));
+    }
+    if name_order {
+        qsort_entries.sort_by(|a, b| a.0.cmp(&b.0));
+    } else {
+        qsort_entries.sort_by(|a, b| {
+            name_with_suffix(a.1, a.0.as_slice()).cmp(&name_with_suffix(b.1, b.0.as_slice()))
+        });
+    }
+    let objectsm = py.import("dulwich.objects")?;
+    let tree_entry_cls = objectsm.getattr("TreeEntry")?;
+    qsort_entries
+        .into_iter()
+        .map(|(name, mode, hexsha)| -> PyResult<PyObject> {
+            Ok(tree_entry_cls
+                .call1((
+                    PyBytes::new(py, name.as_slice()).to_object(py),
+                    mode,
+                    PyBytes::new(py, hexsha.as_slice()).to_object(py),
+                ))?
+                .to_object(py))
+        })
+        .collect::<PyResult<Vec<PyObject>>>()
+}
+
+#[pymodule]
+fn _objects(_py: Python, m: &PyModule) -> PyResult<()> {
+    m.add_function(wrap_pyfunction!(sorted_tree_items, m)?)?;
+    m.add_function(wrap_pyfunction!(parse_tree, m)?)?;
+    Ok(())
+}

+ 4 - 4
dulwich/tests/test_objects.py

@@ -962,10 +962,10 @@ class TreeTests(ShaFileCheckTests):
 
         # ordering
         sha2 = hex_to_sha(b_sha)
-        self.assertCheckSucceeds(t, b"100644 a\0" + sha + b"\n100644 b\0" + sha)
-        self.assertCheckSucceeds(t, b"100644 a\0" + sha + b"\n100644 b\0" + sha2)
-        self.assertCheckFails(t, b"100644 a\0" + sha + b"\n100755 a\0" + sha2)
-        self.assertCheckFails(t, b"100644 b\0" + sha2 + b"\n100644 a\0" + sha)
+        self.assertCheckSucceeds(t, b"100644 a\0" + sha + b"100644 b\0" + sha)
+        self.assertCheckSucceeds(t, b"100644 a\0" + sha + b"100644 b\0" + sha2)
+        self.assertCheckFails(t, b"100644 a\0" + sha + b"100755 a\0" + sha2)
+        self.assertCheckFails(t, b"100644 b\0" + sha2 + b"100644 a\0" + sha)
 
     def test_iter(self):
         t = Tree()

+ 2 - 1
setup.py

@@ -6,6 +6,7 @@ import os
 import sys
 
 from setuptools import Extension, setup
+from setuptools_rust import RustExtension, Binding
 
 if sys.platform == "darwin" and os.path.exists("/usr/bin/xcodebuild"):
     # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
@@ -35,7 +36,6 @@ if "__pypy__" not in sys.modules and sys.platform != "win32":
 optional = os.environ.get("CIBUILDWHEEL", "0") != "1"
 
 ext_modules = [
-    Extension("dulwich._objects", ["dulwich/_objects.c"], optional=optional),
     Extension("dulwich._pack", ["dulwich/_pack.c"], optional=optional),
     Extension("dulwich._diff_tree", ["dulwich/_diff_tree.c"], optional=optional),
 ]
@@ -48,6 +48,7 @@ if "--pure" in sys.argv:
 
 setup(
     package_data={"": ["../docs/tutorial/*.txt", "py.typed"]},
+    rust_extensions=[RustExtension("dulwich._objects", "Cargo.toml", binding=Binding.PyO3, optional = True)],
     ext_modules=ext_modules,
     tests_require=tests_require,
 )