|
1 | 1 | # SPDX-FileCopyrightText: 2025 Helio Chissini de Castro <heliocastro@gmail.com> |
2 | | -# |
3 | 2 | # SPDX-License-Identifier: MIT |
4 | 3 |
|
| 4 | +from typing import ClassVar |
5 | 5 |
|
6 | | -from enum import Enum |
| 6 | +from pydantic import BaseModel, Field, model_validator |
7 | 7 |
|
8 | 8 |
|
9 | | -class HashAlgorithm(Enum): |
| 9 | +class HashAlgorithm(BaseModel): |
10 | 10 | """ |
11 | | - An enum of supported hash algorithms. Each algorithm has one or more [aliases] associated to it, |
12 | | - where the first alias is the definite name. |
13 | | -
|
14 | | - Attributes: |
15 | | - NONE: No hash algorithm. |
16 | | - UNKNOWN: An unknown hash algorithm. |
17 | | - MD5: The Message-Digest 5 hash algorithm, see [MD5](http://en.wikipedia.org/wiki/MD5). |
18 | | - SHA1: The Secure Hash Algorithm 1, see [SHA-1](https://en.wikipedia.org/wiki/SHA-1). |
19 | | - SHA256: The Secure Hash Algorithm 2 with 256 bits, see [SHA-256](https://en.wikipedia.org/wiki/SHA-256). |
20 | | - SHA384: The Secure Hash Algorithm 2 with 384 bits, see [SHA-384](https://en.wikipedia.org/wiki/SHA-384). |
21 | | - SHA512: The Secure Hash Algorithm 2 with 512 bits, see [SHA-512](https://en.wikipedia.org/wiki/SHA-512). |
22 | | - SHA1GIT: The Secure Hash Algorithm 1, but calculated on a Git "blob" object, see |
23 | | - - https://git-scm.com/book/en/v2/Git-Internals-Git-Objects#_object_storage |
24 | | - - https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html#git-compatibility |
| 11 | + A Python port of the Kotlin HashAlgorithm enum class. |
| 12 | +
|
| 13 | + Each algorithm has one or more aliases, an empty hash value, |
| 14 | + and an 'is_verifiable' flag. |
25 | 15 | """ |
26 | 16 |
|
27 | | - NONE = "NONE" |
28 | | - UNKNOWN = "UNKNOWN" |
29 | | - MD5 = "MD5" |
30 | | - SHA1 = "SHA1" |
31 | | - SHA256 = "SHA256" |
32 | | - SHA384 = "SHA384" |
33 | | - SHA512 = "SHA512" |
34 | | - SHA1GIT = ( |
35 | | - ["SHA-1-GIT", "SHA1-GIT", "SHA1GIT", "SWHID"], |
36 | | - "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", |
37 | | - ) |
| 17 | + aliases: list[str] = Field(default_factory=list) |
| 18 | + empty_value: str = "" |
| 19 | + is_verifiable: bool = True |
| 20 | + |
| 21 | + # ---- known algorithms ---- |
| 22 | + NONE: ClassVar["HashAlgorithm"] |
| 23 | + UNKNOWN: ClassVar["HashAlgorithm"] |
| 24 | + MD5: ClassVar["HashAlgorithm"] |
| 25 | + SHA1: ClassVar["HashAlgorithm"] |
| 26 | + SHA256: ClassVar["HashAlgorithm"] |
| 27 | + SHA384: ClassVar["HashAlgorithm"] |
| 28 | + SHA512: ClassVar["HashAlgorithm"] |
| 29 | + SHA1GIT: ClassVar["HashAlgorithm"] |
| 30 | + |
| 31 | + # ---- derived property ---- |
| 32 | + @property |
| 33 | + def size(self) -> int: |
| 34 | + """The length of the empty hash string for this algorithm.""" |
| 35 | + return len(self.empty_value) |
| 36 | + |
| 37 | + # ---- validation ---- |
| 38 | + @model_validator(mode="before") |
| 39 | + @classmethod |
| 40 | + def _from_alias(cls, value): |
| 41 | + """Allow initialization from alias string.""" |
| 42 | + if isinstance(value, str): |
| 43 | + algo = cls.from_string(value) |
| 44 | + return algo.model_dump() |
| 45 | + return value |
| 46 | + |
| 47 | + # ---- class methods ---- |
| 48 | + @classmethod |
| 49 | + def from_string(cls, alias: str) -> "HashAlgorithm": |
| 50 | + """Find a HashAlgorithm by alias name (case-insensitive).""" |
| 51 | + alias_upper = alias.upper() |
| 52 | + for algo in cls._entries(): |
| 53 | + if any(a.upper() == alias_upper for a in algo.aliases): |
| 54 | + return algo |
| 55 | + return cls.UNKNOWN |
| 56 | + |
| 57 | + @classmethod |
| 58 | + def create(cls, value: str) -> "HashAlgorithm": |
| 59 | + """ |
| 60 | + Create a HashAlgorithm from a hash value string, based on its length. |
| 61 | + Returns NONE if value is blank, UNKNOWN otherwise. |
| 62 | + """ |
| 63 | + if not value.strip(): |
| 64 | + return cls.NONE |
| 65 | + for algo in cls._entries(): |
| 66 | + if len(value) == algo.size: |
| 67 | + return algo |
| 68 | + return cls.UNKNOWN |
| 69 | + |
| 70 | + @classmethod |
| 71 | + def _entries(cls) -> list["HashAlgorithm"]: |
| 72 | + """Return the list of all defined algorithms.""" |
| 73 | + return [ |
| 74 | + cls.NONE, |
| 75 | + cls.UNKNOWN, |
| 76 | + cls.MD5, |
| 77 | + cls.SHA1, |
| 78 | + cls.SHA256, |
| 79 | + cls.SHA384, |
| 80 | + cls.SHA512, |
| 81 | + cls.SHA1GIT, |
| 82 | + ] |
| 83 | + |
| 84 | + def __str__(self) -> str: |
| 85 | + return self.aliases[0] if self.aliases else "" |
| 86 | + |
| 87 | + |
| 88 | +HashAlgorithm.NONE = HashAlgorithm(aliases=[""], empty_value="", is_verifiable=False) |
| 89 | +HashAlgorithm.UNKNOWN = HashAlgorithm(aliases=["UNKNOWN"], empty_value="", is_verifiable=False) |
| 90 | +HashAlgorithm.MD5 = HashAlgorithm( |
| 91 | + aliases=["MD5"], |
| 92 | + empty_value="d41d8cd98f00b204e9800998ecf8427e", |
| 93 | +) |
| 94 | +HashAlgorithm.SHA1 = HashAlgorithm( |
| 95 | + aliases=["SHA-1", "SHA1"], |
| 96 | + empty_value="da39a3ee5e6b4b0d3255bfef95601890afd80709", |
| 97 | +) |
| 98 | +HashAlgorithm.SHA256 = HashAlgorithm( |
| 99 | + aliases=["SHA-256", "SHA256"], |
| 100 | + empty_value="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", |
| 101 | +) |
| 102 | +HashAlgorithm.SHA384 = HashAlgorithm( |
| 103 | + aliases=["SHA-384", "SHA384"], |
| 104 | + empty_value=("38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b"), |
| 105 | +) |
| 106 | +HashAlgorithm.SHA512 = HashAlgorithm( |
| 107 | + aliases=["SHA-512", "SHA512"], |
| 108 | + empty_value=( |
| 109 | + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce" |
| 110 | + "47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" |
| 111 | + ), |
| 112 | +) |
| 113 | +HashAlgorithm.SHA1GIT = HashAlgorithm( |
| 114 | + aliases=["SHA-1-GIT", "SHA1-GIT", "SHA1GIT", "SWHID"], |
| 115 | + empty_value="e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", |
| 116 | +) |
0 commit comments