Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 444dc0a6d1 |
@@ -40,14 +40,22 @@ _IMAGE_EXTENSIONS = {
|
||||
"image/png": "png",
|
||||
"image/gif": "gif",
|
||||
"image/webp": "webp",
|
||||
"image/bmp": "bmp", # #99
|
||||
"image/x-ms-bmp": "bmp", # #99
|
||||
}
|
||||
|
||||
# #99: the Opto bench cameras save BMP and the worker decodes it (PIL), so the
|
||||
# vision routes accept it on top of files.py's list. files.py is untouched: its
|
||||
# attachments are served by extension and BMP is not meant to be shown there.
|
||||
_VISION_IMAGE_TYPES = ALLOWED_IMAGE_TYPES | {"image/bmp", "image/x-ms-bmp"}
|
||||
|
||||
# Sniffed from the first bytes, independent of the declared Content-Type: a
|
||||
# text file uploaded as "image/png" passes the header check above and must
|
||||
# still be caught before it reaches the worker as a bare 500 (#15). BMP and
|
||||
# TIFF are included even though `_IMAGE_EXTENSIONS` does not accept them,
|
||||
# because PIL - and so the worker - can decode both; this check only refuses
|
||||
# what is not a real image at all, it does not narrow the allow-list above.
|
||||
# still be caught before it reaches the worker as a bare 500 (#15). TIFF is
|
||||
# included even though the allow-list does not accept it (PIL, and so the
|
||||
# worker, can decode it; browsers do not show it in the result); BMP is now
|
||||
# also allowed (#99). This check only refuses what is not a real image at all,
|
||||
# it does not narrow the allow-list above.
|
||||
_IMAGE_SIGNATURES: tuple[tuple[bytes, ...], ...] = (
|
||||
(b"\x89PNG\r\n\x1a\n",), # PNG
|
||||
(b"\xff\xd8\xff",), # JPEG
|
||||
@@ -83,7 +91,7 @@ async def _read_image(image: UploadFile) -> bytes:
|
||||
is in hand - the same two-step convention `files.py::upload_file` already
|
||||
uses, reused rather than reinvented here.
|
||||
"""
|
||||
if image.content_type not in ALLOWED_IMAGE_TYPES:
|
||||
if image.content_type not in _VISION_IMAGE_TYPES: # #99
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File type {image.content_type} not allowed. Must be an image.",
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""#99: the vision routes accept BMP (the Opto bench cameras save it); TIFF and
|
||||
non-images stay refused. files.py (attachments) is not touched by this."""
|
||||
import io
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.backend.config import settings
|
||||
from src.backend.models.orm.vision import VisionReferenceImage
|
||||
from src.backend.tests.conftest import auth_headers, create_test_recipe
|
||||
from src.backend.tests.test_vision_upload_limits import ( # noqa: F401 - fixtures
|
||||
recipe_parts, redirect_uploads, worker_says,
|
||||
)
|
||||
|
||||
|
||||
def a_bmp() -> bytes:
|
||||
buffer = io.BytesIO()
|
||||
Image.new("L", (64, 64), color=0).save(buffer, format="BMP")
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def a_tiff() -> bytes:
|
||||
buffer = io.BytesIO()
|
||||
Image.new("L", (64, 64), color=0).save(buffer, format="TIFF")
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
async def a_camera_task(db_session, user):
|
||||
recipe = await create_test_recipe(db_session, user.id)
|
||||
_, task, _ = await recipe_parts(db_session, recipe.id)
|
||||
task.task_type = "camera_measure"
|
||||
task.vision_json = {"schema_version": 1, "name": "p", "tools": []}
|
||||
await db_session.commit()
|
||||
return task
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("content_type", ["image/bmp", "image/x-ms-bmp"])
|
||||
async def test_preview_accepts_a_bmp(
|
||||
client, db_session, maker_user, worker_says, content_type,
|
||||
):
|
||||
task = await a_camera_task(db_session, maker_user)
|
||||
worker_says({"t1.diameter": 10.0})
|
||||
|
||||
response = await client.post(
|
||||
"/api/vision/preview",
|
||||
headers=auth_headers(maker_user),
|
||||
files={"image": ("p.bmp", a_bmp(), content_type)},
|
||||
data={"task_id": str(task.id)},
|
||||
)
|
||||
|
||||
assert response.status_code != 400, response.text
|
||||
assert response.status_code != 422, response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preview_still_rejects_a_tiff(
|
||||
client, db_session, maker_user, worker_says,
|
||||
):
|
||||
task = await a_camera_task(db_session, maker_user)
|
||||
worker_says({"t1.diameter": 10.0})
|
||||
|
||||
response = await client.post(
|
||||
"/api/vision/preview",
|
||||
headers=auth_headers(maker_user),
|
||||
files={"image": ("p.tif", a_tiff(), "image/tiff")},
|
||||
data={"task_id": str(task.id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_text_file_declared_bmp_is_refused_on_its_bytes(
|
||||
client, db_session, maker_user, worker_says,
|
||||
):
|
||||
task = await a_camera_task(db_session, maker_user)
|
||||
worker_says({"t1.diameter": 10.0})
|
||||
|
||||
response = await client.post(
|
||||
"/api/vision/preview",
|
||||
headers=auth_headers(maker_user),
|
||||
files={"image": ("p.bmp", b"i am not an image", "image/bmp")},
|
||||
data={"task_id": str(task.id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reference_image_bmp_is_saved_with_the_bmp_extension(
|
||||
client, db_session, maker_user,
|
||||
):
|
||||
recipe = await create_test_recipe(db_session, maker_user.id)
|
||||
_, task, _ = await recipe_parts(db_session, recipe.id)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.post(
|
||||
"/api/vision/reference-images",
|
||||
headers=auth_headers(maker_user),
|
||||
files={"image": ("p.bmp", a_bmp(), "image/x-ms-bmp")},
|
||||
data={"task_id": str(task.id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
saved = (await db_session.execute(select(VisionReferenceImage))).scalars().one()
|
||||
assert saved.path.endswith(".bmp")
|
||||
assert (settings.upload_path / saved.path).exists()
|
||||
Reference in New Issue
Block a user