1use std::collections::{HashMap, HashSet};
2use std::fmt;
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug)]
9pub struct CompFile {
10 pub file: PathBuf,
11 pub lines: Vec<String>,
12 pub start: usize,
13}
14
15#[derive(Hash, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
19pub struct Match {
20 pub file: PathBuf,
21 pub line: usize,
22 pub size: usize,
23}
24
25pub struct Matches(pub HashMap<Match, Vec<Match>>);
30
31pub struct MatchesLookup(pub HashMap<Match, Match>);
35
36#[derive(Serialize, Clone, Deserialize, PartialEq, Eq, Debug)]
37pub struct JsonFileInfo {
38 pub count_blocks: usize,
39}
40
41#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug)]
42pub struct JsonBlockInfo {
43 pub starting_line: usize,
44 pub block_length: usize,
45}
46
47#[derive(Serialize, Deserialize, Eq, Debug)]
48pub struct JsonMatch {
49 pub files: HashMap<PathBuf, JsonFileInfo>,
50 pub blocks: HashMap<PathBuf, Vec<JsonBlockInfo>>,
51}
52
53#[derive(Serialize, Deserialize, Eq, Debug)]
54pub struct JsonRoot {
55 pub version: String,
56 pub files: HashMap<PathBuf, JsonFileInfo>,
57 pub matches: Vec<JsonMatch>,
58}
59
60impl From<&Match> for JsonBlockInfo {
61 fn from(m: &Match) -> Self {
62 Self {
63 starting_line: m.line,
64 block_length: m.size,
65 }
66 }
67}
68
69impl PartialEq for JsonMatch {
70 fn eq(&self, other: &Self) -> bool {
71 if self.files != other.files || self.blocks.len() != other.blocks.len() {
72 return false;
73 }
74
75 for (k, v) in &self.blocks {
76 match other.blocks.get(k) {
77 Some(other_v) => {
78 let (a_info, b_info): (HashSet<_>, HashSet<_>) =
79 (v.iter().collect(), other_v.iter().collect());
80 if a_info != b_info {
81 return false;
82 }
83 }
84 None => {
85 return false;
86 }
87 }
88 }
89
90 true
91 }
92}
93
94impl From<(&Match, &Vec<Match>)> for JsonMatch {
95 fn from((initial_match, other_matches): (&Match, &Vec<Match>)) -> Self {
96 let mut blocks = HashMap::new();
97 let mut files = HashMap::new();
98 files.insert(initial_match.file.clone(), JsonFileInfo { count_blocks: 1 });
99 blocks.insert(
100 initial_match.file.clone(),
101 vec![JsonBlockInfo::from(initial_match)],
102 );
103
104 for m in other_matches {
105 let f = m.clone().file;
106 files
107 .entry(f.clone())
108 .and_modify(|info| info.count_blocks += 1)
109 .or_insert(JsonFileInfo { count_blocks: 1 });
110 blocks
111 .entry(f)
112 .and_modify(|v| v.push(JsonBlockInfo::from(m)))
113 .or_insert(vec![JsonBlockInfo::from(m)]);
114 }
115
116 Self { files, blocks }
117 }
118}
119
120impl fmt::Display for JsonMatch {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 write!(
123 f,
124 "=== MATCH ===\n{}\n",
125 self.blocks
126 .iter()
127 .map(|(filename, infos)| format!(
128 "File: {}\nLines: {:?}\nSize: {}",
129 filename.display(),
130 infos
131 .iter()
132 .map(|info| info.starting_line)
133 .collect::<Vec<usize>>(),
134 infos[0].block_length,
135 ))
136 .collect::<Vec<String>>()
137 .join("\n---\n")
138 )
139 }
140}
141
142impl From<&Matches> for JsonRoot {
143 fn from(m: &Matches) -> Self {
144 let version = clap::crate_version!().to_owned();
145 let matches: Vec<JsonMatch> = m.0.iter().map(JsonMatch::from).collect();
146 let jm_files = matches.iter().map(|jm| jm.files.clone());
147 let mut files: HashMap<PathBuf, JsonFileInfo> = HashMap::new();
148
149 for jmf in jm_files {
150 for (filename, info) in jmf {
151 files
152 .entry(filename)
153 .and_modify(|v| v.count_blocks += info.count_blocks)
154 .or_insert(info);
155 }
156 }
157
158 Self {
159 version,
160 files,
161 matches,
162 }
163 }
164}
165
166impl JsonRoot {
167 pub fn unique_matches(&self) -> usize {
168 self.matches.len()
169 }
170
171 pub fn json(&self) -> String {
172 serde_json::to_string(&self).unwrap_or("{}".to_owned())
173 }
174}
175
176impl PartialEq for JsonRoot {
177 fn eq(&self, other: &Self) -> bool {
178 if self.matches.len() != other.matches.len() {
179 return false;
180 }
181
182 for item in &self.matches {
183 if !other.matches.contains(item) {
184 return false;
185 }
186 }
187
188 true
189 }
190}
191
192impl fmt::Display for JsonRoot {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 write!(
195 f,
196 "{}",
197 self.matches
198 .iter()
199 .map(|x| x.to_string())
200 .collect::<Vec<String>>()
201 .join("\n")
202 )
203 }
204}
205
206pub type ComparisonFn = Box<dyn Fn(&String, &String) -> bool>;
207pub type FileCache = HashMap<PathBuf, Vec<String>>;
208
209impl Match {
210 pub fn from_compfiles(f1: &CompFile, f2: &CompFile, block_length: usize) -> (Self, Self) {
211 (
212 Self {
213 file: f1.file.clone(),
214 line: f1.start + 1,
215 size: block_length,
216 },
217 Self {
218 file: f2.file.clone(),
219 line: f2.start + 1,
220 size: block_length,
221 },
222 )
223 }
224}
225
226fn get_lines_from_file(file: &PathBuf) -> std::io::Result<Vec<String>> {
227 Ok(std::fs::read_to_string(file)?
228 .split('\n')
229 .map(|line| line.trim().to_owned())
230 .collect::<Vec<String>>())
231}
232
233impl CompFile {
234 pub fn current_line(&self) -> &String {
235 &self.lines[self.start]
236 }
237
238 pub fn from_files(f1: &PathBuf, f2: &PathBuf) -> Option<(Self, Self)> {
239 match (get_lines_from_file(f1), get_lines_from_file(f2)) {
240 (Ok(lines1), Ok(lines2)) => Some((
241 Self {
242 file: f1.clone(),
243 lines: lines1,
244 start: 0,
245 },
246 Self {
247 file: f2.clone(),
248 lines: lines2,
249 start: 0,
250 },
251 )),
252 _ => None,
253 }
254 }
255}