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
89 changes: 76 additions & 13 deletions mpqp/local_storage/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,59 @@
storage. In the process, they are converted to MPQP objects
(:class:`~mpqp.execution.job.Job` and :class:`~mpqp.execution.result.Result`)."""

# TODO: put DB specific errors here ?

from __future__ import annotations

from typing import Optional
from typing import Any, Optional

from mpqp import *
from mpqp.local_storage.queries import *
from mpqp.local_storage.setup import DictDB


def _build_safe_namespace() -> dict[str, Any]:
"""Build a restricted namespace containing only the MPQP symbols needed
to deserialize objects stored in the local database. This prevents
arbitrary code execution from tampered database content."""
import numpy as np
from numpy import array, complex64, float64 # noqa: F401

import mpqp.all as _all

safe_ns: dict[str, Any] = {"__builtins__": {}}
# Pull in all public symbols from mpqp.all
for name in dir(_all):
if not name.startswith("_"):
safe_ns[name] = getattr(_all, name)
# Add numpy helpers commonly found in serialized repr() strings
safe_ns.update(
{
"np": np,
"array": np.array,
"complex64": np.complex64,
"float64": np.float64,
}
)
return safe_ns


_SAFE_NAMESPACE: dict[str, Any] | None = None


def _safe_eval(expr: str) -> Any:
"""Evaluate an expression in a restricted namespace that only contains
known MPQP symbols. Raises ``ValueError`` on failure."""
global _SAFE_NAMESPACE
if _SAFE_NAMESPACE is None:
_SAFE_NAMESPACE = _build_safe_namespace()
try:
return eval(expr, _SAFE_NAMESPACE) # noqa: S307
except Exception as e:
raise ValueError(
f"Failed to safely deserialize from local storage: {e!r}\n"
f"Expression was: {expr[:200]!r}"
) from e


def jobs_local_storage_to_mpqp(jobs: Optional[list[DictDB] | DictDB]) -> list[Job]:
"""Convert a dictionary or list of dictionaries representing jobs into MPQP Job objects.

Expand All @@ -30,6 +72,7 @@ def jobs_local_storage_to_mpqp(jobs: Optional[list[DictDB] | DictDB]) -> list[Jo
"""
if jobs is None:
return []

from numpy import (
array, # pyright: ignore[reportUnusedImport]
complex64, # pyright: ignore[reportUnusedImport]
Expand All @@ -38,20 +81,32 @@ def jobs_local_storage_to_mpqp(jobs: Optional[list[DictDB] | DictDB]) -> list[Jo

jobs_mpqp = []
if isinstance(jobs, dict):
measure = (
_safe_eval(_safe_eval(jobs['measure']))
if jobs['measure'] is not None
else None
)
jobs_mpqp.append(
Job(
eval("JobType." + jobs['type']),
eval(eval(jobs['circuit'])),
eval(jobs['device']),
_safe_eval("JobType." + jobs['type']),
_safe_eval(_safe_eval(jobs['circuit'])),
_safe_eval(jobs['device']),
measure,
)
)
else:
for job in jobs:
measure = (
_safe_eval(_safe_eval(job['measure']))
if job['measure'] is not None
else None
)
jobs_mpqp.append(
Job(
eval("JobType." + job['type']),
eval(eval(job['circuit'])),
eval(job['device']),
_safe_eval("JobType." + job['type']),
_safe_eval(_safe_eval(job['circuit'])),
_safe_eval(job['device']),
measure,
)
)

Expand Down Expand Up @@ -83,28 +138,36 @@ def results_local_storage_to_mpqp(
return []
results_mpqp = []
if isinstance(results, dict):
error = eval(eval(results['error'])) if results['error'] is not None else None
error = (
_safe_eval(_safe_eval(results['error']))
if results['error'] is not None
else None
)
job = fetch_jobs_with_id(results['job_id'])
if len(job) == 0:
raise ValueError("Job not found for result, can not be instantiated.")
results_mpqp.append(
Result(
jobs_local_storage_to_mpqp(job)[0],
eval(eval(results['data'])),
_safe_eval(_safe_eval(results['data'])),
error,
results['shots'],
)
)
else:
for result in results:
error = None if result['error'] is None else eval(eval(result['error']))
error = (
None
if result['error'] is None
else _safe_eval(_safe_eval(result['error']))
)
job = fetch_jobs_with_id(result['job_id'])
if len(job) == 0:
raise ValueError("Job not found for result, can not be instantiated.")
results_mpqp.append(
Result(
jobs_local_storage_to_mpqp(job)[0],
eval(eval(result['data'])),
_safe_eval(_safe_eval(result['data'])),
error,
result['shots'],
)
Expand Down
8 changes: 5 additions & 3 deletions mpqp/local_storage/save.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def insert_jobs(jobs: Job | list[Job]) -> list[int]:
cursor.execute(
'''
INSERT INTO jobs (type, circuit, device, measure, remote_id, status)
VALUES (?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?)
''',
(
job.job_type.name,
Expand All @@ -71,14 +71,16 @@ def insert_jobs(jobs: Job | list[Job]) -> list[int]:

cursor.execute(
'''
INSERT INTO jobs (type, circuit, device, measure)
VALUES (?, ?, ?, ?)
INSERT INTO jobs (type, circuit, device, measure, remote_id, status)
VALUES (?, ?, ?, ?, ?, ?)
''',
(
jobs.job_type.name,
circuit_json,
str(jobs.device),
measure_json,
str(jobs.id),
str(jobs.status),
),
)
id = cursor.lastrowid
Expand Down
18 changes: 14 additions & 4 deletions mpqp/local_storage/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,20 @@ def wrapper(*args: Any, **kwargs: dict[str, Any]) -> T:

db_version = get_database_version()
if db_version != DATABASE_VERSION:
raise RuntimeError(f"""\
Database version {db_version} is outdated. Current supported version: {DATABASE_VERSION}.
Automated migration is not yet supported, please contact library authors to get\
help for the migration.""")
import shutil
from warnings import warn

db_path = get_env_variable("DB_PATH")
backup_path = db_path + f".v{db_version}.bak"
shutil.copy2(db_path, backup_path)
warn(
f"Database version {db_version} is outdated (current: "
f"{DATABASE_VERSION}). The old database has been backed up to "
f"'{backup_path}' and a fresh database will be created.",
stacklevel=2,
)
Path(db_path).unlink()
setup_local_storage(db_path)
Comment on lines -52 to +65
Copy link
Contributor

Choose a reason for hiding this comment

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

interesting idea, for now we never changed db version so this problem cannot occur. We might prefer leaving the error as it was and create a proper migration script once we have a new version. To be discussed


return func(*args, **kwargs)

Expand Down