|
| 1 | +//! Ruby 原生六类别研究档案的严格读者;不启动原生进程,也不注册已验收能力。 |
| 2 | +use crate::{parse_unique_json, ruby_candidate_profile::RubyCandidateProfile}; |
| 3 | +use codeguard_core::{CANDIDATE_PLATFORMS, CHECK_CATEGORIES}; |
| 4 | +use std::collections::BTreeSet; |
| 5 | + |
| 6 | +/// 读取内置 Ruby 档案并核对六类别、工具对应、方言、版本和条件;返回研究档案或错误。 |
| 7 | +pub fn bundled_ruby_candidate_profile() -> Result<RubyCandidateProfile, String> { |
| 8 | + parse_ruby_candidate_profile(include_str!( |
| 9 | + "../../../rulepacks/ruby_static_candidate_v1.json" |
| 10 | + )) |
| 11 | +} |
| 12 | +/// 消费外部候选 JSON;最多 64KiB,拒绝重复键、未知字段、类别缺项及虚报能力。 |
| 13 | +pub fn parse_ruby_candidate_profile(raw: &str) -> Result<RubyCandidateProfile, String> { |
| 14 | + if raw.len() > 64 * 1024 { |
| 15 | + return Err("Ruby 候选档案超出字节预算".into()); |
| 16 | + } |
| 17 | + let value = parse_unique_json(raw.as_bytes()).map_err(str::to_string)?; |
| 18 | + let profile: RubyCandidateProfile = serde_json::from_value(value).map_err(|e| e.to_string())?; |
| 19 | + if profile.schema_version != "1.0.0" |
| 20 | + || profile.language != "ruby" |
| 21 | + || profile.runtime_validation != "unverified" |
| 22 | + || profile.platform_validation != "unverified" |
| 23 | + || !exact_set(&profile.runtime_dialects, &["mri", "jruby", "truffleruby"]) |
| 24 | + || !exact_set(&profile.platforms, &CANDIDATE_PLATFORMS) |
| 25 | + || !exact_set( |
| 26 | + &profile.target_version_inputs, |
| 27 | + &[ |
| 28 | + ".ruby-version", |
| 29 | + ".tool-versions", |
| 30 | + "mise.toml", |
| 31 | + "Gemfile", |
| 32 | + "Gemfile.lock", |
| 33 | + "*.gemspec", |
| 34 | + ".rubocop.yml", |
| 35 | + ], |
| 36 | + ) |
| 37 | + || !valid_gaps(&profile.version_gaps) |
| 38 | + { |
| 39 | + return Err("Ruby 档案版本、方言、版本输入或平台集合未验证/缺失".into()); |
| 40 | + } |
| 41 | + let names: Vec<String> = profile |
| 42 | + .categories |
| 43 | + .iter() |
| 44 | + .map(|s| s.category.clone()) |
| 45 | + .collect(); |
| 46 | + if !exact_set(&names, &CHECK_CATEGORIES) { |
| 47 | + return Err("Ruby 档案必须恰有六个不重复类别".into()); |
| 48 | + } |
| 49 | + for slot in &profile.categories { |
| 50 | + let expected: &[&str] = match slot.category.as_str() { |
| 51 | + "lint" => &["rubocop"], |
| 52 | + "comments" => &["rubocop_documentation", "rubocop_documentation_method"], |
| 53 | + "dependencies" => &["bundle_list"], |
| 54 | + "cve" => &["bundler_audit"], |
| 55 | + "security" => &["rubocop_security", "brakeman"], |
| 56 | + "build" => &["gem_build"], |
| 57 | + _ => return Err("Ruby 类别未知".into()), |
| 58 | + }; |
| 59 | + let ids: Vec<String> = slot.tools.iter().map(|t| t.tool_id.clone()).collect(); |
| 60 | + if slot.status != "gap" |
| 61 | + || !exact_set(&ids, expected) |
| 62 | + || slot.applicability |
| 63 | + != if slot.category == "build" { |
| 64 | + "project_dependent" |
| 65 | + } else { |
| 66 | + "applicable" |
| 67 | + } |
| 68 | + || slot.requires_database_freshness != (slot.category == "cve") |
| 69 | + || !valid_gaps(&slot.coverage_gaps) |
| 70 | + { |
| 71 | + return Err(format!( |
| 72 | + "Ruby {} 的类别适用性、工具或缺口无效", |
| 73 | + slot.category |
| 74 | + )); |
| 75 | + } |
| 76 | + for tool in &slot.tools { |
| 77 | + let (argv, scope): (&[&str], &str) = match tool.tool_id.as_str() { |
| 78 | + "rubocop" => ( |
| 79 | + &["bundle", "exec", "rubocop", "--format", "json"], |
| 80 | + "ruby_project", |
| 81 | + ), |
| 82 | + "rubocop_documentation" => ( |
| 83 | + &[ |
| 84 | + "bundle", |
| 85 | + "exec", |
| 86 | + "rubocop", |
| 87 | + "--only", |
| 88 | + "Style/Documentation", |
| 89 | + "--format", |
| 90 | + "json", |
| 91 | + ], |
| 92 | + "ruby_project", |
| 93 | + ), |
| 94 | + "rubocop_documentation_method" => ( |
| 95 | + &[ |
| 96 | + "bundle", |
| 97 | + "exec", |
| 98 | + "rubocop", |
| 99 | + "--only", |
| 100 | + "Style/DocumentationMethod", |
| 101 | + "--format", |
| 102 | + "json", |
| 103 | + ], |
| 104 | + "ruby_project", |
| 105 | + ), |
| 106 | + "bundle_list" => (&["bundle", "list"], "bundler_project"), |
| 107 | + "bundler_audit" => ( |
| 108 | + &["bundle-audit", "check", "--no-update", "--format", "json"], |
| 109 | + "bundler_project", |
| 110 | + ), |
| 111 | + "rubocop_security" => ( |
| 112 | + &[ |
| 113 | + "bundle", "exec", "rubocop", "--only", "Security", "--format", "json", |
| 114 | + ], |
| 115 | + "ruby_project", |
| 116 | + ), |
| 117 | + "brakeman" => ( |
| 118 | + &[ |
| 119 | + "bundle", |
| 120 | + "exec", |
| 121 | + "brakeman", |
| 122 | + "--format", |
| 123 | + "json", |
| 124 | + "--no-pager", |
| 125 | + ], |
| 126 | + "rails_project", |
| 127 | + ), |
| 128 | + "gem_build" => (&["gem", "build", "SELECTED_GEMSPEC"], "gem_project"), |
| 129 | + _ => return Err("Ruby 工具未知".into()), |
| 130 | + }; |
| 131 | + if tool.candidate_version != "project_locked" |
| 132 | + || tool.version_status != "unvalidated_project_lock_required" |
| 133 | + || tool.scope != scope |
| 134 | + || tool |
| 135 | + .candidate_argv |
| 136 | + .iter() |
| 137 | + .map(String::as_str) |
| 138 | + .collect::<Vec<_>>() |
| 139 | + != argv |
| 140 | + || tool.sources.is_empty() |
| 141 | + || tool.sources.len() > 8 |
| 142 | + || tool.sources.iter().any(|s| { |
| 143 | + !s.starts_with("https://") |
| 144 | + || s.len() > 512 |
| 145 | + || s.bytes().any(|b| b.is_ascii_whitespace()) |
| 146 | + }) |
| 147 | + { |
| 148 | + return Err(format!( |
| 149 | + "Ruby {} 的版本、命令、范围或来源无效", |
| 150 | + tool.tool_id |
| 151 | + )); |
| 152 | + } |
| 153 | + } |
| 154 | + } |
| 155 | + Ok(profile) |
| 156 | +} |
| 157 | +fn exact_set(actual: &[String], expected: &[&str]) -> bool { |
| 158 | + actual.len() == expected.len() |
| 159 | + && actual.iter().map(String::as_str).collect::<BTreeSet<_>>() |
| 160 | + == expected.iter().copied().collect() |
| 161 | +} |
| 162 | +fn valid_gaps(gaps: &[String]) -> bool { |
| 163 | + !gaps.is_empty() |
| 164 | + && gaps.len() <= 32 |
| 165 | + && gaps.iter().collect::<BTreeSet<_>>().len() == gaps.len() |
| 166 | + && gaps.iter().all(|s| { |
| 167 | + !s.is_empty() |
| 168 | + && s.len() <= 256 |
| 169 | + && s.bytes() |
| 170 | + .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_') |
| 171 | + }) |
| 172 | +} |
0 commit comments