File size: 5,597 Bytes
52a9af3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
use crate::script_attribution::normalized_relative_script_path;
use codex_plugin::PluginId;
use codex_protocol::items::is_safe_plugin_relative_path;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::fs::File;
use std::io::Read;

const ANALYTICS_MANIFEST_FILE: &str = "analytics.yaml";
const MAX_ANALYTICS_MANIFEST_BYTES: u64 = 64 * 1024;
const MAX_IDENTIFIER_LEN: usize = 64;
const MAX_DIMENSIONS_PER_MEASUREMENT: usize = 8;

/// The manifest declaration for one numeric measurement.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginMeasurementDefinition {
    pub enum_dimensions: BTreeMap<String, BTreeSet<String>>,
}

/// Custom metrics allowed for one trusted plugin script operation.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginMetricsOperation {
    pub operation_name: String,
    pub measurements: BTreeMap<String, PluginMeasurementDefinition>,
}

/// A metrics operation bound to identity from a fresh trusted command lookup.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedPluginMetricsOperation {
    pub plugin_id: PluginId,
    pub operation: PluginMetricsOperation,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct AnalyticsManifest {
    version: u32,
    #[serde(with = "serde_with::rust::maps_duplicate_key_is_error")]
    operations: BTreeMap<String, OperationDeclaration>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct OperationDeclaration {
    path: String,
    #[serde(with = "serde_with::rust::maps_duplicate_key_is_error")]
    measurements: BTreeMap<String, MeasurementDeclaration>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct MeasurementDeclaration {
    #[serde(default, with = "serde_with::rust::maps_duplicate_key_is_error")]
    dimensions: BTreeMap<String, Vec<String>>,
}

pub(crate) fn load_plugin_metrics_operations(
    plugin_root: &AbsolutePathBuf,
) -> Option<BTreeMap<String, PluginMetricsOperation>> {
    let manifest_path = plugin_root.join(ANALYTICS_MANIFEST_FILE);
    let canonical_manifest_path = manifest_path.canonicalize().ok()?;
    if canonical_manifest_path != manifest_path || !manifest_path.as_path().is_file() {
        return None;
    }

    let mut contents = Vec::new();
    File::open(manifest_path.as_path())
        .ok()?
        .take(MAX_ANALYTICS_MANIFEST_BYTES + 1)
        .read_to_end(&mut contents)
        .ok()?;
    if contents.len() as u64 > MAX_ANALYTICS_MANIFEST_BYTES {
        return None;
    }
    let manifest: AnalyticsManifest = serde_yaml::from_slice(&contents).ok()?;
    validate_manifest(manifest, plugin_root)
}

fn validate_manifest(
    manifest: AnalyticsManifest,
    plugin_root: &AbsolutePathBuf,
) -> Option<BTreeMap<String, PluginMetricsOperation>> {
    if manifest.version != 1 || manifest.operations.is_empty() {
        return None;
    }

    let mut operations_by_path = BTreeMap::new();
    for (operation_name, operation) in manifest.operations {
        if !valid_identifier(&operation_name) || operation.measurements.is_empty() {
            return None;
        }
        let normalized_path = validated_operation_path(plugin_root, &operation.path)?;
        let mut measurements = BTreeMap::new();
        for (measurement_name, measurement) in operation.measurements {
            if !valid_identifier(&measurement_name)
                || measurement.dimensions.len() > MAX_DIMENSIONS_PER_MEASUREMENT
            {
                return None;
            }
            let mut enum_dimensions = BTreeMap::new();
            for (dimension_name, values) in measurement.dimensions {
                if !valid_identifier(&dimension_name) || values.is_empty() {
                    return None;
                }
                let value_count = values.len();
                let values = values.into_iter().collect::<BTreeSet<_>>();
                if values.len() != value_count
                    || values.iter().any(|value| !valid_identifier(value))
                {
                    return None;
                }
                enum_dimensions.insert(dimension_name, values);
            }
            measurements.insert(
                measurement_name,
                PluginMeasurementDefinition { enum_dimensions },
            );
        }
        if operations_by_path
            .insert(
                normalized_path,
                PluginMetricsOperation {
                    operation_name,
                    measurements,
                },
            )
            .is_some()
        {
            return None;
        }
    }
    Some(operations_by_path)
}

fn validated_operation_path(plugin_root: &AbsolutePathBuf, path: &str) -> Option<String> {
    let normalized_path = path.strip_prefix("./").unwrap_or(path);
    if !is_safe_plugin_relative_path(normalized_path) {
        return None;
    }
    let script = plugin_root.join(normalized_path).canonicalize().ok()?;
    if !script.as_path().is_file() {
        return None;
    }
    let canonical_relative_path = script.as_path().strip_prefix(plugin_root.as_path()).ok()?;
    (normalized_relative_script_path(canonical_relative_path)?.as_str() == normalized_path)
        .then(|| normalized_path.to_string())
}

fn valid_identifier(value: &str) -> bool {
    let mut chars = value.chars();
    matches!(chars.next(), Some('a'..='z'))
        && value.len() <= MAX_IDENTIFIER_LEN
        && chars.all(|character| {
            character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_'
        })
}