diff --git a/docs/tool-guide.md b/docs/tool-guide.md index ed54a58..5912597 100644 --- a/docs/tool-guide.md +++ b/docs/tool-guide.md @@ -40,10 +40,13 @@ Header cells that are empty or whitespace-only are reported by zero-based positi `empty_header_indices`; a blank name is a warning, not an invalid document, and the original names are kept in `columns` unchanged. -## hash_text(text, algorithm="sha256") +## hash_text(text, algorithm="sha256", uppercase=False) Algorithms: `sha256`, `sha512`, `blake2b`. Returns `algorithm` and lowercase -hexadecimal `digest`. Text is encoded as UTF-8. Empty strings are supported. +hexadecimal `digest`. Pass `uppercase=True` for an uppercase digest; the +`algorithm` name is unchanged either way. For example, +`hash_text("abc", uppercase=True)` starts with `BA7816BF`. Text is encoded as +UTF-8. Empty strings are supported. These hashes compare content; this tool does not provide password storage. Unknown algorithms raise `ValueError`. diff --git a/src/mcp_tools_lab/tools/hashing.py b/src/mcp_tools_lab/tools/hashing.py index e66e761..44161d3 100644 --- a/src/mcp_tools_lab/tools/hashing.py +++ b/src/mcp_tools_lab/tools/hashing.py @@ -4,9 +4,14 @@ from typing import Literal -def hash_text(text: str, algorithm: Literal["sha256", "sha512", "blake2b"] = "sha256") -> dict: - """Hash UTF-8 text using SHA-256, SHA-512, or BLAKE2b; return lowercase hex.""" +def hash_text(text: str, algorithm: Literal["sha256", "sha512", "blake2b"] = "sha256", uppercase: bool = False) -> dict: + """Hash UTF-8 text using SHA-256, SHA-512, or BLAKE2b; return hex. + + The digest is lowercase by default. Pass uppercase=True for an uppercase + digest; the algorithm name is returned unchanged. + """ algorithms = {"sha256": hashlib.sha256, "sha512": hashlib.sha512, "blake2b": hashlib.blake2b} if algorithm not in algorithms: raise ValueError("algorithm must be sha256, sha512, or blake2b.") - return {"algorithm": algorithm, "digest": algorithms[algorithm](text.encode("utf-8")).hexdigest()} + digest = algorithms[algorithm](text.encode("utf-8")).hexdigest() + return {"algorithm": algorithm, "digest": digest.upper() if uppercase else digest} diff --git a/tests/test_hashing.py b/tests/test_hashing.py index e8e8be4..4e8e6b2 100644 --- a/tests/test_hashing.py +++ b/tests/test_hashing.py @@ -16,3 +16,29 @@ def test_algorithms(algorithm, length): def test_unsupported_algorithm(): with pytest.raises(ValueError, match="algorithm"): hash_text("abc", "md5") + + +def test_uppercase_digest(): + result = hash_text("abc", uppercase=True) + assert result["digest"] == "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD" + assert result["algorithm"] == "sha256" + + +def test_uppercase_is_optional_and_algorithm_is_unchanged(): + assert hash_text("abc")["digest"].startswith("ba7816bf") + assert hash_text("abc", uppercase=False) == hash_text("abc") + + +@pytest.mark.parametrize("algorithm", ["sha256", "sha512", "blake2b"]) +def test_uppercase_supports_every_algorithm(algorithm): + lower = hash_text("abc", algorithm) + upper = hash_text("abc", algorithm, uppercase=True) + assert upper["algorithm"] == algorithm + assert upper["digest"] == lower["digest"].upper() + assert len(upper["digest"]) == len(lower["digest"]) + + +def test_uppercase_empty_text(): + result = hash_text("", uppercase=True) + assert result["digest"] == hash_text("", "sha256")["digest"].upper() + assert result["digest"] == "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"