Skip to content

Commit 1d4d3d4

Browse files
mithroclaude
andcommitted
Support stripping hash type prefixes from checksum fields
**Feature:** Allow users to paste checksums with type prefixes like "sha256:abc123..." which are automatically stripped before validation. **Implementation:** - Split on first colon (":") to separate prefix from hash value - Strip any prefix before the colon (e.g., "md5:", "sha256:", "SHA-256:") - Normalize hash: lowercase, remove whitespace and dashes - Works with any prefix format - no hardcoded list **Updated behavior:** - MD5/SHA1 fields now accept hashes with or without prefixes - Whitespace and dashes are now stripped (previously invalid) - Increased max_length to 64 to accommodate prefixes - Removed HTML5 pattern attribute (validates in clean method instead) **Examples of supported formats:** - "abc123..." (plain hash - still works) - "md5:abc123..." (lowercase prefix) - "SHA256:ABC123..." (uppercase prefix, normalized to lowercase) - "sha-256:abc 123" (dashes in prefix, spaces in hash - all stripped) **Tests:** Added 8 new tests covering: - Various prefix formats (md5:, sha256:, SHA-256:) - Uppercase hashes (normalized to lowercase) - Whitespace and dashes (now stripped) - Backwards compatibility (hashes without prefixes) All 27 form tests pass. 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4179f0f commit 1d4d3d4

File tree

2 files changed

+133
-20
lines changed

2 files changed

+133
-20
lines changed

wafer_space/projects/forms.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,30 +64,34 @@ class ProjectFileURLSubmitForm(forms.Form):
6464

6565
expected_hash_md5 = forms.CharField(
6666
label="MD5 Hash",
67-
max_length=32,
67+
max_length=64, # Increased to allow for prefix like "sha256:"
6868
required=False,
6969
widget=forms.TextInput(
7070
attrs={
7171
"class": "form-control",
72-
"placeholder": "abc123def456...",
73-
"pattern": "[a-fA-F0-9]{32}",
72+
"placeholder": "abc123def456... or md5:abc123def456...",
7473
},
7574
),
76-
help_text="MD5 checksum for file integrity verification (32 hex characters)",
75+
help_text=(
76+
"MD5 checksum for file integrity verification (32 hex characters). "
77+
"Optional prefix like 'md5:' will be stripped."
78+
),
7779
)
7880

7981
expected_hash_sha1 = forms.CharField(
8082
label="SHA1 Hash",
81-
max_length=40,
83+
max_length=64, # Increased to allow for prefix like "sha256:"
8284
required=False,
8385
widget=forms.TextInput(
8486
attrs={
8587
"class": "form-control",
86-
"placeholder": "abc123def456...",
87-
"pattern": "[a-fA-F0-9]{40}",
88+
"placeholder": "abc123def456... or sha1:abc123def456...",
8889
},
8990
),
90-
help_text="SHA1 checksum for file integrity verification (40 hex characters)",
91+
help_text=(
92+
"SHA1 checksum for file integrity verification (40 hex characters). "
93+
"Optional prefix like 'sha1:' will be stripped."
94+
),
9195
)
9296

9397
def clean_url(self):
@@ -108,12 +112,21 @@ def clean_url(self):
108112
return url
109113

110114
def clean_expected_hash_md5(self):
111-
"""Validate MD5 hash format."""
115+
"""Validate MD5 hash format.
116+
117+
Supports hash values with or without type prefix.
118+
Examples: "md5:abc123..." or "abc123..."
119+
"""
112120
md5_hash = self.cleaned_data.get("expected_hash_md5", "").strip()
113121

114122
if not md5_hash:
115123
return ""
116124

125+
# Strip hash type prefix if present (e.g., "md5:", "sha256:", etc.)
126+
if ":" in md5_hash:
127+
# Take the part after the first colon
128+
md5_hash = md5_hash.split(":", 1)[1].strip()
129+
117130
# Remove any whitespace or dashes
118131
md5_hash = md5_hash.replace(" ", "").replace("-", "")
119132

