1use std::collections::{BTreeSet, HashSet};
27
28use rusqlite::Connection;
29
30use crate::database::{has_column, quote};
31use crate::error::Result;
32use crate::schema::Schema;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
36pub struct Generation {
37 pub csp: &'static str,
40 pub first_seen: &'static str,
42 pub markers: &'static [(&'static str, &'static str)],
44}
45
46pub const GENERATIONS: &[Generation] = &[
49 Generation {
50 csp: "5.1",
51 first_seen: "2026-10",
52 markers: &[("Layer", "Has1_0_19"), ("BrushStyle", "WaterEdgeStyle")],
53 },
54 Generation { csp: "5.0", first_seen: "2026-05", markers: &[("Layer", "Has1_0_18"), ("BrushStyle", "BlurKind")] },
55 Generation { csp: "4.2", first_seen: "2025-12", markers: &[("Layer", "Has1_0_15"), ("Canvas", "TimeLapseCodec")] },
56 Generation {
57 csp: "3.0-4.1",
58 first_seen: "2024-12",
59 markers: &[("Layer", "Has1_0_12"), ("Canvas", "CanvasWorkTime")],
60 },
61 Generation {
62 csp: "2.1-2.x",
63 first_seen: "2024-01",
64 markers: &[("Layer", "Has1_0_8"), ("BrushStyle", "BrushColorMixingMode")],
65 },
66 Generation { csp: "2.0", first_seen: "2023-04", markers: &[("Layer", "WebCooperationLayerEditable")] },
67 Generation {
68 csp: "1.12-1.13",
69 first_seen: "2022-10",
70 markers: &[("Layer", "Has1_0_7"), ("CameraNodeInfo", "CameraNodeUUID")],
71 },
72 Generation {
73 csp: "1.11-1.12",
74 first_seen: "2022-02",
75 markers: &[("Layer", "HasPrimitive"), ("PrimitiveInfo", "PrimitiveUUID")],
76 },
77 Generation { csp: "1.10.10-1.11", first_seen: "2021-06", markers: &[("BrushStyle", "DualStyleIndex")] },
78 Generation { csp: "1.10.5-1.10.9", first_seen: "2020-12", markers: &[("Canvas", "TimeLapseManagerIndex")] },
79 Generation { csp: "1.8-1.10.3", first_seen: "2020-10", markers: &[("Layer", "TextLayerVersion")] },
80];
81
82pub const RETIRED_LABELS: &[(&str, &str, &str)] = &[("CameraInfo", "CameraZoomWithDolly", "3.0-4.1")];
86
87const KNOWN_SCHEME: &str = include_str!("data/param_scheme.tsv");
88
89const CMC_LABELS: &[&str] = &[
91 "ComicBindPosition",
92 "ComicCoverExist",
93 "ComicShowStyle",
94 "ComicStartPage",
95 "ComicWebtoonWrapDirection",
96 "Cover1Prefix",
97 "Cover1Suffix",
98 "Cover4Prefix",
99 "Cover4Suffix",
100 "MaxCanvasFileIndex",
101 "PrintingCompanyID",
102 "SentencePrefix",
103 "SentenceSuffix",
104];
105
106#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
108pub struct VersionInfo {
109 pub saved_by: Option<Generation>,
111 pub created_by: Option<Generation>,
115 pub resaved: bool,
119 pub format_level: Option<u32>,
122 pub locking_features: Vec<String>,
127 pub needs_format_level: Option<u32>,
130 pub unknown_labels: Vec<String>,
132 pub alternative_lock_columns: bool,
134 pub sqlite_version: Option<u32>,
136}
137
138impl VersionInfo {
139 pub fn read(connection: &Connection, image: Option<&[u8]>) -> Result<Self> {
142 let schema = Schema::read(connection)?;
143 let labels: HashSet<(&str, &str)> =
144 schema.params.iter().map(|param| (param.table.as_str(), param.column.as_str())).collect();
145 let saved_by = generation_of(&labels);
146 let blocks = registration_blocks(&schema);
147 let first: HashSet<(&str, &str)> = blocks
148 .first()
149 .map(|block| {
150 block.iter().map(|&i| (schema.params[i].table.as_str(), schema.params[i].column.as_str())).collect()
151 })
152 .unwrap_or_default();
153 let mut created_by = generation_of(&first);
154 let mut resaved = blocks.len() > 1;
155 if retired_label_kept(&labels, saved_by) {
156 resaved = true;
157 if created_by == saved_by {
158 created_by = None;
159 }
160 }
161 let format_level =
162 schema.params.iter().filter(|p| p.table == "Layer").filter_map(|p| has_level(&p.column)).max();
163
164 let mut locking_features = Vec::new();
165 let mut needs_format_level = None;
166 for param in schema.params.iter().filter(|p| p.lock_type == 4 && p.flag == 0 && p.link_table.is_none()) {
167 if !has_column(connection, ¶m.table, ¶m.column)? {
168 continue;
169 }
170 let sql = format!(
171 "SELECT COUNT(*) FROM {} WHERE {} IS NOT NULL AND {} != 0",
172 quote(¶m.table),
173 quote(¶m.column),
174 quote(¶m.column)
175 );
176 let set: i64 = connection.query_row(&sql, [], |row| row.get(0))?;
177 if set > 0 {
178 locking_features.push(format!("{}.{}", param.table, param.column));
179 if let Some(level) = has_level(¶m.column) {
180 needs_format_level = needs_format_level.max(Some(level));
181 }
182 }
183 }
184
185 let known = known_labels();
186 let unknown_labels: BTreeSet<String> = schema
187 .params
188 .iter()
189 .filter(|p| !known.contains(&(p.table.clone(), p.column.clone())))
190 .filter(|p| !RETIRED_LABELS.iter().any(|&(t, c, _)| (t, c) == (p.table.as_str(), p.column.as_str())))
191 .map(|p| format!("{}.{}", p.table, p.column))
192 .collect();
193 let alternative_lock_columns = has_column(connection, "ParamScheme", "AlternativeLockType")?;
194 let sqlite_version = image
195 .filter(|bytes| bytes.len() >= 100 && bytes.starts_with(b"SQLite format 3\0"))
196 .map(|bytes| u32::from_be_bytes([bytes[96], bytes[97], bytes[98], bytes[99]]));
197 Ok(VersionInfo {
198 saved_by,
199 created_by,
200 resaved,
201 format_level,
202 locking_features,
203 needs_format_level,
204 unknown_labels: unknown_labels.into_iter().collect(),
205 alternative_lock_columns,
206 sqlite_version,
207 })
208 }
209}
210
211pub fn generation_of(labels: &HashSet<(&str, &str)>) -> Option<Generation> {
213 GENERATIONS.iter().copied().find(|generation| generation.markers.iter().all(|marker| labels.contains(marker)))
214}
215
216fn retired_label_kept(labels: &HashSet<(&str, &str)>, saved_by: Option<Generation>) -> bool {
219 let Some(saved_by) = saved_by else { return false };
220 let age = |csp: &str| GENERATIONS.iter().position(|g| g.csp == csp);
221 RETIRED_LABELS
222 .iter()
223 .any(|&(table, column, retired_in)| labels.contains(&(table, column)) && age(saved_by.csp) <= age(retired_in))
224}
225
226fn has_level(column: &str) -> Option<u32> {
228 let rest = column.strip_prefix("Has1_0_").or_else(|| column.strip_prefix("LayerEffectHas1_0_"))?;
229 rest.parse().ok()
230}
231
232fn registration_blocks(schema: &Schema) -> Vec<Vec<usize>> {
236 let mut blocks: Vec<Vec<usize>> = vec![Vec::new()];
237 let mut seen: HashSet<&str> = HashSet::new();
238 let mut last: Option<&str> = None;
239 for (index, param) in schema.params.iter().enumerate() {
240 let table = param.table.as_str();
241 if last != Some(table) {
242 if seen.contains(table) {
243 blocks.push(Vec::new());
244 seen.clear();
245 }
246 seen.insert(table);
247 }
248 blocks.last_mut().expect("one block").push(index);
249 last = Some(table);
250 }
251 blocks.retain(|block| !block.is_empty());
252 blocks
253}
254
255fn known_labels() -> HashSet<(String, String)> {
257 let mut known: HashSet<(String, String)> = KNOWN_SCHEME
258 .lines()
259 .filter_map(|line| {
260 let mut fields = line.split('\t');
261 Some((fields.next()?.to_owned(), fields.next()?.to_owned()))
262 })
263 .collect();
264 known.extend(CMC_LABELS.iter().map(|label| ("Project".to_owned(), (*label).to_owned())));
265 known
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 fn labels<'a>(items: &[(&'a str, &'a str)]) -> HashSet<(&'a str, &'a str)> {
273 items.iter().copied().collect()
274 }
275
276 #[test]
277 fn generation_needs_every_marker() {
278 let only_level = labels(&[("Layer", "Has1_0_19")]);
279 assert_eq!(generation_of(&only_level), None);
280 let both = labels(&[("Layer", "Has1_0_19"), ("BrushStyle", "WaterEdgeStyle")]);
281 assert_eq!(generation_of(&both).map(|g| g.csp), Some("5.1"));
282 let older = labels(&[("Layer", "WebCooperationLayerEditable"), ("Canvas", "TimeLapseManagerIndex")]);
283 assert_eq!(generation_of(&older).map(|g| g.csp), Some("2.0"));
284 }
285
286 #[test]
287 fn has_levels() {
288 assert_eq!(has_level("Has1_0_19"), Some(19));
289 assert_eq!(has_level("LayerEffectHas1_0_19"), Some(19));
290 assert_eq!(has_level("HasPrimitive"), None);
291 }
292
293 #[test]
294 fn new_files_are_the_newest_generation() {
295 let file = crate::ClipFile::create(&crate::NewCanvas { width: 16, height: 16, ..Default::default() }).unwrap();
296 let info = file.version_info().unwrap();
297 assert_eq!(info.saved_by.map(|g| g.csp), Some(GENERATIONS[0].csp));
298 assert_eq!(info.created_by, info.saved_by);
299 assert!(!info.resaved);
300 assert_eq!(info.format_level, Some(19));
301 assert!(info.locking_features.is_empty());
302 assert!(info.unknown_labels.is_empty());
303 }
304
305 #[test]
306 fn appended_rows_and_set_markers() {
307 let file = crate::ClipFile::create(&crate::NewCanvas { width: 16, height: 16, ..Default::default() }).unwrap();
308 let db = file.database();
309 db.execute(
310 "INSERT INTO ParamScheme(TableName, LabelName, DataType, Flag, OwnerType, LockType, LockSpecified, LinkTable) \
311 VALUES ('Layer', 'Has1_0_20', 1, 0, 0, 4, 2, '')",
312 [],
313 )
314 .unwrap();
315 db.execute("ALTER TABLE Layer ADD COLUMN Has1_0_20 INTEGER DEFAULT NULL", []).unwrap();
316 db.execute("UPDATE Layer SET Has1_0_20 = 1 WHERE MainId = (SELECT MAX(MainId) FROM Layer)", []).unwrap();
317 let info = file.version_info().unwrap();
318 assert!(info.resaved, "a Layer row after other tables starts a new block");
319 assert_eq!(info.format_level, Some(20));
320 assert_eq!(info.locking_features, vec!["Layer.Has1_0_20".to_owned()]);
321 assert_eq!(info.needs_format_level, Some(20));
322 assert_eq!(info.unknown_labels, vec!["Layer.Has1_0_20".to_owned()]);
323 }
324
325 #[test]
326 fn retired_label_marks_a_rewritten_older_file() {
327 let file = crate::ClipFile::create(&crate::NewCanvas { width: 16, height: 16, ..Default::default() }).unwrap();
328 file.database()
330 .execute(
331 "INSERT INTO ParamScheme(TableName, LabelName, DataType, Flag, OwnerType, LockType, LockSpecified, LinkTable) \
332 VALUES ('CameraInfo', 'CameraZoomWithDolly', 1, 0, 0, 0, 0, '')",
333 [],
334 )
335 .unwrap();
336 let info = file.version_info().unwrap();
337 assert!(info.resaved);
338 assert_eq!(info.created_by, None);
339 assert!(info.unknown_labels.is_empty());
340
341 let old = labels(&[
342 ("Layer", "Has1_0_8"),
343 ("BrushStyle", "BrushColorMixingMode"),
344 ("CameraInfo", "CameraZoomWithDolly"),
345 ]);
346 assert!(!retired_label_kept(&old, generation_of(&old)), "2.x still registered it");
347 }
348
349 #[test]
350 fn newest_known_scheme_is_the_newest_generation() {
351 let known = known_labels();
352 let refs: HashSet<(&str, &str)> = known.iter().map(|(t, c)| (t.as_str(), c.as_str())).collect();
353 assert_eq!(generation_of(&refs).map(|g| g.csp), Some(GENERATIONS[0].csp));
354 }
355}