Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/problems.MD
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,12 @@ curl "http://127.0.0.1:8000/problem/30/test-case" -H "Authorization: Bearer $TOK

Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line still references "MD5" when describing the checksum verification process. Since the PR updates checksum calculation from MD5 to SHA256 throughout the system, this should be updated to say "SHA256" instead of "MD5" for consistency with the rest of the changes.

Copilot uses AI. Check for mistakes.
安全驗證:使用 query string `token` 與後端設定的 `SANDBOX_TOKEN` 比對;不需一般登入權限。若 token 缺失或不符 → 401。

#### 1) 取得 MD5 校驗和
#### 1) 取得 SHA256 校驗和
- 路徑:`GET /problem/<problem_id>/checksum?token=<sandbox_token>`
- 成功回應:
```json
{
"data": { "checksum": "d41d8cd98f00b204e9800998ecf8427e" },
"data": { "checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" },
"message": "OK",
"status": "200"
}
Expand Down
34 changes: 34 additions & 0 deletions problems/services/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
13 changes: 6 additions & 7 deletions problems/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,9 +579,9 @@ def build_meta_entry(ss_idx: int):

class ProblemTestCaseChecksumView(APIView):
"""GET /problem/<pk>/checksum (Sandbox 專用)
目的:沙盒下載測資 zip 後驗證 MD5 完整性。
目的:沙盒下載測資 zip 後驗證 SHA256 完整性。
驗證:使用 query string `token` 與後端設定的 SANDBOX_TOKEN 比對。
回傳:{"checksum": "<md5>"}
回傳:{"checksum": "<sha256>"}
錯誤:401 token 無效;404 題目或測資不存在。
"""
permission_classes = [] # 以 sandbox token 驗證,不用一般身份驗證
Expand All @@ -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):
Expand Down
68 changes: 30 additions & 38 deletions submissions/sandbox_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 設定
Expand Down Expand Up @@ -58,6 +61,7 @@ def submit_to_sandbox(submission):

Raises:
requests.RequestException: API 請求失敗
ValueError: 題目測資包不存在
"""
from problems.models import Problems, Problem_subtasks

Expand All @@ -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:
Expand All @@ -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)
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_file_extension function is being called with submission.language_type, which is a numeric code (0=C, 1=C++, 2=Python, etc.). However, get_file_extension expects a language string ('c', 'cpp', 'python', etc.) as its parameter. The converted language string is already available in the language variable from line 85. This should be changed to use language instead of submission.language_type.

Suggested change
file_extension = get_file_extension(submission.language_type)
file_extension = get_file_extension(language)

Copilot uses AI. Check for mistakes.
files = {
'file': (filename, BytesIO(file_content), 'text/plain')
'file': (f'solution{file_extension}', BytesIO(source_code.encode('utf-8')), 'text/plain')
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filename is missing a dot before the extension. The current code generates filenames like 'solutionpy' or 'solutioncpp' instead of 'solution.py' or 'solution.cpp'. This should be changed to f'solution.{file_extension}'.

Suggested change
'file': (f'solution{file_extension}', BytesIO(source_code.encode('utf-8')), 'text/plain')
'file': (f'solution.{file_extension}', BytesIO(source_code.encode('utf-8')), 'text/plain')

Copilot uses AI. Check for mistakes.
}

# 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()
Comment on lines 122 to +131
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The informative logging statements that were present in the old code have been removed. The old implementation logged the submission details before sending the request and the sandbox response upon success. These logs are valuable for debugging and monitoring. Consider re-adding at least a log statement before sending the request (e.g., logger.info(f'Submitting to Sandbox: submission_id={submission.id}, problem_id={submission.problem_id}')) and after receiving a successful response.

Copilot uses AI. Check for mistakes.

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")
Comment on lines 133 to +134
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling has been reduced compared to the previous implementation. The old code had specific handling for requests.RequestException and a general Exception catch with appropriate logging. Without these handlers, network errors and other exceptions will propagate without being logged, making debugging difficult. Consider re-adding error handling for requests.RequestException and ValueError (for missing testcase packages) with appropriate logging.

Copilot uses AI. Check for mistakes.


def submit_selftest_to_sandbox(problem_id, language_type, source_code, stdin_data):
Expand Down