@@ -132,12 +145,21 @@ def clean_expected_hash_md5(self):
132145
return md5_hash.lower()
133146

134147
def clean_expected_hash_sha1(self):
135-
"""Validate SHA1 hash format."""
148+
"""Validate SHA1 hash format.
149+
150+
Supports hash values with or without type prefix.
151+
Examples: "sha1:abc123..." or "abc123..."
152+
"""
136153
sha1_hash = self.cleaned_data.get("expected_hash_sha1", "").strip()
137154

138155
if not sha1_hash:
139156
return ""
140157

158+
# Strip hash type prefix if present (e.g., "sha1:", "sha256:", etc.)
159+
if ":" in sha1_hash:
160+
# Take the part after the first colon
161+
sha1_hash = sha1_hash.split(":", 1)[1].strip()
162+
141163
# Remove any whitespace or dashes
142164
sha1_hash = sha1_hash.replace(" ", "").replace("-", "")
143165

wafer_space/projects/tests/test_forms.py

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -212,29 +212,31 @@ def test_form_sha1_hash_cleaned_lowercase(self):
212212
assert form.is_valid()
213213
assert form.cleaned_data["expected_hash_sha1"] == expected_sha1
214214

215-
def test_form_md5_hash_with_whitespace_invalid(self):
216-
"""Test MD5 hash with whitespace is invalid (pattern validation)."""
215+
def test_form_md5_hash_with_whitespace_stripped(self):
216+
"""Test MD5 hash with whitespace is stripped and normalized."""
217+
expected_md5 = "abc123def456789012345678901234ab"
217218
form_data = {
218219
"url": "https://example.com/file.gds",
219220
"expected_hash_md5": "abc123 def456 789012 345678 901234ab",
220221
}
221222
form = ProjectFileURLSubmitForm(data=form_data)
222223

223-
# HTML5 pattern attribute rejects this before clean method runs
224-
assert not form.is_valid()
225-
assert "expected_hash_md5" in form.errors
224+
# Whitespace is stripped by clean method
225+
assert form.is_valid()
226+
assert form.cleaned_data["expected_hash_md5"] == expected_md5
226227

227-
def test_form_md5_hash_with_dashes_invalid(self):
228-
"""Test MD5 hash with dashes is invalid (pattern validation)."""
228+
def test_form_md5_hash_with_dashes_stripped(self):
229+
"""Test MD5 hash with dashes is stripped and normalized."""
230+
expected_md5 = "abc123def456789012345678901234ab"
229231
form_data = {
230232
"url": "https://example.com/file.gds",
231233
"expected_hash_md5": "abc123-def456-789012-345678-901234ab",
232234
}
233235
form = ProjectFileURLSubmitForm(data=form_data)
234236

235-
# HTML5 pattern attribute rejects this before clean method runs
236-
assert not form.is_valid()
237-
assert "expected_hash_md5" in form.errors
237+
# Dashes are stripped by clean method
238+
assert form.is_valid()
239+
assert form.cleaned_data["expected_hash_md5"] == expected_md5
238240

