Compare commits

...

26 Commits

Author SHA1 Message Date
F. Levi
b4bf73d650
Merge 7882819795 into 057fcb3993 2024-11-20 15:56:32 +02:00
Lukas Kalbertodt
057fcb3993
Add indices field to _matchesPosition to specify where in an array a match comes from (#5005)
Some checks are pending
Indexing bench (push) / Run and upload benchmarks (push) Waiting to run
Benchmarks of indexing (push) / Run and upload benchmarks (push) Waiting to run
Benchmarks of search for geo (push) / Run and upload benchmarks (push) Waiting to run
Benchmarks of search for songs (push) / Run and upload benchmarks (push) Waiting to run
Benchmarks of search for Wikipedia articles (push) / Run and upload benchmarks (push) Waiting to run
Run the indexing fuzzer / Setup the action (push) Successful in 1h4m31s
* Remove unreachable code

* Add `indices` field to `MatchBounds`

For matches inside arrays, this field holds the indices of the array
elements that matched. For example, searching for `cat` inside
`{ "a": ["dog", "cat", "fox"] }` would return `indices: [1]`. For nested
arrays, this contains multiple indices, starting with the one for the
top-most array. For matches in fields without arrays, `indices` is not
serialized (does not exist) to save space.
2024-11-20 01:00:43 +01:00
F. Levi
7882819795 Fix erroneous Sequence impl 2024-11-13 15:58:54 +02:00
F. Levi
c2b14b8f8f Fix erroneous Sequence impl 2024-11-13 15:08:55 +02:00
F. Levi
ccd79b07f7 Fix test snapshots 2024-11-13 11:13:27 +02:00
F. Levi
146c87bd6f Make Sequence impl work without changing dumps 2024-11-13 10:59:42 +02:00
F. Levi
7e45125e7a Merge branch 'main' into key-actions-to-bitflags 2024-11-06 11:19:05 +02:00
F. Levi
46fc6bbc2e Merge branch 'main' into key-actions-to-bitflags 2024-10-31 15:31:20 +02:00
F. Levi
ba045a0000 Adapt tests 2024-10-21 11:43:58 +03:00
F. Levi
b89fd49d63 Fix some 'all' flags making problems 2024-10-21 11:12:47 +03:00
F. Levi
544961372c Attempt pushing bad code in order to stop stuck GitHub action 2024-10-21 00:19:33 +03:00
F. Levi
0419b93032 Try fix Sequence impl, refactor, improve 2024-10-20 21:46:39 +03:00
F. Levi
e4745a61a8 Resolve clippy error temporarily 2024-10-20 15:37:13 +03:00
F. Levi
04d86a4d9e Adapt code to new flags structure 2024-10-20 10:18:09 +03:00
F. Levi
e9668eff79 Misc 2024-10-05 13:40:38 +03:00
F. Levi
0403ec0a56 Fix All flag 2024-10-04 14:33:35 +03:00
F. Levi
56b5289b2f Make Actions usable as bitflags 2024-10-04 14:26:49 +03:00
F. Levi
4b8dd6dd6b Merge branch 'main' into key-actions-to-bitflags 2024-10-04 11:45:58 +03:00
F. Levi
3d3ae5aa0c Refactoring 2024-09-18 16:01:53 +03:00
F. Levi
a80bb8f77e Misc 2024-09-18 15:40:17 +03:00
F. Levi
f95bd11db2 Improve deserr error for unknown values 2024-09-18 10:46:05 +03:00
F. Levi
e54fbb0d1e Refactor 2024-09-18 09:54:15 +03:00
F. Levi
b6c5a57932 Add Sequence impl, other changes/adjustments 2024-09-17 23:10:59 +03:00
F. Levi
6688604a93 Add Serialize impl 2024-09-17 12:55:45 +03:00
F. Levi
75969f9f68 Add manual serde Deserialize impl 2024-09-16 21:14:47 +03:00
F. Levi
07e7ddbc5b Change Actions enum to bitflags 2024-09-09 12:14:19 +03:00
12 changed files with 445 additions and 315 deletions

1
Cargo.lock generated
View File

@ -3507,6 +3507,7 @@ version = "1.11.0"
dependencies = [
"actix-web",
"anyhow",
"bitflags 2.6.0",
"convert_case 0.6.0",
"csv",
"deserr",

View File

@ -105,7 +105,7 @@ impl HeedAuthStore {
let mut actions = HashSet::new();
for action in &key.actions {
match action {
match *action {
Action::All => actions.extend(enum_iterator::all::<Action>()),
Action::DocumentsAll => {
actions.extend(
@ -128,23 +128,11 @@ impl HeedAuthStore {
Action::SettingsAll => {
actions.extend([Action::SettingsGet, Action::SettingsUpdate].iter());
}
Action::DumpsAll => {
actions.insert(Action::DumpsCreate);
}
Action::SnapshotsAll => {
actions.insert(Action::SnapshotsCreate);
}
Action::TasksAll => {
actions.extend([Action::TasksGet, Action::TasksDelete, Action::TasksCancel]);
}
Action::StatsAll => {
actions.insert(Action::StatsGet);
}
Action::MetricsAll => {
actions.insert(Action::MetricsGet);
}
other => {
actions.insert(*other);
actions.insert(other);
}
}
}
@ -293,18 +281,24 @@ impl HeedAuthStore {
/// optionally on a specific index, for a given key.
pub struct KeyIdActionCodec;
impl KeyIdActionCodec {
fn action_parts_to_32bits([p1, p2, p3, p4]: &[u8; 4]) -> u32 {
((*p1 as u32) << 24) | ((*p2 as u32) << 16) | ((*p3 as u32) << 8) | (*p4 as u32)
}
}
impl<'a> milli::heed::BytesDecode<'a> for KeyIdActionCodec {
type DItem = (KeyId, Action, Option<&'a [u8]>);
fn bytes_decode(bytes: &'a [u8]) -> StdResult<Self::DItem, BoxedError> {
let (key_id_bytes, action_bytes) = try_split_array_at(bytes).ok_or(SliceTooShortError)?;
let (&action_byte, index) =
match try_split_array_at(action_bytes).ok_or(SliceTooShortError)? {
([action], []) => (action, None),
([action], index) => (action, Some(index)),
let (action_bits, index) =
match try_split_array_at::<u8, 4>(action_bytes).ok_or(SliceTooShortError)? {
(action_parts, []) => (Self::action_parts_to_32bits(action_parts), None),
(action_parts, index) => (Self::action_parts_to_32bits(action_parts), Some(index)),
};
let key_id = Uuid::from_bytes(*key_id_bytes);
let action = Action::from_repr(action_byte).ok_or(InvalidActionError { action_byte })?;
let action = Action::from_bits(action_bits).ok_or(InvalidActionError { action_bits })?;
Ok((key_id, action, index))
}
@ -317,7 +311,7 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec {
let mut bytes = Vec::new();
bytes.extend_from_slice(key_id.as_bytes());
let action_bytes = u8::to_be_bytes(action.repr());
let action_bytes = u32::to_be_bytes(action.bits());
bytes.extend_from_slice(&action_bytes);
if let Some(index) = index {
bytes.extend_from_slice(index);
@ -332,9 +326,9 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec {
pub struct SliceTooShortError;
#[derive(Error, Debug)]
#[error("cannot construct a valid Action from {action_byte}")]
#[error("cannot construct a valid Action from {action_bits}")]
pub struct InvalidActionError {
pub action_byte: u8,
pub action_bits: u32,
}
pub fn generate_key_as_hexa(uid: Uuid, master_key: &[u8]) -> String {

View File

@ -38,6 +38,7 @@ time = { version = "0.3.36", features = [
] }
tokio = "1.38"
uuid = { version = "1.10.0", features = ["serde", "v4"] }
bitflags = "2.6.0"
[dev-dependencies]
insta = "1.39.0"

View File

@ -2,10 +2,11 @@ use std::convert::Infallible;
use std::hash::Hash;
use std::str::FromStr;
use deserr::{DeserializeError, Deserr, MergeWithError, ValuePointerRef};
use bitflags::{bitflags, Flags};
use deserr::{take_cf_content, DeserializeError, Deserr, MergeWithError, ValuePointerRef};
use enum_iterator::Sequence;
use milli::update::Setting;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use time::format_description::well_known::Rfc3339;
use time::macros::{format_description, time};
use time::{Date, OffsetDateTime, PrimitiveDateTime};
@ -179,193 +180,284 @@ fn parse_expiration_date(
}
}
#[derive(Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence, Deserr)]
#[repr(u8)]
pub enum Action {
#[serde(rename = "*")]
#[deserr(rename = "*")]
All = 0,
#[serde(rename = "search")]
#[deserr(rename = "search")]
Search,
#[serde(rename = "documents.*")]
#[deserr(rename = "documents.*")]
DocumentsAll,
#[serde(rename = "documents.add")]
#[deserr(rename = "documents.add")]
DocumentsAdd,
#[serde(rename = "documents.get")]
#[deserr(rename = "documents.get")]
DocumentsGet,
#[serde(rename = "documents.delete")]
#[deserr(rename = "documents.delete")]
DocumentsDelete,
#[serde(rename = "indexes.*")]
#[deserr(rename = "indexes.*")]
IndexesAll,
#[serde(rename = "indexes.create")]
#[deserr(rename = "indexes.create")]
IndexesAdd,
#[serde(rename = "indexes.get")]
#[deserr(rename = "indexes.get")]
IndexesGet,
#[serde(rename = "indexes.update")]
#[deserr(rename = "indexes.update")]
IndexesUpdate,
#[serde(rename = "indexes.delete")]
#[deserr(rename = "indexes.delete")]
IndexesDelete,
#[serde(rename = "indexes.swap")]
#[deserr(rename = "indexes.swap")]
IndexesSwap,
#[serde(rename = "tasks.*")]
#[deserr(rename = "tasks.*")]
TasksAll,
#[serde(rename = "tasks.cancel")]
#[deserr(rename = "tasks.cancel")]
TasksCancel,
#[serde(rename = "tasks.delete")]
#[deserr(rename = "tasks.delete")]
TasksDelete,
#[serde(rename = "tasks.get")]
#[deserr(rename = "tasks.get")]
TasksGet,
#[serde(rename = "settings.*")]
#[deserr(rename = "settings.*")]
SettingsAll,
#[serde(rename = "settings.get")]
#[deserr(rename = "settings.get")]
SettingsGet,
#[serde(rename = "settings.update")]
#[deserr(rename = "settings.update")]
SettingsUpdate,
#[serde(rename = "stats.*")]
#[deserr(rename = "stats.*")]
StatsAll,
#[serde(rename = "stats.get")]
#[deserr(rename = "stats.get")]
StatsGet,
#[serde(rename = "metrics.*")]
#[deserr(rename = "metrics.*")]
MetricsAll,
#[serde(rename = "metrics.get")]
#[deserr(rename = "metrics.get")]
MetricsGet,
#[serde(rename = "dumps.*")]
#[deserr(rename = "dumps.*")]
DumpsAll,
#[serde(rename = "dumps.create")]
#[deserr(rename = "dumps.create")]
DumpsCreate,
#[serde(rename = "snapshots.*")]
#[deserr(rename = "snapshots.*")]
SnapshotsAll,
#[serde(rename = "snapshots.create")]
#[deserr(rename = "snapshots.create")]
SnapshotsCreate,
#[serde(rename = "version")]
#[deserr(rename = "version")]
Version,
#[serde(rename = "keys.create")]
#[deserr(rename = "keys.create")]
KeysAdd,
#[serde(rename = "keys.get")]
#[deserr(rename = "keys.get")]
KeysGet,
#[serde(rename = "keys.update")]
#[deserr(rename = "keys.update")]
KeysUpdate,
#[serde(rename = "keys.delete")]
#[deserr(rename = "keys.delete")]
KeysDelete,
#[serde(rename = "experimental.get")]
#[deserr(rename = "experimental.get")]
ExperimentalFeaturesGet,
#[serde(rename = "experimental.update")]
#[deserr(rename = "experimental.update")]
ExperimentalFeaturesUpdate,
bitflags! {
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
#[repr(transparent)]
// NOTE: For `Sequence` impl to work, the values of these must be in ascending order
pub struct Action: u32 {
const Search = 1;
// Documents
const DocumentsAdd = 1 << 1;
const DocumentsGet = 1 << 2;
const DocumentsDelete = 1 << 3;
const DocumentsAll = Self::DocumentsAdd.bits() | Self::DocumentsGet.bits() | Self::DocumentsDelete.bits();
// Indexes
const IndexesAdd = 1 << 4;
const IndexesGet = 1 << 5;
const IndexesUpdate = 1 << 6;
const IndexesDelete = 1 << 7;
const IndexesSwap = 1 << 8;
const IndexesAll = Self::IndexesAdd.bits() | Self::IndexesGet.bits() | Self::IndexesUpdate.bits() | Self::IndexesDelete.bits() | Self::IndexesSwap.bits();
// Tasks
const TasksCancel = 1 << 9;
const TasksDelete = 1 << 10;
const TasksGet = 1 << 11;
const TasksAll = Self::TasksCancel.bits() | Self::TasksDelete.bits() | Self::TasksGet.bits();
// Settings
const SettingsGet = 1 << 12;
const SettingsUpdate = 1 << 13;
const SettingsAll = Self::SettingsGet.bits() | Self::SettingsUpdate.bits();
// Stats
const StatsGet = 1 << 14;
const StatsAll = Self::StatsGet.bits();
// Metrics
const MetricsGet = 1 << 15;
const MetricsAll = Self::MetricsGet.bits();
// Dumps
const DumpsCreate = 1 << 16;
const DumpsAll = Self::DumpsCreate.bits();
// Snapshots
const SnapshotsCreate = 1 << 17;
const SnapshotsAll = Self::SnapshotsCreate.bits();
// Keys without an "all" version
const Version = 1 << 18;
const KeysAdd = 1 << 19;
const KeysGet = 1 << 20;
const KeysUpdate = 1 << 21;
const KeysDelete = 1 << 22;
const ExperimentalFeaturesGet = 1 << 23;
const ExperimentalFeaturesUpdate = 1 << 24;
// All
const All = 0xFFFFFFFF >> (32 - 1 - 24);
}
}
impl Action {
pub const fn from_repr(repr: u8) -> Option<Self> {
use actions::*;
match repr {
ALL => Some(Self::All),
SEARCH => Some(Self::Search),
DOCUMENTS_ALL => Some(Self::DocumentsAll),
DOCUMENTS_ADD => Some(Self::DocumentsAdd),
DOCUMENTS_GET => Some(Self::DocumentsGet),
DOCUMENTS_DELETE => Some(Self::DocumentsDelete),
INDEXES_ALL => Some(Self::IndexesAll),
INDEXES_CREATE => Some(Self::IndexesAdd),
INDEXES_GET => Some(Self::IndexesGet),
INDEXES_UPDATE => Some(Self::IndexesUpdate),
INDEXES_DELETE => Some(Self::IndexesDelete),
INDEXES_SWAP => Some(Self::IndexesSwap),
TASKS_ALL => Some(Self::TasksAll),
TASKS_CANCEL => Some(Self::TasksCancel),
TASKS_DELETE => Some(Self::TasksDelete),
TASKS_GET => Some(Self::TasksGet),
SETTINGS_ALL => Some(Self::SettingsAll),
SETTINGS_GET => Some(Self::SettingsGet),
SETTINGS_UPDATE => Some(Self::SettingsUpdate),
STATS_ALL => Some(Self::StatsAll),
STATS_GET => Some(Self::StatsGet),
METRICS_ALL => Some(Self::MetricsAll),
METRICS_GET => Some(Self::MetricsGet),
DUMPS_ALL => Some(Self::DumpsAll),
DUMPS_CREATE => Some(Self::DumpsCreate),
SNAPSHOTS_CREATE => Some(Self::SnapshotsCreate),
VERSION => Some(Self::Version),
KEYS_CREATE => Some(Self::KeysAdd),
KEYS_GET => Some(Self::KeysGet),
KEYS_UPDATE => Some(Self::KeysUpdate),
KEYS_DELETE => Some(Self::KeysDelete),
EXPERIMENTAL_FEATURES_GET => Some(Self::ExperimentalFeaturesGet),
EXPERIMENTAL_FEATURES_UPDATE => Some(Self::ExperimentalFeaturesUpdate),
_otherwise => None,
}
const SERDE_MAP_ARR: [(&'static str, Self); 34] = [
("search", Self::Search),
("documents.add", Self::DocumentsAdd),
("documents.get", Self::DocumentsGet),
("documents.delete", Self::DocumentsDelete),
("documents.*", Self::DocumentsAll),
("indexes.create", Self::IndexesAdd),
("indexes.get", Self::IndexesGet),
("indexes.update", Self::IndexesUpdate),
("indexes.delete", Self::IndexesDelete),
("indexes.swap", Self::IndexesSwap),
("indexes.*", Self::IndexesAll),
("tasks.cancel", Self::TasksCancel),
("tasks.delete", Self::TasksDelete),
("tasks.get", Self::TasksGet),
("tasks.*", Self::TasksAll),
("settings.get", Self::SettingsGet),
("settings.update", Self::SettingsUpdate),
("settings.*", Self::SettingsAll),
("stats.get", Self::StatsGet),
("stats.*", Self::StatsAll),
("metrics.get", Self::MetricsGet),
("metrics.*", Self::MetricsAll),
("dumps.create", Self::DumpsCreate),
("dumps.*", Self::DumpsAll),
("snapshots.create", Self::SnapshotsCreate),
("snapshots.*", Self::SnapshotsAll),
("version", Self::Version),
("keys.create", Self::KeysAdd),
("keys.get", Self::KeysGet),
("keys.update", Self::KeysUpdate),
("keys.delete", Self::KeysDelete),
("experimental.get", Self::ExperimentalFeaturesGet),
("experimental.update", Self::ExperimentalFeaturesUpdate),
("*", Self::All),
];
fn get_action(v: &str) -> Option<Action> {
Self::SERDE_MAP_ARR
.iter()
.find(|(serde_name, _)| &v == serde_name)
.map(|(_, action)| *action)
}
pub const fn repr(&self) -> u8 {
*self as u8
fn get_action_serde_name(v: &Action) -> &'static str {
Self::SERDE_MAP_ARR
.iter()
.find(|(_, action)| v == action)
.map(|(serde_name, _)| serde_name)
.expect("an action is missing a matching serialized value")
}
// when we remove "all" flags, this will give us the exact index
fn get_potential_index(&self) -> usize {
if self.is_empty() {
return 0;
}
// most significant bit for u32
let msb = 1u32 << (31 - self.bits().leading_zeros());
// index of the single set bit
msb.trailing_zeros() as usize
}
}
pub mod actions {
use super::Action::*;
use super::Action as A;
pub(crate) const ALL: u8 = All.repr();
pub const SEARCH: u8 = Search.repr();
pub const DOCUMENTS_ALL: u8 = DocumentsAll.repr();
pub const DOCUMENTS_ADD: u8 = DocumentsAdd.repr();
pub const DOCUMENTS_GET: u8 = DocumentsGet.repr();
pub const DOCUMENTS_DELETE: u8 = DocumentsDelete.repr();
pub const INDEXES_ALL: u8 = IndexesAll.repr();
pub const INDEXES_CREATE: u8 = IndexesAdd.repr();
pub const INDEXES_GET: u8 = IndexesGet.repr();
pub const INDEXES_UPDATE: u8 = IndexesUpdate.repr();
pub const INDEXES_DELETE: u8 = IndexesDelete.repr();
pub const INDEXES_SWAP: u8 = IndexesSwap.repr();
pub const TASKS_ALL: u8 = TasksAll.repr();
pub const TASKS_CANCEL: u8 = TasksCancel.repr();
pub const TASKS_DELETE: u8 = TasksDelete.repr();
pub const TASKS_GET: u8 = TasksGet.repr();
pub const SETTINGS_ALL: u8 = SettingsAll.repr();
pub const SETTINGS_GET: u8 = SettingsGet.repr();
pub const SETTINGS_UPDATE: u8 = SettingsUpdate.repr();
pub const STATS_ALL: u8 = StatsAll.repr();
pub const STATS_GET: u8 = StatsGet.repr();
pub const METRICS_ALL: u8 = MetricsAll.repr();
pub const METRICS_GET: u8 = MetricsGet.repr();
pub const DUMPS_ALL: u8 = DumpsAll.repr();
pub const DUMPS_CREATE: u8 = DumpsCreate.repr();
pub const SNAPSHOTS_CREATE: u8 = SnapshotsCreate.repr();
pub const VERSION: u8 = Version.repr();
pub const KEYS_CREATE: u8 = KeysAdd.repr();
pub const KEYS_GET: u8 = KeysGet.repr();
pub const KEYS_UPDATE: u8 = KeysUpdate.repr();
pub const KEYS_DELETE: u8 = KeysDelete.repr();
pub const EXPERIMENTAL_FEATURES_GET: u8 = ExperimentalFeaturesGet.repr();
pub const EXPERIMENTAL_FEATURES_UPDATE: u8 = ExperimentalFeaturesUpdate.repr();
pub const SEARCH: u32 = A::Search.bits();
pub const DOCUMENTS_ADD: u32 = A::DocumentsAdd.bits();
pub const DOCUMENTS_GET: u32 = A::DocumentsGet.bits();
pub const DOCUMENTS_DELETE: u32 = A::DocumentsDelete.bits();
pub const DOCUMENTS_ALL: u32 = A::DocumentsAll.bits();
pub const INDEXES_CREATE: u32 = A::IndexesAdd.bits();
pub const INDEXES_GET: u32 = A::IndexesGet.bits();
pub const INDEXES_UPDATE: u32 = A::IndexesUpdate.bits();
pub const INDEXES_DELETE: u32 = A::IndexesDelete.bits();
pub const INDEXES_SWAP: u32 = A::IndexesSwap.bits();
pub const INDEXES_ALL: u32 = A::IndexesAll.bits();
pub const TASKS_CANCEL: u32 = A::TasksCancel.bits();
pub const TASKS_DELETE: u32 = A::TasksDelete.bits();
pub const TASKS_GET: u32 = A::TasksGet.bits();
pub const TASKS_ALL: u32 = A::TasksAll.bits();
pub const SETTINGS_GET: u32 = A::SettingsGet.bits();
pub const SETTINGS_UPDATE: u32 = A::SettingsUpdate.bits();
pub const SETTINGS_ALL: u32 = A::SettingsAll.bits();
pub const STATS_GET: u32 = A::StatsGet.bits();
pub const STATS_ALL: u32 = A::StatsAll.bits();
pub const METRICS_GET: u32 = A::MetricsGet.bits();
pub const METRICS_ALL: u32 = A::MetricsAll.bits();
pub const DUMPS_CREATE: u32 = A::DumpsCreate.bits();
pub const DUMPS_ALL: u32 = A::DumpsAll.bits();
pub const SNAPSHOTS_CREATE: u32 = A::SnapshotsCreate.bits();
pub const SNAPSHOTS_ALL: u32 = A::SnapshotsAll.bits();
pub const VERSION: u32 = A::Version.bits();
pub const KEYS_CREATE: u32 = A::KeysAdd.bits();
pub const KEYS_GET: u32 = A::KeysGet.bits();
pub const KEYS_UPDATE: u32 = A::KeysUpdate.bits();
pub const KEYS_DELETE: u32 = A::KeysDelete.bits();
pub const EXPERIMENTAL_FEATURES_GET: u32 = A::ExperimentalFeaturesGet.bits();
pub const EXPERIMENTAL_FEATURES_UPDATE: u32 = A::ExperimentalFeaturesUpdate.bits();
pub const ALL: u32 = A::All.bits();
}
impl<E: DeserializeError> Deserr<E> for Action {
fn deserialize_from_value<V: deserr::IntoValue>(
value: deserr::Value<V>,
location: deserr::ValuePointerRef<'_>,
) -> Result<Self, E> {
match value {
deserr::Value::String(s) => match Self::get_action(&s) {
Some(action) => Ok(action),
None => Err(deserr::take_cf_content(E::error::<std::convert::Infallible>(
None,
deserr::ErrorKind::UnknownValue {
value: &s,
accepted: &Self::SERDE_MAP_ARR.map(|(ser_action, _)| ser_action),
},
location,
))),
},
_ => Err(take_cf_content(E::error(
None,
deserr::ErrorKind::IncorrectValueKind {
actual: value,
accepted: &[deserr::ValueKind::String],
},
location,
))),
}
}
}
impl Serialize for Action {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(Self::get_action_serde_name(self))
}
}
impl<'de> Deserialize<'de> for Action {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = Action;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "the name of a valid action (string)")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match Self::Value::get_action(s) {
Some(action) => Ok(action),
None => Err(E::invalid_value(serde::de::Unexpected::Str(s), &"a valid action")),
}
}
}
deserializer.deserialize_str(Visitor)
}
}
// TODO: Once "all" type flags are removed, simplify
// Essentially `get_potential_index` will give the exact index, +1 the exact next, -1 the exact previous
impl Sequence for Action {
const CARDINALITY: usize = Self::FLAGS.len();
fn next(&self) -> Option<Self> {
let mut potential_next_index = self.get_potential_index() + 1;
loop {
if let Some(next_flag) = Self::FLAGS.get(potential_next_index).map(|v| v.value()) {
if next_flag > self {
return Some(*next_flag);
}
potential_next_index += 1;
} else {
return None;
}
}
}
fn previous(&self) -> Option<Self> {
// -2 because of "all" type flags that represent a single flag, otherwise -1 would suffice
let initial_potential_index = self.get_potential_index();
if initial_potential_index == 0 {
return None;
}
let mut potential_previous_index: usize =
if initial_potential_index == 1 { 0 } else { initial_potential_index - 2 };
let mut previous_item: Option<Self> = None;
let mut pre_previous_item: Option<Self> = None;
loop {
if let Some(next_flag) = Self::FLAGS.get(potential_previous_index).map(|v| v.value()) {
if next_flag > self {
return pre_previous_item;
}
pre_previous_item = previous_item;
previous_item = Some(*next_flag);
potential_previous_index += 1;
} else {
return pre_previous_item;
}
}
}
fn first() -> Option<Self> {
Self::FLAGS.first().map(|v| *v.value())
}
fn last() -> Option<Self> {
Self::FLAGS.last().map(|v| *v.value())
}
}

View File

@ -171,7 +171,7 @@ pub mod policies {
#[error("Could not decode tenant token, {0}.")]
CouldNotDecodeTenantToken(jsonwebtoken::errors::Error),
#[error("Invalid action `{0}`.")]
InternalInvalidAction(u8),
InternalInvalidAction(u32),
}
impl From<jsonwebtoken::errors::Error> for AuthError {
@ -214,14 +214,14 @@ pub mod policies {
Ok(api_key_uid)
}
fn is_keys_action(action: u8) -> bool {
fn is_keys_action(action: u32) -> bool {
use actions::*;
matches!(action, KEYS_GET | KEYS_CREATE | KEYS_UPDATE | KEYS_DELETE)
}
pub struct ActionPolicy<const A: u8>;
pub struct ActionPolicy<const A: u32>;
impl<const A: u8> Policy for ActionPolicy<A> {
impl<const A: u32> Policy for ActionPolicy<A> {
/// Attempts to grant authentication from a bearer token (that can be a tenant token or an API key), the requested Action,
/// and a list of requested indexes.
///
@ -255,7 +255,7 @@ pub mod policies {
};
// check that the indexes are allowed
let action = Action::from_repr(A).ok_or(AuthError::InternalInvalidAction(A))?;
let action = Action::from_bits(A).ok_or(AuthError::InternalInvalidAction(A))?;
let auth_filter = auth
.get_key_filters(key_uuid, search_rules)
.map_err(|_e| AuthError::InvalidApiKey)?;
@ -294,7 +294,7 @@ pub mod policies {
}
}
impl<const A: u8> ActionPolicy<A> {
impl<const A: u32> ActionPolicy<A> {
fn authenticate_tenant_token(
auth: &AuthController,
token: &str,

View File

@ -1733,46 +1733,51 @@ fn format_fields(
// select the attributes to retrieve
let displayable_names =
displayable_ids.iter().map(|&fid| field_ids_map.name(fid).expect("Missing field name"));
permissive_json_pointer::map_leaf_values(&mut document, displayable_names, |key, value| {
// To get the formatting option of each key we need to see all the rules that applies
// to the value and merge them together. eg. If a user said he wanted to highlight `doggo`
// and crop `doggo.name`. `doggo.name` needs to be highlighted + cropped while `doggo.age` is only
// highlighted.
// Warn: The time to compute the format list scales with the number of fields to format;
// cumulated with map_leaf_values that iterates over all the nested fields, it gives a quadratic complexity:
// d*f where d is the total number of fields to display and f is the total number of fields to format.
let format = formatting_fields_options
.iter()
.filter(|(name, _option)| {
milli::is_faceted_by(name, key) || milli::is_faceted_by(key, name)
})
.map(|(_, option)| **option)
.reduce(|acc, option| acc.merge(option));
let mut infos = Vec::new();
// if no locales has been provided, we try to find the locales in the localized_attributes.
let locales = locales.or_else(|| {
localized_attributes
permissive_json_pointer::map_leaf_values(
&mut document,
displayable_names,
|key, array_indices, value| {
// To get the formatting option of each key we need to see all the rules that applies
// to the value and merge them together. eg. If a user said he wanted to highlight `doggo`
// and crop `doggo.name`. `doggo.name` needs to be highlighted + cropped while `doggo.age` is only
// highlighted.
// Warn: The time to compute the format list scales with the number of fields to format;
// cumulated with map_leaf_values that iterates over all the nested fields, it gives a quadratic complexity:
// d*f where d is the total number of fields to display and f is the total number of fields to format.
let format = formatting_fields_options
.iter()
.find(|rule| rule.match_str(key))
.map(LocalizedAttributesRule::locales)
});
.filter(|(name, _option)| {
milli::is_faceted_by(name, key) || milli::is_faceted_by(key, name)
})
.map(|(_, option)| **option)
.reduce(|acc, option| acc.merge(option));
let mut infos = Vec::new();
*value = format_value(
std::mem::take(value),
builder,
format,
&mut infos,
compute_matches,
locales,
);
// if no locales has been provided, we try to find the locales in the localized_attributes.
let locales = locales.or_else(|| {
localized_attributes
.iter()
.find(|rule| rule.match_str(key))
.map(LocalizedAttributesRule::locales)
});
if let Some(matches) = matches_position.as_mut() {
if !infos.is_empty() {
matches.insert(key.to_owned(), infos);
*value = format_value(
std::mem::take(value),
builder,
format,
&mut infos,
compute_matches,
array_indices,
locales,
);
if let Some(matches) = matches_position.as_mut() {
if !infos.is_empty() {
matches.insert(key.to_owned(), infos);
}
}
}
});
},
);
let selectors = formatted_options
.keys()
@ -1790,13 +1795,14 @@ fn format_value(
format_options: Option<FormatOptions>,
infos: &mut Vec<MatchBounds>,
compute_matches: bool,
array_indices: &[usize],
locales: Option<&[Language]>,
) -> Value {
match value {
Value::String(old_string) => {
let mut matcher = builder.build(&old_string, locales);
if compute_matches {
let matches = matcher.matches();
let matches = matcher.matches(array_indices);
infos.extend_from_slice(&matches[..]);
}
@ -1808,51 +1814,15 @@ fn format_value(
None => Value::String(old_string),
}
}
Value::Array(values) => Value::Array(
values
.into_iter()
.map(|v| {
format_value(
v,
builder,
format_options.map(|format_options| FormatOptions {
highlight: format_options.highlight,
crop: None,
}),
infos,
compute_matches,
locales,
)
})
.collect(),
),
Value::Object(object) => Value::Object(
object
.into_iter()
.map(|(k, v)| {
(
k,
format_value(
v,
builder,
format_options.map(|format_options| FormatOptions {
highlight: format_options.highlight,
crop: None,
}),
infos,
compute_matches,
locales,
),
)
})
.collect(),
),
// `map_leaf_values` makes sure this is only called for leaf fields
Value::Array(_) => unreachable!(),
Value::Object(_) => unreachable!(),
Value::Number(number) => {
let s = number.to_string();
let mut matcher = builder.build(&s, locales);
if compute_matches {
let matches = matcher.matches();
let matches = matcher.matches(array_indices);
infos.extend_from_slice(&matches[..]);
}

View File

@ -421,7 +421,7 @@ async fn error_add_api_key_invalid_parameters_actions() {
meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{
"message": "Unknown value `doc.add` at `.actions[0]`: expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `snapshots.*`, `snapshots.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`",
"message": "Unknown value `doc.add` at `.actions[0]`: expected one of `search`, `documents.add`, `documents.get`, `documents.delete`, `documents.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `indexes.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `tasks.*`, `settings.get`, `settings.update`, `settings.*`, `stats.get`, `stats.*`, `metrics.get`, `metrics.*`, `dumps.create`, `dumps.*`, `snapshots.create`, `snapshots.*`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`, `*`",
"code": "invalid_api_key_actions",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"

View File

@ -53,14 +53,14 @@ pub static AUTHORIZATIONS: Lazy<HashMap<(&'static str, &'static str), HashSet<&'
("PUT", "/indexes/products/settings/sortable-attributes") => hashset!{"settings.update", "settings.*", "*"},
("PUT", "/indexes/products/settings/stop-words") => hashset!{"settings.update", "settings.*", "*"},
("PUT", "/indexes/products/settings/synonyms") => hashset!{"settings.update", "settings.*", "*"},
("GET", "/indexes/products/stats") => hashset!{"stats.get", "stats.*", "*"},
("GET", "/stats") => hashset!{"stats.get", "stats.*", "*"},
("POST", "/dumps") => hashset!{"dumps.create", "dumps.*", "*"},
("POST", "/snapshots") => hashset!{"snapshots.create", "snapshots.*", "*"},
("GET", "/indexes/products/stats") => hashset!{"stats.get", "*"},
("GET", "/stats") => hashset!{"stats.get", "*"},
("POST", "/dumps") => hashset!{"dumps.create", "*"},
("POST", "/snapshots") => hashset!{"snapshots.create", "*"},
("GET", "/version") => hashset!{"version", "*"},
("GET", "/metrics") => hashset!{"metrics.get", "metrics.*", "*"},
("POST", "/logs/stream") => hashset!{"metrics.get", "metrics.*", "*"},
("DELETE", "/logs/stream") => hashset!{"metrics.get", "metrics.*", "*"},
("GET", "/metrics") => hashset!{"metrics.get", "*"},
("POST", "/logs/stream") => hashset!{"metrics.get", "*"},
("DELETE", "/logs/stream") => hashset!{"metrics.get", "*"},
("PATCH", "/keys/mykey/") => hashset!{"keys.update", "*"},
("GET", "/keys/mykey/") => hashset!{"keys.get", "*"},
("DELETE", "/keys/mykey/") => hashset!{"keys.delete", "*"},

View File

@ -93,7 +93,7 @@ async fn create_api_key_bad_actions() {
snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###"
{
"message": "Unknown value `doggo` at `.actions[0]`: expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `snapshots.*`, `snapshots.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`",
"message": "Unknown value `doggo` at `.actions[0]`: expected one of `search`, `documents.add`, `documents.get`, `documents.delete`, `documents.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `indexes.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `tasks.*`, `settings.get`, `settings.update`, `settings.*`, `stats.get`, `stats.*`, `metrics.get`, `metrics.*`, `dumps.create`, `dumps.*`, `snapshots.create`, `snapshots.*`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`, `*`",
"code": "invalid_api_key_actions",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"

View File

@ -208,7 +208,10 @@ async fn format_nested() {
"doggos.name": [
{
"start": 0,
"length": 5
"length": 5,
"indices": [
0
]
}
]
}

View File

@ -105,6 +105,8 @@ impl FormatOptions {
pub struct MatchBounds {
pub start: usize,
pub length: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub indices: Option<Vec<usize>>,
}
/// Structure used to analyze a string, compute words that match,
@ -220,15 +222,20 @@ impl<'t, 'tokenizer> Matcher<'t, 'tokenizer, '_, '_> {
}
/// Returns boundaries of the words that match the query.
pub fn matches(&mut self) -> Vec<MatchBounds> {
pub fn matches(&mut self, array_indices: &[usize]) -> Vec<MatchBounds> {
match &self.matches {
None => self.compute_matches().matches(),
None => self.compute_matches().matches(array_indices),
Some((tokens, matches)) => matches
.iter()
.map(|m| MatchBounds {
start: tokens[m.get_first_token_pos()].byte_start,
// TODO: Why is this in chars, while start is in bytes?
length: m.char_count,
indices: if array_indices.is_empty() {
None
} else {
Some(array_indices.to_owned())
},
})
.collect(),
}

View File

@ -45,7 +45,7 @@ fn contained_in(selector: &str, key: &str) -> bool {
/// map_leaf_values(
/// value.as_object_mut().unwrap(),
/// ["jean.race.name"],
/// |key, value| match (value, key) {
/// |key, _array_indices, value| match (value, key) {
/// (Value::String(name), "jean.race.name") => *name = "patou".to_string(),
/// _ => unreachable!(),
/// },
@ -66,17 +66,18 @@ fn contained_in(selector: &str, key: &str) -> bool {
pub fn map_leaf_values<'a>(
value: &mut Map<String, Value>,
selectors: impl IntoIterator<Item = &'a str>,
mut mapper: impl FnMut(&str, &mut Value),
mut mapper: impl FnMut(&str, &[usize], &mut Value),
) {
let selectors: Vec<_> = selectors.into_iter().collect();
map_leaf_values_in_object(value, &selectors, "", &mut mapper);
map_leaf_values_in_object(value, &selectors, "", &[], &mut mapper);
}
pub fn map_leaf_values_in_object(
value: &mut Map<String, Value>,
selectors: &[&str],
base_key: &str,
mapper: &mut impl FnMut(&str, &mut Value),
array_indices: &[usize],
mapper: &mut impl FnMut(&str, &[usize], &mut Value),
) {
for (key, value) in value.iter_mut() {
let base_key = if base_key.is_empty() {
@ -94,12 +95,12 @@ pub fn map_leaf_values_in_object(
if should_continue {
match value {
Value::Object(object) => {
map_leaf_values_in_object(object, selectors, &base_key, mapper)
map_leaf_values_in_object(object, selectors, &base_key, array_indices, mapper)
}
Value::Array(array) => {
map_leaf_values_in_array(array, selectors, &base_key, mapper)
map_leaf_values_in_array(array, selectors, &base_key, array_indices, mapper)
}
value => mapper(&base_key, value),
value => mapper(&base_key, array_indices, value),
}
}
}
@ -109,13 +110,24 @@ pub fn map_leaf_values_in_array(
values: &mut [Value],
selectors: &[&str],
base_key: &str,
mapper: &mut impl FnMut(&str, &mut Value),
base_array_indices: &[usize],
mapper: &mut impl FnMut(&str, &[usize], &mut Value),
) {
for value in values.iter_mut() {
// This avoids allocating twice
let mut array_indices = Vec::with_capacity(base_array_indices.len() + 1);
array_indices.extend_from_slice(base_array_indices);
array_indices.push(0);
for (i, value) in values.iter_mut().enumerate() {
*array_indices.last_mut().unwrap() = i;
match value {
Value::Object(object) => map_leaf_values_in_object(object, selectors, base_key, mapper),
Value::Array(array) => map_leaf_values_in_array(array, selectors, base_key, mapper),
value => mapper(base_key, value),
Value::Object(object) => {
map_leaf_values_in_object(object, selectors, base_key, &array_indices, mapper)
}
Value::Array(array) => {
map_leaf_values_in_array(array, selectors, base_key, &array_indices, mapper)
}
value => mapper(base_key, &array_indices, value),
}
}
}
@ -743,12 +755,14 @@ mod tests {
}
});
map_leaf_values(value.as_object_mut().unwrap(), ["jean.race.name"], |key, value| {
match (value, key) {
map_leaf_values(
value.as_object_mut().unwrap(),
["jean.race.name"],
|key, _, value| match (value, key) {
(Value::String(name), "jean.race.name") => *name = S("patou"),
_ => unreachable!(),
}
});
},
);
assert_eq!(
value,
@ -775,7 +789,7 @@ mod tests {
});
let mut calls = 0;
map_leaf_values(value.as_object_mut().unwrap(), ["jean"], |key, value| {
map_leaf_values(value.as_object_mut().unwrap(), ["jean"], |key, _, value| {
calls += 1;
match (value, key) {
(Value::String(name), "jean.race.name") => *name = S("patou"),
@ -798,4 +812,52 @@ mod tests {
})
);
}
#[test]
fn map_array() {
let mut value: Value = json!({
"no_array": "peter",
"simple": ["foo", "bar"],
"nested": [
{
"a": [
["cat", "dog"],
["fox", "bear"],
],
"b": "hi",
},
{
"a": ["green", "blue"],
},
],
});
map_leaf_values(
value.as_object_mut().unwrap(),
["no_array", "simple", "nested"],
|_key, array_indices, value| {
*value = format!("{array_indices:?}").into();
},
);
assert_eq!(
value,
json!({
"no_array": "[]",
"simple": ["[0]", "[1]"],
"nested": [
{
"a": [
["[0, 0, 0]", "[0, 0, 1]"],
["[0, 1, 0]", "[0, 1, 1]"],
],
"b": "[0]",
},
{
"a": ["[1, 0]", "[1, 1]"],
},
],
})
);
}
}