From 84d443d770374a50408e3ac9fe86ae578a8afb65 Mon Sep 17 00:00:00 2001 From: happylittle7 Date: Sat, 27 Dec 2025 04:52:44 +0800 Subject: [PATCH] fix: update checksum method from MD5 to SHA256 and add related functions --- docs/problems.MD | 4 +-- problems/services/storage.py | 34 ++++++++++++++++++ problems/views/api.py | 13 ++++--- submissions/sandbox_client.py | 68 ++++++++++++++++------------------- 4 files changed, 72 insertions(+), 47 deletions(-) diff --git a/docs/problems.MD b/docs/problems.MD index 2e379a89..b93dc46f 100644 --- a/docs/problems.MD +++ b/docs/problems.MD @@ -147,12 +147,12 @@ curl "http://127.0.0.1:8000/problem/30/test-case" -H "Authorization: Bearer $TOK 安全驗證:使用 query string `token` 與後端設定的 `SANDBOX_TOKEN` 比對;不需一般登入權限。若 token 缺失或不符 → 401。 -#### 1) 取得 MD5 校驗和 +#### 1) 取得 SHA256 校驗和 - 路徑:`GET /problem//checksum?token=` - 成功回應: ```json { - "data": { "checksum": "d41d8cd98f00b204e9800998ecf8427e" }, + "data": { "checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, "message": "OK", "status": "200" } diff --git a/problems/services/storage.py b/problems/services/storage.py index 64904ed4..e9b734d1 100644 --- a/problems/services/storage.py +++ b/problems/services/storage.py @@ -30,3 +30,37 @@ def save_testcase_file(problem_id: int, subtask_id: int, idx: int, kind: str, fi def open_testcase_file(rel_path: str): return _storage.open(rel_path, "rb") + +def get_problem_testcase_hash(problem_id: int) -> str | None: + """ + 取得題目測資包的 SHA256 hash 值 + + Args: + problem_id: 題目 ID + + Returns: + str: 測資包的 SHA256 hash,若不存在則回傳 None + """ + rel = os.path.join("testcases", f"p{problem_id}", "problem.zip") + if not _storage.exists(rel): + return None + + with _storage.open(rel, 'rb') as f: + return _sha256_of_fileobj(f) + + +def get_problem_testcase_path(problem_id: int) -> str | None: + """ + 取得題目測資包的路徑 + + Args: + problem_id: 題目 ID + + Returns: + str: 測資包路徑,若不存在則回傳 None + """ + rel = os.path.join("testcases", f"p{problem_id}", "problem.zip") + if not _storage.exists(rel): + return None + + return _storage.path(rel) \ No newline at end of file diff --git a/problems/views/api.py b/problems/views/api.py index da7c9401..79fb9396 100644 --- a/problems/views/api.py +++ b/problems/views/api.py @@ -579,9 +579,9 @@ def build_meta_entry(ss_idx: int): class ProblemTestCaseChecksumView(APIView): """GET /problem//checksum (Sandbox 專用) - 目的:沙盒下載測資 zip 後驗證 MD5 完整性。 + 目的:沙盒下載測資 zip 後驗證 SHA256 完整性。 驗證:使用 query string `token` 與後端設定的 SANDBOX_TOKEN 比對。 - 回傳:{"checksum": ""} + 回傳:{"checksum": ""} 錯誤:401 token 無效;404 題目或測資不存在。 """ permission_classes = [] # 以 sandbox token 驗證,不用一般身份驗證 @@ -592,15 +592,14 @@ def get(self, request, pk: int): if not token_expected or token_req != token_expected: return api_response(None, "Invalid sandbox token", status_code=401) problem = get_object_or_404(Problems, pk=pk) - from ..services.storage import _storage + from ..services.storage import _storage, _sha256_of_fileobj rel = os.path.join("testcases", f"p{problem.id}", "problem.zip") if not _storage.exists(rel): raise Http404("Test case archive not found") - # 計算 MD5 - import hashlib + # 計算 SHA256 with _storage.open(rel, 'rb') as fh: - md5 = hashlib.md5(fh.read()).hexdigest() - return api_response({"checksum": md5}, "OK", status_code=200) + sha256 = _sha256_of_fileobj(fh) + return api_response({"checksum": sha256}, "OK", status_code=200) class ProblemTestCaseMetaView(APIView): diff --git a/submissions/sandbox_client.py b/submissions/sandbox_client.py index 2ffb4311..be697ba7 100644 --- a/submissions/sandbox_client.py +++ b/submissions/sandbox_client.py @@ -9,6 +9,9 @@ from io import BytesIO from django.conf import settings +from problems.services.storage import get_problem_testcase_hash + + logger = logging.getLogger(__name__) # Sandbox API 設定 @@ -58,6 +61,7 @@ def submit_to_sandbox(submission): Raises: requests.RequestException: API 請求失敗 + ValueError: 題目測資包不存在 """ from problems.models import Problems, Problem_subtasks @@ -71,7 +75,7 @@ def submit_to_sandbox(submission): time_limit = subtask.time_limit_ms / 1000.0 # 轉換成秒 else: time_limit = 1.0 # 預設 1 秒 - + if subtask and subtask.memory_limit_mb: memory_limit = subtask.memory_limit_mb * 1024 # 轉換成 KB else: @@ -80,66 +84,54 @@ def submit_to_sandbox(submission): # 3. 轉換語言代碼 language = convert_language_code(submission.language_type) - # 4. 組裝 payload(multipart/form-data) + # 4. 取得題目包 hash + problem_hash = get_problem_testcase_hash(submission.problem_id) + if not problem_hash: + raise ValueError(f"Problem {submission.problem_id} has no testcase package") + + # 5. 組裝 payload(multipart/form-data) data = { 'submission_id': str(submission.id), 'problem_id': str(submission.problem_id), - 'problem_hash': f'TODO_HASH_{submission.problem_id}', # TODO: 實現題目包管理後取得真實 hash - 'mode': 'normal', # 目前只支援 single file + 'problem_hash': problem_hash, + 'mode': 'normal', 'language': language, 'file_hash': submission.code_hash, 'time_limit': time_limit, 'memory_limit': memory_limit, - # 只有在啟用自訂 checker 時才使用設定的 checker_name,否則強制使用 'diff' - 'use_checker': problem.use_custom_checker, - 'checker_name': problem.checker_name if problem.use_custom_checker else 'diff', - 'use_static_analysis': False, # TODO: 從 assignment 設定取得 - 'priority': 0, # 一般優先級 - 'callback_url': f'{settings.BACKEND_BASE_URL}/submissions/callback/', # Sandbox 判題完成後回傳結果的 URL + 'use_checker': getattr(problem, 'use_custom_checker', False), + 'checker_name': getattr(problem, 'checker_name', 'diff') or 'diff', + 'use_static_analysis': False, + 'priority': 0, + 'callback_url': f'{settings.BACKEND_BASE_URL}/submissions/callback/', } - # 5. 準備檔案 - filename = f'solution.{get_file_extension(language)}' - file_content = submission.source_code.encode('utf-8') + # 6. 準備程式碼檔案 + source_code = submission.source_code or '' + file_extension = get_file_extension(submission.language_type) files = { - 'file': (filename, BytesIO(file_content), 'text/plain') + 'file': (f'solution{file_extension}', BytesIO(source_code.encode('utf-8')), 'text/plain') } - # 6. 發送請求 - url = f'{SANDBOX_API_URL}/api/v1/submissions' - logger.info(f'Submitting to Sandbox: submission_id={submission.id}, problem_id={submission.problem_id}') - - # 準備 headers(包含認證) + # 7. 發送請求 headers = {} - if SANDBOX_API_KEY: - headers['X-API-KEY'] = SANDBOX_API_KEY + api_key = getattr(settings, 'SANDBOX_API_KEY', None) + if api_key: + headers['X-API-KEY'] = api_key response = requests.post( - url, + f'{settings.SANDBOX_API_URL}/api/v1/submissions', data=data, files=files, headers=headers, - timeout=SANDBOX_TIMEOUT + timeout=getattr(settings, 'SANDBOX_TIMEOUT', 30) ) - # 7. 檢查回應 response.raise_for_status() - result = response.json() - - logger.info(f'Sandbox response: {result}') - return result + return response.json() except Problems.DoesNotExist: - logger.error(f'Problem not found: problem_id={submission.problem_id}') - raise ValueError(f'Problem {submission.problem_id} not found') - - except requests.RequestException as e: - logger.error(f'Sandbox API error: {str(e)}') - raise - - except Exception as e: - logger.error(f'Unexpected error submitting to sandbox: {str(e)}') - raise + raise ValueError(f"Problem {submission.problem_id} does not exist") def submit_selftest_to_sandbox(problem_id, language_type, source_code, stdin_data):