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
|
use std::path::{Path, PathBuf};
const PLUGIN_CONFIG_PATH_COMPONENT_MAX_CHARS: usize = 120;
pub(crate) fn plugin_config_dir(plugin_id: &str) -> PathBuf {
crate::config::config_dir()
.join("plugins")
.join("config")
.join(plugin_config_path_component(plugin_id))
}
pub(crate) fn plugin_state_dir(plugin_id: &str) -> PathBuf {
crate::config::state_dir()
.join("plugins")
.join(plugin_config_path_component(plugin_id))
}
pub(crate) fn ensure_plugin_user_dirs(plugin_id: &str) -> std::io::Result<()> {
ensure_plugin_config_dir(plugin_id)?;
std::fs::create_dir_all(plugin_state_dir(plugin_id))?;
Ok(())
}
fn ensure_plugin_config_dir(plugin_id: &str) -> std::io::Result<()> {
let config_dir = plugin_config_dir(plugin_id);
if config_dir.exists() {
return std::fs::create_dir_all(config_dir);
}
if let Some(legacy_dir) = legacy_plugin_config_dirs(plugin_id)
.into_iter()
.find(|path| path.is_dir())
{
copy_dir_all(&legacy_dir, &config_dir)?;
return Ok(());
}
std::fs::create_dir_all(config_dir)
}
fn legacy_plugin_config_dirs(plugin_id: &str) -> Vec<PathBuf> {
let plugins_dir = crate::config::config_dir().join("plugins");
let old_unhashed =
(!matches!(plugin_id, "config" | "github")).then(|| plugins_dir.join(plugin_id));
let current_hashed =
plugins_dir.join(crate::api::schema::plugin_managed_path_component(plugin_id));
let mut candidates = Vec::new();
if let Some(old_unhashed) = old_unhashed {
if old_unhashed != current_hashed {
candidates.push(old_unhashed);
}
}
candidates.push(current_hashed);
candidates
}
fn plugin_config_path_component(value: &str) -> String {
let mut component = String::new();
for byte in value.bytes() {
if byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'_' | b'-')
{
component.push(byte as char);
} else {
use std::fmt::Write as _;
let _ = write!(component, "%{byte:02X}");
}
}
if component.ends_with('.') {
component.pop();
component.push_str("%2E");
}
if component.is_empty() {
return "%plugin".to_string();
}
if crate::api::schema::has_windows_reserved_stem_for_path_component(&component) {
component = format!("%{component}");
}
if component.chars().count() > PLUGIN_CONFIG_PATH_COMPONENT_MAX_CHARS {
let hash = crate::api::schema::short_plugin_id_hash_for_path_component(value);
let prefix_len = PLUGIN_CONFIG_PATH_COMPONENT_MAX_CHARS - hash.len() - 1;
let prefix = component.chars().take(prefix_len).collect::<String>();
return format!("{prefix}-{hash}");
}
component
}
fn copy_dir_all(source: &Path, destination: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(destination)?;
for entry in std::fs::read_dir(source)? {
let entry = entry?;
let file_type = entry.file_type()?;
let destination_path = destination.join(entry.file_name());
if file_type.is_dir() {
copy_dir_all(&entry.path(), &destination_path)?;
} else {
std::fs::copy(entry.path(), destination_path)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plugin_config_path_component_is_readable_and_collision_free() {
assert_eq!(
plugin_config_path_component("examples.agent-telegram-notify"),
"examples.agent-telegram-notify"
);
assert_eq!(plugin_config_path_component("example:a"), "example%3Aa");
assert_eq!(plugin_config_path_component("Example"), "%45xample");
assert_ne!(
plugin_config_path_component("example:a"),
plugin_config_path_component("example-a")
);
assert_ne!(
plugin_config_path_component("Example"),
plugin_config_path_component("example")
);
assert_ne!(
plugin_config_path_component(&"A".repeat(120)),
plugin_config_path_component(&"B".repeat(120))
);
assert!(plugin_config_path_component(&"A".repeat(120)).len() <= 120);
assert_eq!(plugin_config_path_component("con"), "%con");
assert_eq!(plugin_config_path_component("example."), "example%2E");
}
}
|