1# hash.py -- Object format abstraction layer for Git
2# Copyright (C) 2024 The Dulwich contributors
3#
4# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
5# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
6# General Public License as published by the Free Software Foundation; version 2.0
7# or (at your option) any later version. You can redistribute it and/or
8# modify it under the terms of either of these two licenses.
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16# You should have received a copy of the licenses; if not, see
17# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
18# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
19# License, Version 2.0.
20#
21
22"""Object format abstraction for Git objects.
23
24This module provides an abstraction layer for different object formats
25used in Git repositories (SHA-1 and SHA-256).
26"""
27
28__all__ = [
29 "DEFAULT_OBJECT_FORMAT",
30 "OBJECT_FORMATS",
31 "OBJECT_FORMAT_TYPE_NUMS",
32 "SHA1",
33 "SHA256",
34 "ObjectFormat",
35 "get_object_format",
36 "verify_same_object_format",
37]
38
39from collections.abc import Callable
40from hashlib import sha1, sha256
41from typing import TYPE_CHECKING
42
43if TYPE_CHECKING:
44 from _hashlib import HASH
45
46
47class ObjectFormat:
48 """Object format (hash algorithm) used in Git."""
49
50 def __init__(
51 self,
52 name: str,
53 type_num: int,
54 oid_length: int,
55 hex_length: int,
56 hash_func: Callable[[], "HASH"],
57 ) -> None:
58 """Initialize an object format.
59
60 Args:
61 name: Name of the format (e.g., "sha1", "sha256")
62 type_num: Format type number used in Git
63 oid_length: Length of the binary object ID in bytes
64 hex_length: Length of the hexadecimal object ID in characters
65 hash_func: Hash function from hashlib
66 """
67 self.name = name
68 self.type_num = type_num
69 self.oid_length = oid_length
70 self.hex_length = hex_length
71 self.hash_func = hash_func
72
73 def __str__(self) -> str:
74 """Return string representation."""
75 return self.name
76
77 def __repr__(self) -> str:
78 """Return repr."""
79 return f"ObjectFormat({self.name!r})"
80
81 def new_hash(self) -> "HASH":
82 """Create a new hash object."""
83 return self.hash_func()
84
85 def hash_object(self, data: bytes) -> bytes:
86 """Hash data and return the digest.
87
88 Args:
89 data: Data to hash
90
91 Returns:
92 Binary digest
93 """
94 h = self.new_hash()
95 h.update(data)
96 return h.digest()
97
98 def hash_object_hex(self, data: bytes) -> bytes:
99 """Hash data and return the hexadecimal digest.
100
101 Args:
102 data: Data to hash
103
104 Returns:
105 Hexadecimal digest as bytes
106 """
107 h = self.new_hash()
108 h.update(data)
109 return h.hexdigest().encode("ascii")
110
111
112# Define the supported object formats
113SHA1 = ObjectFormat("sha1", type_num=1, oid_length=20, hex_length=40, hash_func=sha1)
114SHA256 = ObjectFormat(
115 "sha256", type_num=20, oid_length=32, hex_length=64, hash_func=sha256
116)
117
118# Map of format names to ObjectFormat instances
119OBJECT_FORMATS = {
120 "sha1": SHA1,
121 "sha256": SHA256,
122}
123
124# Map of format numbers to ObjectFormat instances
125OBJECT_FORMAT_TYPE_NUMS = {
126 1: SHA1,
127 2: SHA256,
128}
129
130# Default format for backward compatibility
131DEFAULT_OBJECT_FORMAT = SHA1
132
133
134def get_object_format(name: str | None = None) -> ObjectFormat:
135 """Get an object format by name.
136
137 Args:
138 name: Format name ("sha1" or "sha256"). If None, returns default.
139
140 Returns:
141 ObjectFormat instance
142
143 Raises:
144 ValueError: If the format name is not supported
145 """
146 if name is None:
147 return DEFAULT_OBJECT_FORMAT
148 try:
149 return OBJECT_FORMATS[name.lower()]
150 except KeyError:
151 raise ValueError(f"Unsupported object format: {name}")
152
153
154def verify_same_object_format(*formats: ObjectFormat) -> ObjectFormat:
155 """Verify that all provided object formats are the same.
156
157 Args:
158 *formats: Object format instances to verify
159
160 Returns:
161 The common object format
162
163 Raises:
164 ValueError: If formats don't match or no formats provided
165 """
166 if not formats:
167 raise ValueError("At least one object format must be provided")
168
169 first = formats[0]
170 for fmt in formats[1:]:
171 if fmt != first:
172 raise ValueError(f"Object format mismatch: {first.name} != {fmt.name}")
173
174 return first