239241
def test_form_invalid_md5_hash_wrong_length(self):
240242
"""Test MD5 hash with wrong length is invalid."""
@@ -327,3 +329,92 @@ def test_form_requires_at_least_one_hash(self):
327329
error_msg = str(form.errors["__all__"])
328330
assert "At least one checksum" in error_msg
329331
assert "MD5 or SHA1" in error_msg
332+
333+
def test_form_md5_hash_strips_md5_prefix(self):
334+
"""Test MD5 hash with 'md5:' prefix is stripped and normalized."""
335+
expected_md5 = "abc123def456789012345678901234ab"
336+
form_data = {
337+
"url": "https://example.com/file.gds",
338+
"expected_hash_md5": f"md5:{expected_md5}",
339+
}
340+
form = ProjectFileURLSubmitForm(data=form_data)
341+
342+
assert form.is_valid()
343+
assert form.cleaned_data["expected_hash_md5"] == expected_md5
344+
345+
def test_form_md5_hash_strips_sha256_prefix(self):
346+
"""Test MD5 hash with 'sha256:' prefix is stripped (any prefix works)."""
347+
expected_md5 = "abc123def456789012345678901234ab"
348+
form_data = {
349+
"url": "https://example.com/file.gds",
350+
"expected_hash_md5": f"sha256:{expected_md5}",
351+
}
352+
form = ProjectFileURLSubmitForm(data=form_data)
353+
354+
assert form.is_valid()
355+
assert form.cleaned_data["expected_hash_md5"] == expected_md5
356+
357+
def test_form_md5_hash_strips_uppercase_prefix(self):
358+
"""Test MD5 hash with uppercase prefix is stripped and hash normalized."""
359+
expected_md5 = "abc123def456789012345678901234ab"
360+
form_data = {
361+
"url": "https://example.com/file.gds",
362+
"expected_hash_md5": f"MD5:{expected_md5.upper()}",
363+
}
364+
form = ProjectFileURLSubmitForm(data=form_data)
365+
366+
assert form.is_valid()
367+
# Hash should be lowercased
368+
assert form.cleaned_data["expected_hash_md5"] == expected_md5
369+
370+
def test_form_sha1_hash_strips_sha1_prefix(self):
371+
"""Test SHA1 hash with 'sha1:' prefix is stripped and normalized."""
372+
expected_sha1 = "abc123def456789012345678901234567890abcd"
373+
form_data = {
374+
"url": "https://example.com/file.gds",
375+
"expected_hash_sha1": f"sha1:{expected_sha1}",
376+
}
377+
form = ProjectFileURLSubmitForm(data=form_data)
378+
379+
assert form.is_valid()
380+
assert form.cleaned_data["expected_hash_sha1"] == expected_sha1
381+
382+
def test_form_sha1_hash_strips_sha256_prefix(self):
383+
"""Test SHA1 hash with 'sha256:' prefix is stripped (any prefix works)."""
384+
expected_sha1 = "abc123def456789012345678901234567890abcd"
385+
form_data = {
386+
"url": "https://example.com/file.gds",
387+
"expected_hash_sha1": f"sha256:{expected_sha1}",
388+
}
389+
form = ProjectFileURLSubmitForm(data=form_data)
390+
391+
assert form.is_valid()
392+
assert form.cleaned_data["expected_hash_sha1"] == expected_sha1
393+
394+
def test_form_sha1_hash_strips_uppercase_dashed_prefix(self):
395+
"""Test SHA1 hash with 'SHA-256:' prefix is stripped and hash normalized."""
396+
expected_sha1 = "abc123def456789012345678901234567890abcd"
397+
form_data = {
398+
"url": "https://example.com/file.gds",
399+
"expected_hash_sha1": f"SHA-256:{expected_sha1.upper()}",
400+
}
401+
form = ProjectFileURLSubmitForm(data=form_data)
402+
403+
assert form.is_valid()
404+
# Hash should be lowercased
405+
assert form.cleaned_data["expected_hash_sha1"] == expected_sha1
406+
407+
def test_form_hash_without_prefix_still_works(self):
408+
"""Test that hashes without prefixes still work (backwards compatibility)."""
409+
expected_md5 = "abc123def456789012345678901234ab"
410+
expected_sha1 = "abc123def456789012345678901234567890abcd"
411+
form_data = {
412+
"url": "https://example.com/file.gds",
413+
"expected_hash_md5": expected_md5,
414+
"expected_hash_sha1": expected_sha1,
415+
}
416+
form = ProjectFileURLSubmitForm(data=form_data)
417+
418+
assert form.is_valid()
419+
assert form.cleaned_data["expected_hash_md5"] == expected_md5
420+
assert form.cleaned_data["expected_hash_sha1"] == expected_sha1

0 commit comments

Comments
 (0)