1pub const DEFAULT_RETENTION_DAYS: i32 = 30;
5pub const DEFAULT_AUTO_PURGE_ENABLED: bool = false;
7#[cfg(feature = "server")]
9pub const MIN_RETENTION_DAYS: i32 = 1;
10#[cfg(feature = "server")]
12pub const MAX_RETENTION_DAYS: i32 = 365;
13
14#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
16pub struct TrashSettings {
17 pub auto_purge_enabled: bool,
19 pub retention_days: i32,
21}
22
23impl Default for TrashSettings {
24 fn default() -> Self {
25 Self {
26 auto_purge_enabled: DEFAULT_AUTO_PURGE_ENABLED,
27 retention_days: DEFAULT_RETENTION_DAYS,
28 }
29 }
30}
31
32impl TrashSettings {
33 #[cfg(feature = "server")]
35 pub fn clamp_retention(days: i32) -> i32 {
36 days.clamp(MIN_RETENTION_DAYS, MAX_RETENTION_DAYS)
37 }
38}
39
40pub const DEFAULT_BACKUP_AUTO_ENABLED: bool = false;
46pub const DEFAULT_BACKUP_TIME_UTC: &str = "04:00";
48pub const DEFAULT_BACKUP_RETENTION_COUNT: i32 = 30;
50pub const DEFAULT_BACKUP_INCLUDE_UPLOADS: bool = true;
52#[cfg(feature = "server")]
54pub const MIN_BACKUP_RETENTION_COUNT: i32 = 1;
55#[cfg(feature = "server")]
57pub const MAX_BACKUP_RETENTION_COUNT: i32 = 365;
58
59#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
61pub struct BackupSettings {
62 pub auto_enabled: bool,
64 pub time_utc: String,
66 pub retention_count: i32,
68 pub include_uploads: bool,
70}
71
72impl Default for BackupSettings {
73 fn default() -> Self {
74 Self {
75 auto_enabled: DEFAULT_BACKUP_AUTO_ENABLED,
76 time_utc: DEFAULT_BACKUP_TIME_UTC.to_string(),
77 retention_count: DEFAULT_BACKUP_RETENTION_COUNT,
78 include_uploads: DEFAULT_BACKUP_INCLUDE_UPLOADS,
79 }
80 }
81}
82
83impl BackupSettings {
84 #[cfg(feature = "server")]
86 pub fn clamp_retention(count: i32) -> i32 {
87 count.clamp(MIN_BACKUP_RETENTION_COUNT, MAX_BACKUP_RETENTION_COUNT)
88 }
89
90 #[cfg(feature = "server")]
92 pub fn is_valid_time_utc(s: &str) -> bool {
93 parse_hhmm(s).is_some()
94 }
95
96 #[cfg(feature = "server")]
99 pub fn normalize_time_utc(s: &str) -> String {
100 match parse_hhmm(s) {
101 Some((h, m)) => format!("{h:02}:{m:02}"),
102 None => DEFAULT_BACKUP_TIME_UTC.to_string(),
103 }
104 }
105
106 #[cfg(feature = "server")]
108 pub fn next_run_after(
109 &self,
110 now: chrono::DateTime<chrono::Utc>,
111 ) -> Option<chrono::DateTime<chrono::Utc>> {
112 let (h, m) = parse_hhmm(&self.time_utc)?;
113 let today = now.date_naive();
114 let today_at = today.and_hms_opt(h as u32, m as u32, 0)?.and_utc();
115 if today_at > now {
116 Some(today_at)
117 } else {
118 Some(
119 today
120 .succ_opt()?
121 .and_hms_opt(h as u32, m as u32, 0)?
122 .and_utc(),
123 )
124 }
125 }
126}
127
128#[cfg(feature = "server")]
130fn parse_hhmm(s: &str) -> Option<(u8, u8)> {
131 let mut parts = s.trim().split(':');
132 let h: u8 = parts.next()?.trim().parse().ok()?;
133 let m: u8 = parts.next()?.trim().parse().ok()?;
134 if parts.next().is_some() || h > 23 || m > 59 {
135 return None;
136 }
137 Some((h, m))
138}
139
140#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
142pub struct LastBackupRun {
143 pub at: String,
145 pub ok: bool,
147 pub file: Option<String>,
149 pub error: Option<String>,
151}
152
153#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
155pub struct BackupSettingsView {
156 pub settings: BackupSettings,
157 pub last_run: Option<LastBackupRun>,
159 pub next_run_at: Option<String>,
161}
162
163pub const DEFAULT_UPLOAD_CONCURRENCY: i32 = 3;
170#[cfg(feature = "server")]
172pub const MIN_UPLOAD_CONCURRENCY: i32 = 1;
173#[cfg(feature = "server")]
175pub const MAX_UPLOAD_CONCURRENCY: i32 = 8;
176
177#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
179pub struct UploadSettings {
180 pub concurrency: i32,
182}
183
184impl Default for UploadSettings {
185 fn default() -> Self {
186 Self {
187 concurrency: DEFAULT_UPLOAD_CONCURRENCY,
188 }
189 }
190}
191
192impl UploadSettings {
193 #[cfg(feature = "server")]
195 pub fn clamp_concurrency(n: i32) -> i32 {
196 n.clamp(MIN_UPLOAD_CONCURRENCY, MAX_UPLOAD_CONCURRENCY)
197 }
198}
199
200pub const DEFAULT_SITE_GITHUB_URL: &str = "";
202#[cfg(any(feature = "server", test))]
203pub const MAX_SITE_GITHUB_URL_LEN: usize = 500;
205
206pub const DEFAULT_HOME_TITLE: &str = "慢慢读源码,";
208pub const DEFAULT_HOME_SUBTITLE: &str = "认真写长文。";
210pub const DEFAULT_HOME_INTRO: &str =
212 "Rust、Go 与前端工程的源码剖析和长文笔记。每一篇,都试着把一个实现讲清楚。";
213pub const DEFAULT_SITE_DESCRIPTION: &str = "Rust、Go 与前端工程的源码剖析和长文笔记。";
215#[cfg(any(feature = "server", test))]
216pub const MAX_SITE_DESCRIPTION_LEN: usize = 120;
218#[cfg(any(feature = "server", test))]
219pub const MAX_HOME_TITLE_LEN: usize = 40;
221#[cfg(any(feature = "server", test))]
222pub const MAX_HOME_INTRO_LEN: usize = 200;
224
225#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
230pub struct SiteSettings {
231 pub github_url: String,
233 #[serde(default)]
235 pub home_title: String,
236 #[serde(default)]
238 pub home_subtitle: String,
239 #[serde(default)]
241 pub home_intro: String,
242 #[serde(default)]
244 pub site_description: String,
245}
246
247impl Default for SiteSettings {
248 fn default() -> Self {
249 Self {
250 github_url: DEFAULT_SITE_GITHUB_URL.to_string(),
251 home_title: String::new(),
252 home_subtitle: String::new(),
253 home_intro: String::new(),
254 site_description: String::new(),
255 }
256 }
257}
258
259#[derive(Debug, Clone, PartialEq)]
261pub struct HomeTagline {
262 pub title: String,
263 pub subtitle: String,
265 pub intro: String,
266}
267
268impl SiteSettings {
269 pub fn home_tagline(&self) -> HomeTagline {
272 let (title, subtitle) = if self.home_title.is_empty() {
273 (DEFAULT_HOME_TITLE, DEFAULT_HOME_SUBTITLE)
274 } else {
275 (self.home_title.as_str(), self.home_subtitle.as_str())
276 };
277 let intro = if self.home_intro.is_empty() {
278 DEFAULT_HOME_INTRO
279 } else {
280 &self.home_intro
281 };
282 HomeTagline {
283 title: title.to_string(),
284 subtitle: subtitle.to_string(),
285 intro: intro.to_string(),
286 }
287 }
288
289 #[cfg(any(feature = "server", test))]
291 pub fn description(&self) -> &str {
292 if self.site_description.is_empty() {
293 DEFAULT_SITE_DESCRIPTION
294 } else {
295 &self.site_description
296 }
297 }
298
299 #[cfg(any(feature = "server", test))]
300 pub fn normalized(&self) -> Self {
302 Self {
303 github_url: Self::normalize_github_url(&self.github_url),
304 home_title: Self::normalize_home_text(&self.home_title, MAX_HOME_TITLE_LEN),
305 home_subtitle: Self::normalize_home_text(&self.home_subtitle, MAX_HOME_TITLE_LEN),
306 home_intro: Self::normalize_home_text(&self.home_intro, MAX_HOME_INTRO_LEN),
307 site_description: Self::normalize_home_text(
308 &self.site_description,
309 MAX_SITE_DESCRIPTION_LEN,
310 ),
311 }
312 }
313
314 #[cfg(any(feature = "server", test))]
315 pub fn normalize_home_text(text: &str, max_chars: usize) -> String {
317 let joined = text.split_whitespace().collect::<Vec<_>>().join(" ");
318 joined.chars().take(max_chars).collect()
319 }
320
321 #[cfg(any(feature = "server", test))]
322 pub fn normalize_github_url(url: &str) -> String {
329 let trimmed = url.trim();
330 if trimmed.is_empty() {
331 return String::new();
332 }
333 let limited: String = trimmed.chars().take(MAX_SITE_GITHUB_URL_LEN).collect();
334 if limited.starts_with("http://") || limited.starts_with("https://") {
335 limited
336 } else {
337 format!("https://{limited}")
338 }
339 }
340}
341
342pub const DEFAULT_APP_BASE_URL: &str = "";
355pub const DEFAULT_COOKIE_SECURE: bool = false;
357pub const DEFAULT_TRUSTED_PROXY_COUNT: u32 = 0;
359pub const DEFAULT_MAX_SESSIONS_PER_USER: u32 = 5;
361
362#[cfg(feature = "server")]
365pub const MAX_TRUSTED_PROXY_COUNT: u32 = 10;
366#[cfg(feature = "server")]
368pub const MIN_MAX_SESSIONS_PER_USER: u32 = 1;
369#[cfg(feature = "server")]
371pub const MAX_MAX_SESSIONS_PER_USER: u32 = 100;
372
373#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
377pub struct SecuritySettings {
378 pub app_base_url: String,
381 pub cookie_secure: bool,
383 pub trusted_proxy_count: u32,
385 pub max_sessions_per_user: u32,
387}
388
389impl Default for SecuritySettings {
390 fn default() -> Self {
391 Self {
392 app_base_url: DEFAULT_APP_BASE_URL.to_string(),
393 cookie_secure: DEFAULT_COOKIE_SECURE,
394 trusted_proxy_count: DEFAULT_TRUSTED_PROXY_COUNT,
395 max_sessions_per_user: DEFAULT_MAX_SESSIONS_PER_USER,
396 }
397 }
398}
399
400impl SecuritySettings {
401 #[cfg(feature = "server")]
403 pub fn clamp_trusted_proxy_count(n: u32) -> u32 {
404 n.min(MAX_TRUSTED_PROXY_COUNT)
405 }
406
407 #[cfg(feature = "server")]
409 pub fn clamp_max_sessions(n: u32) -> u32 {
410 n.clamp(MIN_MAX_SESSIONS_PER_USER, MAX_MAX_SESSIONS_PER_USER)
411 }
412
413 #[cfg(any(feature = "server", test))]
414 pub fn parse_app_base_url(url: &str) -> Option<String> {
418 let url = url.trim();
419 if url.is_empty() {
420 return Some(String::new());
421 }
422 let rest = url
423 .strip_prefix("https://")
424 .or_else(|| url.strip_prefix("http://"))?;
425 let host = rest.split(['/', '?', '#']).next().unwrap_or_default();
426 let valid =
427 !host.is_empty() && !host.starts_with(':') && !rest.chars().any(char::is_whitespace);
428 valid.then(|| url.to_string())
429 }
430}
431
432pub const DEFAULT_IMAGE_DISK_CACHE_MAX_MB: u32 = 1024;
441pub const DEFAULT_IMAGE_DISK_CACHE_MAX_AGE_HOURS: u32 = 168;
443#[cfg(feature = "server")]
445pub const MIN_IMAGE_DISK_CACHE_MAX_MB: u32 = 1;
446#[cfg(feature = "server")]
448pub const MIN_IMAGE_DISK_CACHE_MAX_AGE_HOURS: u32 = 1;
449#[cfg(feature = "server")]
451pub const MAX_IMAGE_DISK_CACHE_MAX_MB: u32 = 1_048_576;
452#[cfg(feature = "server")]
454pub const MAX_IMAGE_DISK_CACHE_MAX_AGE_HOURS: u32 = 87_600;
455
456#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
458pub struct ImageCacheSettings {
459 pub disk_cache_max_mb: u32,
461 pub disk_cache_max_age_hours: u32,
463}
464
465impl Default for ImageCacheSettings {
466 fn default() -> Self {
467 Self {
468 disk_cache_max_mb: DEFAULT_IMAGE_DISK_CACHE_MAX_MB,
469 disk_cache_max_age_hours: DEFAULT_IMAGE_DISK_CACHE_MAX_AGE_HOURS,
470 }
471 }
472}
473
474impl ImageCacheSettings {
475 #[cfg(feature = "server")]
477 pub fn clamp_max_mb(n: u32) -> u32 {
478 n.clamp(MIN_IMAGE_DISK_CACHE_MAX_MB, MAX_IMAGE_DISK_CACHE_MAX_MB)
479 }
480
481 #[cfg(feature = "server")]
483 pub fn clamp_max_age_hours(n: u32) -> u32 {
484 n.clamp(
485 MIN_IMAGE_DISK_CACHE_MAX_AGE_HOURS,
486 MAX_IMAGE_DISK_CACHE_MAX_AGE_HOURS,
487 )
488 }
489}
490
491pub const DEFAULT_ASSET_ORPHAN_PURGE_ENABLED: bool = true;
503pub const DEFAULT_ASSET_ORPHAN_RETENTION_DAYS: i32 = 7;
506#[cfg(feature = "server")]
508pub const MIN_ASSET_ORPHAN_RETENTION_DAYS: i32 = 1;
509#[cfg(feature = "server")]
511pub const MAX_ASSET_ORPHAN_RETENTION_DAYS: i32 = 365;
512
513#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
515pub struct AssetPurgeSettings {
516 pub auto_purge_enabled: bool,
518 pub retention_days: i32,
520}
521
522impl Default for AssetPurgeSettings {
523 fn default() -> Self {
524 Self {
525 auto_purge_enabled: DEFAULT_ASSET_ORPHAN_PURGE_ENABLED,
526 retention_days: DEFAULT_ASSET_ORPHAN_RETENTION_DAYS,
527 }
528 }
529}
530
531impl AssetPurgeSettings {
532 #[cfg(feature = "server")]
534 pub fn clamp_retention(days: i32) -> i32 {
535 days.clamp(
536 MIN_ASSET_ORPHAN_RETENTION_DAYS,
537 MAX_ASSET_ORPHAN_RETENTION_DAYS,
538 )
539 }
540}
541
542pub const DEFAULT_RATE_LIMIT_STRICT_PER_SEC: u32 = 1;
556pub const DEFAULT_RATE_LIMIT_STRICT_BURST: u32 = 5;
557pub const DEFAULT_RATE_LIMIT_UPLOAD_PER_SEC: u32 = 2;
559pub const DEFAULT_RATE_LIMIT_UPLOAD_BURST: u32 = 15;
560pub const DEFAULT_RATE_LIMIT_IMAGE_PER_SEC: u32 = 10;
562pub const DEFAULT_RATE_LIMIT_IMAGE_BURST: u32 = 50;
563pub const DEFAULT_RATE_LIMIT_COMMENT_PER_SEC: u32 = 1;
565pub const DEFAULT_RATE_LIMIT_COMMENT_BURST: u32 = 5;
566pub const DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_PER_SEC: u32 = 1;
568pub const DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_BURST: u32 = 5;
569pub const DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_DAILY: u32 = 30;
572pub const DEFAULT_RATE_LIMIT_CODE_EXEC_PER_SEC: u32 = 1;
574pub const DEFAULT_RATE_LIMIT_CODE_EXEC_BURST: u32 = 3;
575pub const DEFAULT_RATE_LIMIT_CODE_EXEC_DAILY: u32 = 50;
577pub const DEFAULT_RATE_LIMIT_UNKNOWN_PER_SEC: u32 = 30;
579pub const DEFAULT_RATE_LIMIT_UNKNOWN_BURST: u32 = 100;
580pub const DEFAULT_RATE_LIMIT_MCP_PER_SEC: u32 = 10;
582pub const DEFAULT_RATE_LIMIT_MCP_BURST: u32 = 30;
583pub const DEFAULT_RATE_LIMIT_MCP_UPLOAD_PER_SEC: u32 = 2;
585pub const DEFAULT_RATE_LIMIT_MCP_UPLOAD_BURST: u32 = 5;
586pub const DEFAULT_RATE_LIMIT_GC_INTERVAL_SECS: u32 = 300;
588
589#[cfg(feature = "server")]
591pub const MIN_RATE_LIMIT_PER_SEC: u32 = 1;
592#[cfg(feature = "server")]
594pub const MAX_RATE_LIMIT_PER_SEC: u32 = 10_000;
595#[cfg(feature = "server")]
597pub const MIN_RATE_LIMIT_BURST: u32 = 1;
598#[cfg(feature = "server")]
600pub const MIN_RATE_LIMIT_DAILY: u32 = 1;
601#[cfg(feature = "server")]
603pub const MIN_RATE_LIMIT_GC_INTERVAL_SECS: u32 = 1;
604
605#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
610pub struct RateLimitSettings {
611 pub strict_per_sec: u32,
613 pub strict_burst: u32,
615 pub upload_per_sec: u32,
617 pub upload_burst: u32,
619 pub image_per_sec: u32,
621 pub image_burst: u32,
623 pub comment_per_sec: u32,
625 pub comment_burst: u32,
627 pub comment_upload_per_sec: u32,
629 pub comment_upload_burst: u32,
631 pub comment_upload_daily: u32,
633 pub code_exec_per_sec: u32,
635 pub code_exec_burst: u32,
637 pub code_exec_daily: u32,
639 pub unknown_per_sec: u32,
641 pub unknown_burst: u32,
643 pub mcp_per_sec: u32,
645 pub mcp_burst: u32,
647 pub mcp_upload_per_sec: u32,
649 pub mcp_upload_burst: u32,
651 pub gc_interval_secs: u32,
653}
654
655impl Default for RateLimitSettings {
656 fn default() -> Self {
657 Self {
658 strict_per_sec: DEFAULT_RATE_LIMIT_STRICT_PER_SEC,
659 strict_burst: DEFAULT_RATE_LIMIT_STRICT_BURST,
660 upload_per_sec: DEFAULT_RATE_LIMIT_UPLOAD_PER_SEC,
661 upload_burst: DEFAULT_RATE_LIMIT_UPLOAD_BURST,
662 image_per_sec: DEFAULT_RATE_LIMIT_IMAGE_PER_SEC,
663 image_burst: DEFAULT_RATE_LIMIT_IMAGE_BURST,
664 comment_per_sec: DEFAULT_RATE_LIMIT_COMMENT_PER_SEC,
665 comment_burst: DEFAULT_RATE_LIMIT_COMMENT_BURST,
666 comment_upload_per_sec: DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_PER_SEC,
667 comment_upload_burst: DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_BURST,
668 comment_upload_daily: DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_DAILY,
669 code_exec_per_sec: DEFAULT_RATE_LIMIT_CODE_EXEC_PER_SEC,
670 code_exec_burst: DEFAULT_RATE_LIMIT_CODE_EXEC_BURST,
671 code_exec_daily: DEFAULT_RATE_LIMIT_CODE_EXEC_DAILY,
672 unknown_per_sec: DEFAULT_RATE_LIMIT_UNKNOWN_PER_SEC,
673 unknown_burst: DEFAULT_RATE_LIMIT_UNKNOWN_BURST,
674 mcp_per_sec: DEFAULT_RATE_LIMIT_MCP_PER_SEC,
675 mcp_burst: DEFAULT_RATE_LIMIT_MCP_BURST,
676 mcp_upload_per_sec: DEFAULT_RATE_LIMIT_MCP_UPLOAD_PER_SEC,
677 mcp_upload_burst: DEFAULT_RATE_LIMIT_MCP_UPLOAD_BURST,
678 gc_interval_secs: DEFAULT_RATE_LIMIT_GC_INTERVAL_SECS,
679 }
680 }
681}
682
683impl RateLimitSettings {
684 #[cfg(feature = "server")]
686 pub fn clamp_per_sec(n: u32) -> u32 {
687 n.clamp(MIN_RATE_LIMIT_PER_SEC, MAX_RATE_LIMIT_PER_SEC)
688 }
689
690 #[cfg(feature = "server")]
692 pub fn clamp_burst(n: u32) -> u32 {
693 n.max(MIN_RATE_LIMIT_BURST)
694 }
695
696 #[cfg(feature = "server")]
698 pub fn clamp_daily(n: u32) -> u32 {
699 n.max(MIN_RATE_LIMIT_DAILY)
700 }
701
702 #[cfg(feature = "server")]
704 pub fn clamp_gc_interval(n: u32) -> u32 {
705 n.max(MIN_RATE_LIMIT_GC_INTERVAL_SECS)
706 }
707}
708
709pub const DEFAULT_WEBP_QUALITY: f32 = 85.0;
719pub const DEFAULT_WEBP_METHOD: u32 = 2;
721
722#[cfg(feature = "server")]
724pub const MIN_WEBP_QUALITY: f32 = 0.0;
725#[cfg(feature = "server")]
727pub const MAX_WEBP_QUALITY: f32 = 100.0;
728#[cfg(feature = "server")]
730pub const MAX_WEBP_METHOD: u32 = 6;
731
732#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
736pub struct WebpSettings {
737 pub quality: f32,
739 pub method: u32,
741}
742
743impl Default for WebpSettings {
744 fn default() -> Self {
745 Self {
746 quality: DEFAULT_WEBP_QUALITY,
747 method: DEFAULT_WEBP_METHOD,
748 }
749 }
750}
751
752impl WebpSettings {
753 #[cfg(feature = "server")]
755 pub fn clamp_quality(q: f32) -> f32 {
756 q.clamp(MIN_WEBP_QUALITY, MAX_WEBP_QUALITY)
757 }
758
759 #[cfg(feature = "server")]
761 pub fn clamp_method(m: u32) -> u32 {
762 m.min(MAX_WEBP_METHOD)
763 }
764}
765
766pub const DEFAULT_IMAGE_MAX_DIMENSION: u32 = 8192;
776pub const DEFAULT_IMAGE_MAX_PIXELS: u64 = 50_000_000;
778pub const DEFAULT_IMAGE_DIMENSIONS_CACHE_TTL_SECS: u64 = 86400;
780
781#[cfg(feature = "server")]
783pub const MIN_IMAGE_MAX_DIMENSION: u32 = 512;
784#[cfg(feature = "server")]
786pub const MIN_IMAGE_MAX_PIXELS: u64 = 1_000_000;
787#[cfg(feature = "server")]
789pub const MIN_IMAGE_DIMENSIONS_CACHE_TTL_SECS: u64 = 1;
790
791#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
795pub struct ImageLimitSettings {
796 pub max_dimension: u32,
798 pub max_pixels: u64,
801 pub dimensions_cache_ttl_secs: u64,
803}
804
805impl Default for ImageLimitSettings {
806 fn default() -> Self {
807 Self {
808 max_dimension: DEFAULT_IMAGE_MAX_DIMENSION,
809 max_pixels: DEFAULT_IMAGE_MAX_PIXELS,
810 dimensions_cache_ttl_secs: DEFAULT_IMAGE_DIMENSIONS_CACHE_TTL_SECS,
811 }
812 }
813}
814
815impl ImageLimitSettings {
816 #[cfg(feature = "server")]
818 pub fn clamp_max_dimension(n: u32) -> u32 {
819 n.max(MIN_IMAGE_MAX_DIMENSION)
820 }
821
822 #[cfg(feature = "server")]
824 pub fn clamp_max_pixels(n: u64) -> u64 {
825 n.max(MIN_IMAGE_MAX_PIXELS)
826 }
827
828 #[cfg(feature = "server")]
830 pub fn clamp_dimensions_cache_ttl_secs(n: u64) -> u64 {
831 n.max(MIN_IMAGE_DIMENSIONS_CACHE_TTL_SECS)
832 }
833}
834
835pub const DEFAULT_SSR_CACHE_MAX_PAGE_BYTES: u64 = 2 * 1024 * 1024;
844#[cfg(feature = "server")]
846pub const MIN_SSR_CACHE_MAX_PAGE_BYTES: u64 = 64 * 1024;
847#[cfg(feature = "server")]
850pub const MAX_SSR_CACHE_MAX_PAGE_BYTES: u64 = 32 * 1024 * 1024;
851
852pub const MAX_SSR_CACHE_EDGE_TTL_SECS: u64 = 7 * 24 * 3600;
854
855#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
859pub struct SsrCacheSettings {
860 pub max_page_bytes: u64,
862 pub edge_ttl_secs: u64,
866}
867
868impl Default for SsrCacheSettings {
869 fn default() -> Self {
870 Self {
871 max_page_bytes: DEFAULT_SSR_CACHE_MAX_PAGE_BYTES,
872 edge_ttl_secs: 0,
873 }
874 }
875}
876
877impl SsrCacheSettings {
878 #[cfg(feature = "server")]
880 pub fn clamp_max_page_bytes(n: u64) -> u64 {
881 n.clamp(MIN_SSR_CACHE_MAX_PAGE_BYTES, MAX_SSR_CACHE_MAX_PAGE_BYTES)
882 }
883
884 #[cfg(feature = "server")]
886 pub fn clamp_edge_ttl_secs(n: u64) -> u64 {
887 n.min(MAX_SSR_CACHE_EDGE_TTL_SECS)
888 }
889}
890
891pub const DEFAULT_RUNNER_ALLOW_NETWORK: bool = false;
902pub const DEFAULT_RUNNER_MAX_CONCURRENT: u32 = 4;
904pub const DEFAULT_RUNNER_MAX_CPU_CORES: f64 = 2.0;
906pub const DEFAULT_RUNNER_MAX_MEMORY_MB: u32 = 1024;
908pub const DEFAULT_RUNNER_MAX_TIMEOUT_SECS: u32 = 30;
910pub const DEFAULT_RUNNER_MAX_OUTPUT_BYTES: u64 = 1_048_576;
912pub const DEFAULT_RUNNER_MAX_SOURCE_BYTES: u64 = 65_536;
914pub const DEFAULT_RUNNER_QUEUE_TIMEOUT_SECS: u32 = 30;
916pub const DEFAULT_RUNNER_TASK_TTL_SECS: u32 = 300;
918
919#[cfg(feature = "server")]
921pub const MIN_RUNNER_MAX_CONCURRENT: u32 = 1;
922#[cfg(feature = "server")]
924pub const MIN_RUNNER_MAX_CPU_CORES: f64 = 0.1;
925#[cfg(feature = "server")]
927pub const MIN_RUNNER_MAX_MEMORY_MB: u32 = 16;
928#[cfg(feature = "server")]
930pub const MIN_RUNNER_MAX_TIMEOUT_SECS: u32 = 1;
931#[cfg(feature = "server")]
933pub const MAX_RUNNER_MAX_CONCURRENT: u32 = 64;
934#[cfg(feature = "server")]
936pub const MAX_RUNNER_MAX_CPU_CORES: f64 = 64.0;
937#[cfg(feature = "server")]
939pub const MAX_RUNNER_MAX_MEMORY_MB: u32 = 65_536;
940#[cfg(feature = "server")]
942pub const MIN_RUNNER_MAX_BYTES: u64 = 1;
943#[cfg(feature = "server")]
946pub const MAX_RUNNER_MAX_OUTPUT_BYTES: u64 = 64 * 1024 * 1024;
947#[cfg(feature = "server")]
949pub const MAX_RUNNER_MAX_SOURCE_BYTES: u64 = 64 * 1024 * 1024;
950
951#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
955pub struct RunnerSettings {
956 pub allow_network: bool,
958 pub max_concurrent: u32,
960 pub max_cpu_cores: f64,
962 pub max_memory_mb: u32,
964 pub max_timeout_secs: u32,
966 pub max_output_bytes: u64,
968 pub max_source_bytes: u64,
970 pub queue_timeout_secs: u32,
972 pub task_ttl_secs: u32,
974 pub languages: Option<String>,
976}
977
978impl Default for RunnerSettings {
979 fn default() -> Self {
980 Self {
981 allow_network: DEFAULT_RUNNER_ALLOW_NETWORK,
982 max_concurrent: DEFAULT_RUNNER_MAX_CONCURRENT,
983 max_cpu_cores: DEFAULT_RUNNER_MAX_CPU_CORES,
984 max_memory_mb: DEFAULT_RUNNER_MAX_MEMORY_MB,
985 max_timeout_secs: DEFAULT_RUNNER_MAX_TIMEOUT_SECS,
986 max_output_bytes: DEFAULT_RUNNER_MAX_OUTPUT_BYTES,
987 max_source_bytes: DEFAULT_RUNNER_MAX_SOURCE_BYTES,
988 queue_timeout_secs: DEFAULT_RUNNER_QUEUE_TIMEOUT_SECS,
989 task_ttl_secs: DEFAULT_RUNNER_TASK_TTL_SECS,
990 languages: None,
991 }
992 }
993}
994
995impl RunnerSettings {
996 #[cfg(feature = "server")]
998 pub fn clamp_max_concurrent(n: u32) -> u32 {
999 n.clamp(MIN_RUNNER_MAX_CONCURRENT, MAX_RUNNER_MAX_CONCURRENT)
1000 }
1001
1002 #[cfg(feature = "server")]
1004 pub fn clamp_max_cpu_cores(n: f64) -> f64 {
1005 if n.is_nan() {
1006 return DEFAULT_RUNNER_MAX_CPU_CORES;
1007 }
1008 n.clamp(MIN_RUNNER_MAX_CPU_CORES, MAX_RUNNER_MAX_CPU_CORES)
1009 }
1010
1011 #[cfg(feature = "server")]
1013 pub fn clamp_max_memory_mb(n: u32) -> u32 {
1014 n.clamp(MIN_RUNNER_MAX_MEMORY_MB, MAX_RUNNER_MAX_MEMORY_MB)
1015 }
1016
1017 #[cfg(feature = "server")]
1019 pub fn clamp_max_timeout_secs(n: u32) -> u32 {
1020 n.max(MIN_RUNNER_MAX_TIMEOUT_SECS)
1021 }
1022
1023 #[cfg(feature = "server")]
1025 pub fn clamp_max_output_bytes(n: u64) -> u64 {
1026 n.clamp(MIN_RUNNER_MAX_BYTES, MAX_RUNNER_MAX_OUTPUT_BYTES)
1027 }
1028
1029 #[cfg(feature = "server")]
1031 pub fn clamp_max_source_bytes(n: u64) -> u64 {
1032 n.clamp(MIN_RUNNER_MAX_BYTES, MAX_RUNNER_MAX_SOURCE_BYTES)
1033 }
1034
1035 #[cfg(feature = "server")]
1037 pub fn clamp_queue_timeout_secs(n: u32) -> u32 {
1038 n.max(MIN_RUNNER_MAX_TIMEOUT_SECS)
1039 }
1040
1041 #[cfg(feature = "server")]
1043 pub fn clamp_task_ttl_secs(n: u32) -> u32 {
1044 n.max(1)
1045 }
1046
1047 #[cfg(feature = "server")]
1049 pub fn normalize_languages(s: &str) -> Option<String> {
1050 let parts: Vec<String> = s
1051 .split(',')
1052 .map(|t| t.trim().to_lowercase())
1053 .filter(|t| !t.is_empty())
1054 .collect();
1055 if parts.is_empty() {
1056 None
1057 } else {
1058 Some(parts.join(","))
1059 }
1060 }
1061}
1062
1063#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1068pub struct SystemInfo {
1069 pub database_url_masked: String,
1071 pub rust_log: String,
1073 pub db_pool_size: u32,
1075 pub statement_timeout_secs: u64,
1077 pub ssr_cache_secs: u64,
1079 pub compression_algorithms: String,
1081 pub expose_version_headers: bool,
1083 pub docker_socket_path: String,
1085 pub mcp_token_enc_key_set: bool,
1087 pub migrate_startup_timeout_secs: u64,
1089 pub sysinfo_sample_secs: f64,
1091 pub log_viewer_level: String,
1093 pub backup_import_max_mb: u64,
1095 pub previous_public_dir: String,
1097}
1098
1099#[cfg(test)]
1100mod tests {
1101 use super::*;
1102
1103 #[test]
1104 fn default_is_disabled_30_days() {
1105 let s = TrashSettings::default();
1106 assert!(!s.auto_purge_enabled);
1107 assert_eq!(s.retention_days, 30);
1108 }
1109
1110 #[test]
1111 #[cfg(feature = "server")]
1112 fn clamp_retention_keeps_valid() {
1113 assert_eq!(TrashSettings::clamp_retention(7), 7);
1114 assert_eq!(TrashSettings::clamp_retention(30), 30);
1115 }
1116
1117 #[test]
1118 #[cfg(feature = "server")]
1119 fn clamp_retention_clamps_below_min() {
1120 assert_eq!(TrashSettings::clamp_retention(0), MIN_RETENTION_DAYS);
1121 assert_eq!(TrashSettings::clamp_retention(-5), MIN_RETENTION_DAYS);
1122 }
1123
1124 #[test]
1125 #[cfg(feature = "server")]
1126 fn clamp_retention_clamps_above_max() {
1127 assert_eq!(TrashSettings::clamp_retention(366), MAX_RETENTION_DAYS);
1128 assert_eq!(TrashSettings::clamp_retention(i32::MAX), MAX_RETENTION_DAYS);
1129 }
1130
1131 #[test]
1132 #[cfg(feature = "server")]
1133 fn clamp_retention_boundary() {
1134 assert_eq!(
1135 TrashSettings::clamp_retention(MIN_RETENTION_DAYS),
1136 MIN_RETENTION_DAYS
1137 );
1138 assert_eq!(
1139 TrashSettings::clamp_retention(MAX_RETENTION_DAYS),
1140 MAX_RETENTION_DAYS
1141 );
1142 }
1143
1144 #[test]
1145 fn asset_purge_default_is_enabled_7_days() {
1146 let s = AssetPurgeSettings::default();
1147 assert!(s.auto_purge_enabled);
1148 assert_eq!(s.retention_days, 7);
1149 }
1150
1151 #[test]
1152 #[cfg(feature = "server")]
1153 fn asset_purge_clamp_retention() {
1154 assert_eq!(
1155 AssetPurgeSettings::clamp_retention(0),
1156 MIN_ASSET_ORPHAN_RETENTION_DAYS
1157 );
1158 assert_eq!(AssetPurgeSettings::clamp_retention(7), 7);
1159 assert_eq!(
1160 AssetPurgeSettings::clamp_retention(366),
1161 MAX_ASSET_ORPHAN_RETENTION_DAYS
1162 );
1163 }
1164
1165 #[test]
1166 fn site_settings_default_empty() {
1167 let s = SiteSettings::default();
1168 assert_eq!(s.github_url, "");
1169 }
1170
1171 #[test]
1172 fn home_tagline_defaults_when_unset() {
1173 let tagline = SiteSettings::default().home_tagline();
1174 assert_eq!(tagline.title, DEFAULT_HOME_TITLE);
1175 assert_eq!(tagline.subtitle, DEFAULT_HOME_SUBTITLE);
1176 assert_eq!(tagline.intro, DEFAULT_HOME_INTRO);
1177 }
1178
1179 #[test]
1180 fn home_tagline_keeps_empty_subtitle_with_custom_title() {
1181 let settings = SiteSettings {
1182 home_title: "写点什么".into(),
1183 ..SiteSettings::default()
1184 };
1185 let tagline = settings.home_tagline();
1186 assert_eq!(tagline.title, "写点什么");
1187 assert_eq!(tagline.subtitle, "");
1188 assert_eq!(tagline.intro, DEFAULT_HOME_INTRO);
1189 }
1190
1191 #[test]
1192 fn site_description_falls_back_and_is_normalized() {
1193 assert_eq!(
1194 SiteSettings::default().description(),
1195 DEFAULT_SITE_DESCRIPTION
1196 );
1197 let settings = SiteSettings {
1198 site_description: format!(" 写长文\n{}", "字".repeat(200)),
1199 ..SiteSettings::default()
1200 }
1201 .normalized();
1202 assert!(settings.site_description.starts_with("写长文 字"));
1203 assert_eq!(
1204 settings.site_description.chars().count(),
1205 MAX_SITE_DESCRIPTION_LEN
1206 );
1207 }
1208
1209 #[test]
1210 fn site_settings_accept_payload_without_home_fields() {
1211 let settings: SiteSettings =
1212 serde_json::from_str(r#"{"github_url":"https://github.com/x"}"#).unwrap();
1213 assert_eq!(settings.home_title, "");
1214 }
1215
1216 #[test]
1217 fn normalize_home_text_collapses_whitespace_and_truncates() {
1218 assert_eq!(
1219 SiteSettings::normalize_home_text(" 慢慢读,\n 认真写 ", 40),
1220 "慢慢读, 认真写"
1221 );
1222 let long = "字".repeat(MAX_HOME_TITLE_LEN + 5);
1223 let normalized = SiteSettings::normalize_home_text(&long, MAX_HOME_TITLE_LEN);
1224 assert_eq!(normalized.chars().count(), MAX_HOME_TITLE_LEN);
1225 }
1226
1227 #[test]
1228 fn normalize_github_url_empty() {
1229 assert_eq!(SiteSettings::normalize_github_url(""), "");
1230 assert_eq!(SiteSettings::normalize_github_url(" "), "");
1231 }
1232
1233 #[test]
1234 fn normalize_github_url_prepends_https() {
1235 assert_eq!(
1236 SiteSettings::normalize_github_url("github.com/DefectingCat/yggdrasil"),
1237 "https://github.com/DefectingCat/yggdrasil"
1238 );
1239 }
1240
1241 #[test]
1242 fn normalize_github_url_keeps_scheme() {
1243 assert_eq!(
1244 SiteSettings::normalize_github_url("https://github.com/foo"),
1245 "https://github.com/foo"
1246 );
1247 assert_eq!(
1248 SiteSettings::normalize_github_url("http://example.com"),
1249 "http://example.com"
1250 );
1251 }
1252
1253 #[test]
1254 fn normalize_github_url_neutralizes_dangerous_scheme() {
1255 assert_eq!(
1257 SiteSettings::normalize_github_url("javascript:alert(1)"),
1258 "https://javascript:alert(1)"
1259 );
1260 }
1261
1262 #[test]
1263 fn normalize_github_url_trims_and_truncates() {
1264 assert_eq!(
1265 SiteSettings::normalize_github_url(" github.com/x "),
1266 "https://github.com/x"
1267 );
1268 let long = "a".repeat(MAX_SITE_GITHUB_URL_LEN + 50);
1269 let normalized = SiteSettings::normalize_github_url(&long);
1270 assert_eq!(normalized.len(), "https://".len() + MAX_SITE_GITHUB_URL_LEN);
1272 }
1273
1274 #[test]
1277 fn backup_settings_default() {
1278 let s = BackupSettings::default();
1279 assert!(!s.auto_enabled);
1280 assert_eq!(s.time_utc, "04:00");
1281 assert_eq!(s.retention_count, 30);
1282 assert!(s.include_uploads);
1283 }
1284
1285 #[test]
1286 fn normalize_time_pads_single_digits() {
1287 assert_eq!(BackupSettings::normalize_time_utc("4:5"), "04:05");
1288 assert_eq!(BackupSettings::normalize_time_utc(" 23:59 "), "23:59");
1289 assert_eq!(BackupSettings::normalize_time_utc("0:00"), "00:00");
1290 }
1291
1292 #[test]
1293 fn normalize_time_rejects_garbage() {
1294 for bad in ["", "abc", "24:00", "12:60", "1:2:3", "-1:30", "12:", ":30"] {
1295 assert_eq!(
1296 BackupSettings::normalize_time_utc(bad),
1297 DEFAULT_BACKUP_TIME_UTC,
1298 "非法时间应回退默认: {bad:?}"
1299 );
1300 }
1301 }
1302
1303 #[test]
1304 #[cfg(feature = "server")]
1305 fn backup_clamp_retention() {
1306 assert_eq!(
1307 BackupSettings::clamp_retention(0),
1308 MIN_BACKUP_RETENTION_COUNT
1309 );
1310 assert_eq!(
1311 BackupSettings::clamp_retention(366),
1312 MAX_BACKUP_RETENTION_COUNT
1313 );
1314 assert_eq!(BackupSettings::clamp_retention(30), 30);
1315 }
1316
1317 #[test]
1318 #[cfg(feature = "server")]
1319 fn next_run_same_day_when_future() {
1320 use chrono::{TimeZone, Utc};
1321 let s = BackupSettings {
1322 time_utc: "04:00".into(),
1323 ..Default::default()
1324 };
1325 let now = Utc.with_ymd_and_hms(2026, 8, 10, 1, 30, 0).unwrap();
1326 let next = s.next_run_after(now).expect("合法时间必有下次执行");
1327 assert_eq!(next, Utc.with_ymd_and_hms(2026, 8, 10, 4, 0, 0).unwrap());
1328 }
1329
1330 #[test]
1331 #[cfg(feature = "server")]
1332 fn next_run_rolls_to_tomorrow_when_past() {
1333 use chrono::{TimeZone, Utc};
1334 let s = BackupSettings {
1335 time_utc: "04:00".into(),
1336 ..Default::default()
1337 };
1338 for now in [
1340 Utc.with_ymd_and_hms(2026, 8, 10, 4, 0, 0).unwrap(),
1341 Utc.with_ymd_and_hms(2026, 8, 10, 23, 59, 59).unwrap(),
1342 ] {
1343 let next = s.next_run_after(now).expect("合法时间必有下次执行");
1344 assert_eq!(next, Utc.with_ymd_and_hms(2026, 8, 11, 4, 0, 0).unwrap());
1345 }
1346 }
1347
1348 #[test]
1349 #[cfg(feature = "server")]
1350 fn next_run_none_on_invalid_time() {
1351 use chrono::{TimeZone, Utc};
1352 let s = BackupSettings {
1353 time_utc: "garbage".into(),
1354 ..Default::default()
1355 };
1356 let now = Utc.with_ymd_and_hms(2026, 8, 10, 0, 0, 0).unwrap();
1357 assert!(s.next_run_after(now).is_none());
1358 }
1359
1360 #[test]
1363 fn upload_settings_default() {
1364 assert_eq!(UploadSettings::default().concurrency, 3);
1365 }
1366
1367 #[test]
1368 #[cfg(feature = "server")]
1369 fn upload_clamp_concurrency() {
1370 assert_eq!(UploadSettings::clamp_concurrency(0), MIN_UPLOAD_CONCURRENCY);
1371 assert_eq!(
1372 UploadSettings::clamp_concurrency(-1),
1373 MIN_UPLOAD_CONCURRENCY
1374 );
1375 assert_eq!(UploadSettings::clamp_concurrency(9), MAX_UPLOAD_CONCURRENCY);
1376 assert_eq!(
1377 UploadSettings::clamp_concurrency(i32::MAX),
1378 MAX_UPLOAD_CONCURRENCY
1379 );
1380 assert_eq!(UploadSettings::clamp_concurrency(5), 5);
1381 }
1382
1383 #[test]
1386 fn security_settings_default() {
1387 let s = SecuritySettings::default();
1388 assert!(s.app_base_url.is_empty());
1389 assert!(!s.cookie_secure);
1390 assert_eq!(s.trusted_proxy_count, 0);
1391 assert_eq!(s.max_sessions_per_user, 5);
1392 }
1393
1394 #[test]
1395 #[cfg(feature = "server")]
1396 fn security_clamp_trusted_proxy_count() {
1397 assert_eq!(SecuritySettings::clamp_trusted_proxy_count(0), 0);
1398 assert_eq!(SecuritySettings::clamp_trusted_proxy_count(3), 3);
1399 assert_eq!(
1400 SecuritySettings::clamp_trusted_proxy_count(99),
1401 MAX_TRUSTED_PROXY_COUNT
1402 );
1403 }
1404
1405 #[test]
1406 #[cfg(feature = "server")]
1407 fn security_clamp_max_sessions() {
1408 assert_eq!(
1409 SecuritySettings::clamp_max_sessions(0),
1410 MIN_MAX_SESSIONS_PER_USER
1411 );
1412 assert_eq!(SecuritySettings::clamp_max_sessions(5), 5);
1413 assert_eq!(
1414 SecuritySettings::clamp_max_sessions(u32::MAX),
1415 MAX_MAX_SESSIONS_PER_USER
1416 );
1417 }
1418
1419 #[test]
1420 fn security_parse_app_base_url_requires_http_scheme_and_host() {
1421 let parse = SecuritySettings::parse_app_base_url;
1422 assert_eq!(parse(" https://x.com ").as_deref(), Some("https://x.com"));
1423 assert_eq!(
1424 parse("http://localhost:8080/").as_deref(),
1425 Some("http://localhost:8080/")
1426 );
1427 assert_eq!(parse("").as_deref(), Some(""));
1429 assert_eq!(parse(" ").as_deref(), Some(""));
1430 assert_eq!(parse("localhost:8080"), None);
1432 assert_eq!(parse("ftp://x.com"), None);
1433 assert_eq!(parse("https://"), None);
1434 assert_eq!(parse("https://:8080"), None);
1435 assert_eq!(parse("https://x .com"), None);
1436 }
1437
1438 #[test]
1441 fn image_cache_settings_default() {
1442 let s = ImageCacheSettings::default();
1443 assert_eq!(s.disk_cache_max_mb, 1024);
1444 assert_eq!(s.disk_cache_max_age_hours, 168);
1445 }
1446
1447 #[test]
1448 #[cfg(feature = "server")]
1449 fn image_cache_clamp_max_mb() {
1450 assert_eq!(
1451 ImageCacheSettings::clamp_max_mb(0),
1452 MIN_IMAGE_DISK_CACHE_MAX_MB
1453 );
1454 assert_eq!(ImageCacheSettings::clamp_max_mb(2048), 2048);
1455 }
1456
1457 #[test]
1458 #[cfg(feature = "server")]
1459 fn image_cache_clamp_max_age_hours() {
1460 assert_eq!(
1461 ImageCacheSettings::clamp_max_age_hours(0),
1462 MIN_IMAGE_DISK_CACHE_MAX_AGE_HOURS
1463 );
1464 assert_eq!(ImageCacheSettings::clamp_max_age_hours(720), 720);
1465 }
1466
1467 #[test]
1468 #[cfg(feature = "server")]
1469 fn image_cache_clamps_cap_huge_values() {
1470 assert_eq!(
1471 ImageCacheSettings::clamp_max_mb(u32::MAX),
1472 MAX_IMAGE_DISK_CACHE_MAX_MB
1473 );
1474 assert_eq!(
1475 ImageCacheSettings::clamp_max_age_hours(u32::MAX),
1476 MAX_IMAGE_DISK_CACHE_MAX_AGE_HOURS
1477 );
1478 assert_eq!(ImageCacheSettings::clamp_max_mb(1_048_576), 1_048_576);
1480 assert_eq!(ImageCacheSettings::clamp_max_age_hours(87_600), 87_600);
1481 }
1482
1483 #[test]
1484 fn rate_limit_default_values() {
1485 let s = RateLimitSettings::default();
1486 assert_eq!(s.strict_per_sec, 1);
1487 assert_eq!(s.strict_burst, 5);
1488 assert_eq!(s.upload_per_sec, 2);
1489 assert_eq!(s.upload_burst, 15);
1490 assert_eq!(s.image_per_sec, 10);
1491 assert_eq!(s.image_burst, 50);
1492 assert_eq!(s.comment_per_sec, 1);
1493 assert_eq!(s.comment_burst, 5);
1494 assert_eq!(s.comment_upload_per_sec, 1);
1495 assert_eq!(s.comment_upload_burst, 5);
1496 assert_eq!(s.comment_upload_daily, 30);
1497 assert_eq!(s.code_exec_per_sec, 1);
1498 assert_eq!(s.code_exec_burst, 3);
1499 assert_eq!(s.code_exec_daily, 50);
1500 assert_eq!(s.unknown_per_sec, 30);
1501 assert_eq!(s.unknown_burst, 100);
1502 assert_eq!(s.gc_interval_secs, 300);
1503 }
1504
1505 #[test]
1506 #[cfg(feature = "server")]
1507 fn rate_limit_clamp_per_sec() {
1508 assert_eq!(RateLimitSettings::clamp_per_sec(0), MIN_RATE_LIMIT_PER_SEC);
1509 assert_eq!(RateLimitSettings::clamp_per_sec(5), 5);
1510 }
1511
1512 #[test]
1513 #[cfg(feature = "server")]
1514 fn rate_limit_clamp_per_sec_caps_huge_values() {
1515 assert_eq!(
1516 RateLimitSettings::clamp_per_sec(u32::MAX),
1517 MAX_RATE_LIMIT_PER_SEC
1518 );
1519 assert_eq!(RateLimitSettings::clamp_per_sec(10_000), 10_000);
1520 }
1521
1522 #[test]
1523 #[cfg(feature = "server")]
1524 fn rate_limit_clamp_burst() {
1525 assert_eq!(RateLimitSettings::clamp_burst(0), MIN_RATE_LIMIT_BURST);
1526 assert_eq!(RateLimitSettings::clamp_burst(15), 15);
1527 }
1528
1529 #[test]
1530 #[cfg(feature = "server")]
1531 fn rate_limit_clamp_daily() {
1532 assert_eq!(RateLimitSettings::clamp_daily(0), MIN_RATE_LIMIT_DAILY);
1533 assert_eq!(RateLimitSettings::clamp_daily(50), 50);
1534 }
1535
1536 #[test]
1537 #[cfg(feature = "server")]
1538 fn rate_limit_clamp_gc_interval() {
1539 assert_eq!(
1540 RateLimitSettings::clamp_gc_interval(0),
1541 MIN_RATE_LIMIT_GC_INTERVAL_SECS
1542 );
1543 assert_eq!(RateLimitSettings::clamp_gc_interval(300), 300);
1544 }
1545 #[test]
1548 fn webp_settings_default() {
1549 let s = WebpSettings::default();
1550 assert_eq!(s.quality, 85.0);
1551 assert_eq!(s.method, 2);
1552 }
1553
1554 #[test]
1555 #[cfg(feature = "server")]
1556 fn webp_clamp_quality() {
1557 assert_eq!(WebpSettings::clamp_quality(-1.0), 0.0);
1558 assert_eq!(WebpSettings::clamp_quality(50.0), 50.0);
1559 assert_eq!(WebpSettings::clamp_quality(150.0), 100.0);
1560 }
1561
1562 #[test]
1563 #[cfg(feature = "server")]
1564 fn webp_clamp_method() {
1565 assert_eq!(WebpSettings::clamp_method(0), 0);
1566 assert_eq!(WebpSettings::clamp_method(3), 3);
1567 assert_eq!(WebpSettings::clamp_method(9), MAX_WEBP_METHOD);
1568 }
1569
1570 #[test]
1573 fn image_limit_settings_default() {
1574 let s = ImageLimitSettings::default();
1575 assert_eq!(s.max_dimension, 8192);
1576 assert_eq!(s.max_pixels, 50_000_000);
1577 assert_eq!(s.dimensions_cache_ttl_secs, 86400);
1578 }
1579
1580 #[test]
1581 #[cfg(feature = "server")]
1582 fn image_limit_clamp_max_dimension() {
1583 assert_eq!(
1584 ImageLimitSettings::clamp_max_dimension(0),
1585 MIN_IMAGE_MAX_DIMENSION
1586 );
1587 assert_eq!(ImageLimitSettings::clamp_max_dimension(2048), 2048);
1588 }
1589
1590 #[test]
1591 #[cfg(feature = "server")]
1592 fn image_limit_clamp_max_pixels() {
1593 assert_eq!(
1594 ImageLimitSettings::clamp_max_pixels(0),
1595 MIN_IMAGE_MAX_PIXELS
1596 );
1597 assert_eq!(ImageLimitSettings::clamp_max_pixels(99_999_999), 99_999_999);
1598 }
1599
1600 #[test]
1601 #[cfg(feature = "server")]
1602 fn image_limit_clamp_ttl() {
1603 assert_eq!(
1604 ImageLimitSettings::clamp_dimensions_cache_ttl_secs(0),
1605 MIN_IMAGE_DIMENSIONS_CACHE_TTL_SECS
1606 );
1607 assert_eq!(
1608 ImageLimitSettings::clamp_dimensions_cache_ttl_secs(7200),
1609 7200
1610 );
1611 }
1612
1613 #[test]
1614 #[cfg(feature = "server")]
1615 fn ssr_cache_clamp_max_page_bytes() {
1616 assert_eq!(
1617 SsrCacheSettings::clamp_max_page_bytes(0),
1618 MIN_SSR_CACHE_MAX_PAGE_BYTES
1619 );
1620 assert_eq!(
1621 SsrCacheSettings::clamp_max_page_bytes(u64::MAX),
1622 MAX_SSR_CACHE_MAX_PAGE_BYTES
1623 );
1624 assert_eq!(SsrCacheSettings::clamp_max_page_bytes(1 << 20), 1 << 20);
1625 assert_eq!(SsrCacheSettings::clamp_edge_ttl_secs(0), 0);
1626 assert_eq!(SsrCacheSettings::clamp_edge_ttl_secs(3600), 3600);
1627 assert_eq!(
1628 SsrCacheSettings::clamp_edge_ttl_secs(u64::MAX),
1629 MAX_SSR_CACHE_EDGE_TTL_SECS
1630 );
1631 }
1632
1633 #[test]
1636 fn runner_settings_default() {
1637 let s = RunnerSettings::default();
1638 assert!(!s.allow_network);
1639 assert_eq!(s.max_concurrent, 4);
1640 assert_eq!(s.max_cpu_cores, 2.0);
1641 assert_eq!(s.max_memory_mb, 1024);
1642 assert_eq!(s.max_timeout_secs, 30);
1643 assert_eq!(s.max_output_bytes, 1_048_576);
1644 assert_eq!(s.max_source_bytes, 65_536);
1645 assert_eq!(s.queue_timeout_secs, 30);
1646 assert_eq!(s.task_ttl_secs, 300);
1647 assert!(s.languages.is_none());
1648 }
1649
1650 #[test]
1651 #[cfg(feature = "server")]
1652 fn runner_clamp_max_concurrent() {
1653 assert_eq!(
1654 RunnerSettings::clamp_max_concurrent(0),
1655 MIN_RUNNER_MAX_CONCURRENT
1656 );
1657 assert_eq!(RunnerSettings::clamp_max_concurrent(8), 8);
1658 assert_eq!(
1659 RunnerSettings::clamp_max_concurrent(999),
1660 MAX_RUNNER_MAX_CONCURRENT
1661 );
1662 }
1663
1664 #[test]
1665 #[cfg(feature = "server")]
1666 fn runner_clamp_max_cpu_cores() {
1667 assert_eq!(
1668 RunnerSettings::clamp_max_cpu_cores(0.0),
1669 MIN_RUNNER_MAX_CPU_CORES
1670 );
1671 assert_eq!(RunnerSettings::clamp_max_cpu_cores(4.0), 4.0);
1672 assert_eq!(
1673 RunnerSettings::clamp_max_cpu_cores(999.0),
1674 MAX_RUNNER_MAX_CPU_CORES
1675 );
1676 assert_eq!(RunnerSettings::clamp_max_cpu_cores(f64::NAN), 2.0);
1678 }
1679
1680 #[test]
1681 #[cfg(feature = "server")]
1682 fn runner_clamp_max_memory_mb() {
1683 assert_eq!(
1684 RunnerSettings::clamp_max_memory_mb(0),
1685 MIN_RUNNER_MAX_MEMORY_MB
1686 );
1687 assert_eq!(RunnerSettings::clamp_max_memory_mb(2048), 2048);
1688 assert_eq!(
1689 RunnerSettings::clamp_max_memory_mb(u32::MAX),
1690 MAX_RUNNER_MAX_MEMORY_MB
1691 );
1692 }
1693
1694 #[test]
1695 #[cfg(feature = "server")]
1696 fn runner_clamp_max_timeout_secs() {
1697 assert_eq!(
1698 RunnerSettings::clamp_max_timeout_secs(0),
1699 MIN_RUNNER_MAX_TIMEOUT_SECS
1700 );
1701 assert_eq!(RunnerSettings::clamp_max_timeout_secs(120), 120);
1702 }
1703
1704 #[test]
1705 #[cfg(feature = "server")]
1706 fn runner_clamp_byte_limits() {
1707 assert_eq!(RunnerSettings::clamp_max_output_bytes(0), 1);
1708 assert_eq!(RunnerSettings::clamp_max_source_bytes(0), 1);
1709 assert_eq!(
1710 RunnerSettings::clamp_queue_timeout_secs(0),
1711 MIN_RUNNER_MAX_TIMEOUT_SECS
1712 );
1713 assert_eq!(RunnerSettings::clamp_task_ttl_secs(0), 1);
1714 }
1715
1716 #[test]
1717 #[cfg(feature = "server")]
1718 fn runner_clamp_byte_limits_cap_huge_values() {
1719 assert_eq!(
1720 RunnerSettings::clamp_max_output_bytes(u64::MAX),
1721 MAX_RUNNER_MAX_OUTPUT_BYTES
1722 );
1723 assert_eq!(
1724 RunnerSettings::clamp_max_source_bytes(u64::MAX),
1725 MAX_RUNNER_MAX_SOURCE_BYTES
1726 );
1727 assert_eq!(
1729 RunnerSettings::clamp_max_output_bytes(DEFAULT_RUNNER_MAX_OUTPUT_BYTES),
1730 DEFAULT_RUNNER_MAX_OUTPUT_BYTES
1731 );
1732 assert_eq!(
1733 RunnerSettings::clamp_max_source_bytes(DEFAULT_RUNNER_MAX_SOURCE_BYTES),
1734 DEFAULT_RUNNER_MAX_SOURCE_BYTES
1735 );
1736 assert_eq!(
1737 RunnerSettings::clamp_max_output_bytes(65_536 * 1024),
1738 65_536 * 1024
1739 );
1740 assert_eq!(
1741 RunnerSettings::clamp_max_source_bytes(65_536 * 1024),
1742 65_536 * 1024
1743 );
1744 }
1745
1746 #[test]
1747 #[cfg(feature = "server")]
1748 fn runner_normalize_languages() {
1749 assert!(RunnerSettings::normalize_languages("").is_none());
1750 assert!(RunnerSettings::normalize_languages(" ,, ").is_none());
1751 assert_eq!(
1752 RunnerSettings::normalize_languages("Python, Node, RUST").as_deref(),
1753 Some("python,node,rust")
1754 );
1755 assert_eq!(
1756 RunnerSettings::normalize_languages(" go ").as_deref(),
1757 Some("go")
1758 );
1759 }
1760}