1
//! This module contains serialization and deserialization logic for
2
//! editing database privileges in a text editor.
3

            
4
use super::base::{
5
    DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeRow, db_priv_field_human_readable_name,
6
};
7
use crate::core::{
8
    common::{rev_yn, yn},
9
    types::{MySQLDatabase, MySQLUser},
10
};
11
use anyhow::{Context, anyhow};
12
use itertools::Itertools;
13
use std::{
14
    cmp::max,
15
    collections::{HashMap, HashSet},
16
};
17

            
18
/// Generates a single row of the privileges table for the editor.
19
#[must_use]
20
6
pub fn format_privileges_line_for_editor(
21
6
    privs: &DatabasePrivilegeRow,
22
6
    database_name_len: usize,
23
6
    username_len: usize,
24
6
) -> String {
25
    DATABASE_PRIVILEGE_FIELDS
26
6
        .into_iter()
27
96
        .map(|field| match field {
28
96
            "Db" => format!("{:width$}", privs.db, width = database_name_len),
29
90
            "User" => format!("{:width$}", privs.user, width = username_len),
30
84
            privilege => format!(
31
                "{:width$}",
32
                // SAFETY: unwrap is safe here because the field names are static
33
84
                yn(privs.get_privilege_by_name(privilege).unwrap()),
34
84
                width = db_priv_field_human_readable_name(privilege).len()
35
            ),
36
96
        })
37
6
        .join(" ")
38
6
        .trim()
39
6
        .to_string()
40
6
}
41

            
42
const EDITOR_COMMENT: &str = r"
43
# Welcome to the privilege editor.
44
# Each line defines what privileges a single user has on a single database.
45
# The first two columns respectively represent the database name and the user, and the remaining columns are the privileges.
46
# If the user should have a certain privilege, write 'Y', otherwise write 'N'.
47
#
48
# Lines starting with '#' are comments and will be ignored.
49
";
50

            
51
/// Generates the content for the privilege editor.
52
///
53
/// The unix user is used in case there are no privileges to edit,
54
/// so that the user can see an example line based on their username.
55
2
pub fn generate_editor_content_from_privilege_data(
56
2
    privilege_data: &[DatabasePrivilegeRow],
57
2
    unix_user: &str,
58
2
    database_name: Option<&MySQLDatabase>,
59
2
) -> String {
60
2
    let example_user = format!("{unix_user}_user");
61
2
    let example_db = database_name
62
2
        .unwrap_or(&format!("{unix_user}_db").into())
63
2
        .to_string();
64

            
65
    // NOTE: `.max()`` fails when the iterator is empty.
66
    //       In this case, we know that the only fields in the
67
    //       editor will be the example user and example db name.
68
    //       Hence, it's put as the fallback value, despite not really
69
    //       being a "fallback" in the normal sense.
70
2
    let longest_username = max(
71
2
        privilege_data
72
2
            .iter()
73
4
            .map(|p| p.user.len())
74
2
            .max()
75
2
            .unwrap_or(example_user.len()),
76
2
        "User".len(),
77
    );
78

            
79
2
    let longest_database_name = max(
80
2
        privilege_data
81
2
            .iter()
82
4
            .map(|p| p.db.len())
83
2
            .max()
84
2
            .unwrap_or(example_db.len()),
85
2
        "Database".len(),
86
    );
87

            
88
2
    let mut header: Vec<_> = DATABASE_PRIVILEGE_FIELDS
89
2
        .into_iter()
90
2
        .map(db_priv_field_human_readable_name)
91
2
        .collect();
92

            
93
    // Pad the first two columns with spaces to align the privileges.
94
2
    header[0] = format!("{:width$}", header[0], width = longest_database_name);
95
2
    header[1] = format!("{:width$}", header[1], width = longest_username);
96

            
97
2
    let example_line = format_privileges_line_for_editor(
98
2
        &DatabasePrivilegeRow {
99
2
            db: example_db.into(),
100
2
            user: example_user.into(),
101
2
            select_priv: true,
102
2
            insert_priv: true,
103
2
            update_priv: true,
104
2
            delete_priv: true,
105
2
            create_priv: false,
106
2
            drop_priv: false,
107
2
            alter_priv: false,
108
2
            index_priv: false,
109
2
            create_tmp_table_priv: false,
110
2
            lock_tables_priv: false,
111
2
            references_priv: false,
112
2
            create_view_priv: false,
113
2
            show_view_priv: false,
114
2
            trigger_priv: false,
115
2
        },
116
2
        longest_database_name,
117
2
        longest_username,
118
    );
119

            
120
2
    format!(
121
        "{}\n{}\n{}",
122
        EDITOR_COMMENT,
123
2
        header.join(" "),
124
2
        if privilege_data.is_empty() {
125
            format!("# {example_line}")
126
        } else {
127
2
            privilege_data
128
2
                .iter()
129
4
                .map(|privs| {
130
4
                    format_privileges_line_for_editor(
131
4
                        privs,
132
4
                        longest_database_name,
133
4
                        longest_username,
134
                    )
135
4
                })
136
2
                .join("\n")
137
        }
138
    )
139
2
}
140

            
141
#[derive(Debug)]
142
enum PrivilegeRowParseResult {
143
    PrivilegeRow(DatabasePrivilegeRow),
144
    ParserError(anyhow::Error),
145
    TooFewFields(usize),
146
    TooManyFields(usize),
147
    Header,
148
    Comment,
149
    Empty,
150
}
151

            
152
#[inline]
153
155
fn parse_privilege_cell_from_editor(yn: &str, name: &str) -> anyhow::Result<bool> {
154
155
    let human_readable_name = db_priv_field_human_readable_name(name);
155
155
    rev_yn(yn)
156
155
        .ok_or_else(|| anyhow!("Expected Y or N, found {yn}"))
157
155
        .context(format!("Could not parse '{human_readable_name}' privilege"))
158
155
}
159

            
160
#[inline]
161
13
fn editor_row_is_header(row: &str) -> bool {
162
13
    row.split_ascii_whitespace()
163
13
        .zip(DATABASE_PRIVILEGE_FIELDS.iter())
164
28
        .map(|(field, priv_name)| (field, db_priv_field_human_readable_name(priv_name)))
165
28
        .all(|(field, header_field)| field == header_field)
166
13
}
167

            
168
/// Parse a single row of the privileges table from the editor.
169
28
fn parse_privilege_row_from_editor(row: &str) -> PrivilegeRowParseResult {
170
28
    if row.starts_with('#') || row.starts_with("//") {
171
8
        return PrivilegeRowParseResult::Comment;
172
20
    }
173

            
174
20
    if row.trim().is_empty() {
175
5
        return PrivilegeRowParseResult::Empty;
176
15
    }
177

            
178
15
    let parts: Vec<&str> = row.trim().split_ascii_whitespace().collect();
179

            
180
15
    match parts.len() {
181
15
        n if (n < DATABASE_PRIVILEGE_FIELDS.len()) => {
182
1
            return PrivilegeRowParseResult::TooFewFields(n);
183
        }
184
14
        n if (n > DATABASE_PRIVILEGE_FIELDS.len()) => {
185
1
            return PrivilegeRowParseResult::TooManyFields(n);
186
        }
187
13
        _ => {}
188
    }
189

            
190
13
    if editor_row_is_header(row) {
191
1
        return PrivilegeRowParseResult::Header;
192
12
    }
193

            
194
11
    let row = DatabasePrivilegeRow {
195
12
        db: (*parts.first().unwrap()).into(),
196
12
        user: (*parts.get(1).unwrap()).into(),
197
12
        select_priv: match parse_privilege_cell_from_editor(
198
12
            parts.get(2).unwrap(),
199
12
            DATABASE_PRIVILEGE_FIELDS[2],
200
12
        ) {
201
11
            Ok(p) => p,
202
1
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
203
        },
204
11
        insert_priv: match parse_privilege_cell_from_editor(
205
11
            parts.get(3).unwrap(),
206
11
            DATABASE_PRIVILEGE_FIELDS[3],
207
11
        ) {
208
11
            Ok(p) => p,
209
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
210
        },
211
11
        update_priv: match parse_privilege_cell_from_editor(
212
11
            parts.get(4).unwrap(),
213
11
            DATABASE_PRIVILEGE_FIELDS[4],
214
11
        ) {
215
11
            Ok(p) => p,
216
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
217
        },
218
11
        delete_priv: match parse_privilege_cell_from_editor(
219
11
            parts.get(5).unwrap(),
220
11
            DATABASE_PRIVILEGE_FIELDS[5],
221
11
        ) {
222
11
            Ok(p) => p,
223
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
224
        },
225
11
        create_priv: match parse_privilege_cell_from_editor(
226
11
            parts.get(6).unwrap(),
227
11
            DATABASE_PRIVILEGE_FIELDS[6],
228
11
        ) {
229
11
            Ok(p) => p,
230
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
231
        },
232
11
        drop_priv: match parse_privilege_cell_from_editor(
233
11
            parts.get(7).unwrap(),
234
11
            DATABASE_PRIVILEGE_FIELDS[7],
235
11
        ) {
236
11
            Ok(p) => p,
237
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
238
        },
239
11
        alter_priv: match parse_privilege_cell_from_editor(
240
11
            parts.get(8).unwrap(),
241
11
            DATABASE_PRIVILEGE_FIELDS[8],
242
11
        ) {
243
11
            Ok(p) => p,
244
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
245
        },
246
11
        index_priv: match parse_privilege_cell_from_editor(
247
11
            parts.get(9).unwrap(),
248
11
            DATABASE_PRIVILEGE_FIELDS[9],
249
11
        ) {
250
11
            Ok(p) => p,
251
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
252
        },
253
11
        create_tmp_table_priv: match parse_privilege_cell_from_editor(
254
11
            parts.get(10).unwrap(),
255
11
            DATABASE_PRIVILEGE_FIELDS[10],
256
11
        ) {
257
11
            Ok(p) => p,
258
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
259
        },
260
11
        lock_tables_priv: match parse_privilege_cell_from_editor(
261
11
            parts.get(11).unwrap(),
262
11
            DATABASE_PRIVILEGE_FIELDS[11],
263
11
        ) {
264
11
            Ok(p) => p,
265
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
266
        },
267
11
        references_priv: match parse_privilege_cell_from_editor(
268
11
            parts.get(12).unwrap(),
269
11
            DATABASE_PRIVILEGE_FIELDS[12],
270
11
        ) {
271
11
            Ok(p) => p,
272
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
273
        },
274
11
        create_view_priv: match parse_privilege_cell_from_editor(
275
11
            parts.get(13).unwrap(),
276
11
            DATABASE_PRIVILEGE_FIELDS[13],
277
11
        ) {
278
11
            Ok(p) => p,
279
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
280
        },
281
11
        show_view_priv: match parse_privilege_cell_from_editor(
282
11
            parts.get(14).unwrap(),
283
11
            DATABASE_PRIVILEGE_FIELDS[14],
284
11
        ) {
285
11
            Ok(p) => p,
286
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
287
        },
288
11
        trigger_priv: match parse_privilege_cell_from_editor(
289
11
            parts.get(15).unwrap(),
290
11
            DATABASE_PRIVILEGE_FIELDS[15],
291
11
        ) {
292
11
            Ok(p) => p,
293
            Err(e) => return PrivilegeRowParseResult::ParserError(e),
294
        },
295
    };
296

            
297
11
    PrivilegeRowParseResult::PrivilegeRow(row)
298
28
}
299

            
300
#[derive(Debug, Clone)]
301
pub struct PrivilegeLineError {
302
    pub line_number: usize,
303
    pub message: String,
304
}
305

            
306
/// Parse the content of the privilege editor into a list of privilege rows.
307
///
308
/// Lines containing errors will not be added to the list, but rather will be
309
/// collected as [`PrivilegeLineError`]s in the second element of the tuple.
310
5
pub fn parse_privilege_data_from_editor_content(
311
5
    content: &str,
312
5
) -> (Vec<DatabasePrivilegeRow>, Vec<PrivilegeLineError>) {
313
5
    let mut rows: Vec<(usize, DatabasePrivilegeRow)> = Vec::new();
314
5
    let mut errors = Vec::new();
315

            
316
28
    for (line_number, line) in content.lines().map(str::trim).enumerate() {
317
28
        match parse_privilege_row_from_editor(line) {
318
11
            PrivilegeRowParseResult::PrivilegeRow(row) => rows.push((line_number, row)),
319
1
            PrivilegeRowParseResult::ParserError(e) => errors.push(PrivilegeLineError {
320
1
                line_number,
321
1
                message: format!("{e:#}"),
322
1
            }),
323
1
            PrivilegeRowParseResult::TooFewFields(n) => errors.push(PrivilegeLineError {
324
1
                line_number,
325
1
                message: format!(
326
1
                    "Too few fields: expected {}, found {n}",
327
1
                    DATABASE_PRIVILEGE_FIELDS.len(),
328
1
                ),
329
1
            }),
330
1
            PrivilegeRowParseResult::TooManyFields(n) => errors.push(PrivilegeLineError {
331
1
                line_number,
332
1
                message: format!(
333
1
                    "Too many fields: expected {}, found {n}",
334
1
                    DATABASE_PRIVILEGE_FIELDS.len(),
335
1
                ),
336
1
            }),
337
            PrivilegeRowParseResult::Header
338
            | PrivilegeRowParseResult::Comment
339
14
            | PrivilegeRowParseResult::Empty => {}
340
        }
341
    }
342

            
343
5
    let (duplicate_errors, duplicate_line_numbers) = find_duplicate_privilege_lines(&rows);
344
5
    errors.extend(duplicate_errors);
345
5
    errors.sort_by_key(|error| error.line_number);
346

            
347
5
    let rows = rows
348
5
        .into_iter()
349
11
        .filter(|(line_number, _)| !duplicate_line_numbers.contains(line_number))
350
5
        .map(|(_, row)| row)
351
5
        .collect();
352

            
353
5
    (rows, errors)
354
5
}
355

            
356
/// Detect duplicate (database, user) pairs among `rows`.
357
///
358
/// Returns a tuple containing error messages for the relevant lines, as well
359
/// as a set of line numbers to drop from the final result.
360
type PrivilegeRowOccurrences<'a> = Vec<(usize, &'a DatabasePrivilegeRow)>;
361

            
362
5
fn find_duplicate_privilege_lines(
363
5
    rows: &[(usize, DatabasePrivilegeRow)],
364
5
) -> (Vec<PrivilegeLineError>, HashSet<usize>) {
365
5
    let occurrences: HashMap<(&MySQLDatabase, &MySQLUser), PrivilegeRowOccurrences> = rows
366
5
        .iter()
367
11
        .fold(HashMap::new(), |mut map, (line_number, row)| {
368
11
            map.entry((&row.db, &row.user))
369
11
                .or_default()
370
11
                .push((*line_number, row));
371
11
            map
372
11
        });
373

            
374
5
    let duplicates: Vec<(usize, Option<PrivilegeLineError>)> = occurrences
375
5
        .into_values()
376
7
        .filter(|occurrences| occurrences.len() > 1)
377
5
        .flat_map(|occurrences| {
378
3
            let (_, first_row) = occurrences[0];
379
7
            let all_equal = occurrences.iter().all(|(_, row)| *row == first_row);
380

            
381
3
            let dropped: PrivilegeRowOccurrences = if all_equal {
382
1
                occurrences.into_iter().skip(1).collect()
383
            } else {
384
2
                occurrences
385
            };
386

            
387
3
            dropped
388
3
                .into_iter()
389
3
                .enumerate()
390
3
                .map(move |(i, (line_number, _))| (
391
6
                    line_number,
392
6
                    if all_equal || i == 0 {
393
3
                        None
394
                    } else {
395
3
                        Some(PrivilegeLineError {
396
3
                            line_number,
397
3
                            message: "Duplicate entry for this database/user pair conflicts with an earlier entry.".to_string(),
398
3
                        })
399
                    }
400
                ))
401
3
        })
402
5
        .collect();
403

            
404
5
    let duplicate_line_numbers = duplicates
405
5
        .iter()
406
5
        .map(|(line_number, _)| *line_number)
407
5
        .collect();
408
5
    let errors = duplicates
409
5
        .into_iter()
410
5
        .filter_map(|(_, error)| error)
411
5
        .collect();
412

            
413
5
    (errors, duplicate_line_numbers)
414
5
}
415

            
416
/// Map each (database, user) pair declared in `content` to the line numbers that declare it.
417
1
pub fn map_privilege_lines_by_target(
418
1
    content: &str,
419
1
) -> HashMap<(MySQLDatabase, MySQLUser), Vec<usize>> {
420
1
    content
421
1
        .lines()
422
1
        .enumerate()
423
3
        .filter_map(|(line_number, line)| {
424
3
            let mut fields = line.split_ascii_whitespace();
425
3
            let db: MySQLDatabase = fields.next()?.into();
426
3
            let user: MySQLUser = fields.next()?.into();
427
3
            Some((line_number, (db, user)))
428
3
        })
429
3
        .fold(HashMap::new(), |mut map, (line_number, target)| {
430
3
            map.entry(target).or_default().push(line_number);
431
3
            map
432
3
        })
433
1
}
434

            
435
/// Print each error alongside the line it applies to.
436
pub fn print_privilege_line_errors(content: &str, errors: &[PrivilegeLineError]) {
437
    println!("The following errors were found in your edits:\n");
438
    for (i, error) in errors.iter().enumerate() {
439
        if i > 0 {
440
            println!("---\n");
441
        }
442

            
443
        println!("{}. On line {}:\n", i + 1, error.line_number + 1);
444

            
445
        debug_assert!(
446
            error.line_number < content.lines().count(),
447
            "Error line number {} is out of bounds for content with {} lines",
448
            error.line_number,
449
            content.lines().count()
450
        );
451

            
452
        if let Some(line) = content.lines().nth(error.line_number) {
453
            println!("> {}", format_privilege_row_header_for(line));
454
            println!("> {line}\n");
455
        }
456

            
457
        println!("{}\n", error.message);
458
    }
459
}
460

            
461
pub fn format_privilege_row_header_for(line: &str) -> String {
462
    let mut header: Vec<_> = DATABASE_PRIVILEGE_FIELDS
463
        .into_iter()
464
        .map(db_priv_field_human_readable_name)
465
        .collect();
466

            
467
    let splitline = line.split_ascii_whitespace().collect::<Vec<&str>>();
468
    let dbname = splitline.first().unwrap_or(&"");
469
    let username = splitline.get(1).unwrap_or(&"");
470

            
471
    header[0] = format!("{:width$}", header[0], width = dbname.len());
472
    header[1] = format!("{:width$}", header[1], width = username.len());
473

            
474
    header.join(" ")
475
}
476

            
477
const ERROR_MARKER_PREFIX: &str = "# ^ ERROR: ";
478
const ERROR_CONTINUATION_PREFIX: &str = "#          ";
479

            
480
/// Inline error messages into the editor content, so that the user can easily see what went wrong.
481
2
pub fn inline_errors_into_editor_content(content: &str, errors: &[PrivilegeLineError]) -> String {
482
2
    content
483
2
        .lines()
484
2
        .enumerate()
485
16
        .flat_map(|(line_number, line)| {
486
16
            let comments = errors
487
16
                .iter()
488
40
                .filter(move |e| e.line_number == line_number)
489
16
                .flat_map(|e| e.message.lines())
490
16
                .enumerate()
491
16
                .map(|(i, message_line)| {
492
7
                    if i == 0 {
493
5
                        format!("{ERROR_MARKER_PREFIX}{message_line}")
494
                    } else {
495
2
                        format!("{ERROR_CONTINUATION_PREFIX}{message_line}")
496
                    }
497
7
                });
498

            
499
16
            std::iter::once(line.to_string()).chain(comments)
500
16
        })
501
2
        .join("\n")
502
2
}
503

            
504
/// Remove any error annotations previously added by [`inline_errors_into_editor_content`].
505
1
pub fn strip_inlined_errors(content: &str) -> String {
506
1
    content
507
1
        .lines()
508
11
        .scan(false, |in_error_block, line| {
509
11
            *in_error_block = line.starts_with(ERROR_MARKER_PREFIX)
510
9
                || (*in_error_block && line.starts_with(ERROR_CONTINUATION_PREFIX));
511
11
            Some((line, *in_error_block))
512
11
        })
513
11
        .filter_map(|(line, in_error_block)| (!in_error_block).then_some(line))
514
1
        .join("\n")
515
1
}
516

            
517
#[cfg(test)]
518
mod tests {
519
    use super::*;
520

            
521
    use indoc::indoc;
522
    use pretty_assertions::assert_eq;
523

            
524
    #[test]
525
1
    fn test_generate_editor_content_from_privilege_data() {
526
1
        let permissions = vec![
527
1
            DatabasePrivilegeRow {
528
1
                db: "test_abcdef".into(),
529
1
                user: "test_abcdef".into(),
530
1
                select_priv: true,
531
1
                insert_priv: false,
532
1
                update_priv: true,
533
1
                delete_priv: false,
534
1
                create_priv: true,
535
1
                drop_priv: false,
536
1
                alter_priv: true,
537
1
                index_priv: false,
538
1
                create_tmp_table_priv: true,
539
1
                lock_tables_priv: false,
540
1
                references_priv: true,
541
1
                create_view_priv: false,
542
1
                show_view_priv: true,
543
1
                trigger_priv: false,
544
1
            },
545
1
            DatabasePrivilegeRow {
546
1
                db: "test_abcdefghijlkmno".into(),
547
1
                user: "test_abcdef".into(),
548
1
                select_priv: true,
549
1
                insert_priv: false,
550
1
                update_priv: true,
551
1
                delete_priv: false,
552
1
                create_priv: true,
553
1
                drop_priv: false,
554
1
                alter_priv: true,
555
1
                index_priv: false,
556
1
                create_tmp_table_priv: true,
557
1
                lock_tables_priv: false,
558
1
                references_priv: true,
559
1
                create_view_priv: false,
560
1
                show_view_priv: true,
561
1
                trigger_priv: false,
562
1
            },
563
        ];
564

            
565
1
        let content = generate_editor_content_from_privilege_data(&permissions, "test", None);
566

            
567
1
        let expected_lines = vec![
568
            "",
569
1
            "# Welcome to the privilege editor.",
570
1
            "# Each line defines what privileges a single user has on a single database.",
571
1
            "# The first two columns respectively represent the database name and the user, and the remaining columns are the privileges.",
572
1
            "# If the user should have a certain privilege, write 'Y', otherwise write 'N'.",
573
1
            "#",
574
1
            "# Lines starting with '#' are comments and will be ignored.",
575
1
            "",
576
1
            "Database             User        Select Insert Update Delete Create Drop Alter Index Temp Lock References CreateView ShowView Trigger",
577
1
            "test_abcdef          test_abcdef Y      N      Y      N      Y      N    Y     N     Y    N    Y          N          Y        N",
578
1
            "test_abcdefghijlkmno test_abcdef Y      N      Y      N      Y      N    Y     N     Y    N    Y          N          Y        N",
579
        ];
580

            
581
1
        let generated_lines: Vec<&str> = content.lines().collect();
582

            
583
1
        assert_eq!(generated_lines, expected_lines);
584
1
    }
585

            
586
    #[test]
587
1
    fn ensure_generated_and_parsed_editor_content_is_equal() {
588
1
        let permissions = vec![
589
1
            DatabasePrivilegeRow {
590
1
                db: "db1".into(),
591
1
                user: "user".into(),
592
1
                select_priv: true,
593
1
                insert_priv: true,
594
1
                update_priv: true,
595
1
                delete_priv: true,
596
1
                create_priv: true,
597
1
                drop_priv: true,
598
1
                alter_priv: true,
599
1
                index_priv: true,
600
1
                create_tmp_table_priv: true,
601
1
                lock_tables_priv: true,
602
1
                references_priv: true,
603
1
                create_view_priv: true,
604
1
                show_view_priv: true,
605
1
                trigger_priv: true,
606
1
            },
607
1
            DatabasePrivilegeRow {
608
1
                db: "db2".into(),
609
1
                user: "user".into(),
610
1
                select_priv: false,
611
1
                insert_priv: false,
612
1
                update_priv: false,
613
1
                delete_priv: false,
614
1
                create_priv: false,
615
1
                drop_priv: false,
616
1
                alter_priv: false,
617
1
                index_priv: false,
618
1
                create_tmp_table_priv: false,
619
1
                lock_tables_priv: false,
620
1
                references_priv: false,
621
1
                create_view_priv: false,
622
1
                show_view_priv: false,
623
1
                trigger_priv: false,
624
1
            },
625
        ];
626

            
627
1
        let content = generate_editor_content_from_privilege_data(&permissions, "user", None);
628

            
629
1
        let (parsed_permissions, errors) = parse_privilege_data_from_editor_content(&content);
630

            
631
1
        assert!(errors.is_empty(), "{errors:?}");
632
1
        assert_eq!(permissions, parsed_permissions);
633
1
    }
634

            
635
    #[test]
636
1
    fn test_parse_privilege_data_from_editor_content_collects_all_errors() {
637
1
        let content = indoc! {"
638
1
            # This is a comment and should be ignored.
639
1

            
640
1
            db1 user1 Y Y Y Y Y Y Y Y Y Y Y Y Y Y
641
1

            
642
1
            # Another comment
643
1
            db2 user2 X Y Y Y Y Y Y Y Y Y Y Y Y Y
644
1
            db3 user3 too few fields
645
1

            
646
1
            db4 user4 Y N Y N Y N Y N Y N Y N Y N
647
1
            db5 user5 Y Y Y Y Y Y Y Y Y Y Y Y Y too many fields
648
1
        "};
649

            
650
1
        let (rows, errors) = parse_privilege_data_from_editor_content(content);
651

            
652
1
        assert_eq!(rows.len(), 2);
653
1
        assert_eq!(errors.len(), 3);
654

            
655
1
        assert_eq!(errors[0].line_number, 5);
656
1
        assert!(errors[0].message.contains("Select"));
657

            
658
1
        assert_eq!(errors[1].line_number, 6);
659
1
        assert!(errors[1].message.contains("Too few fields"));
660

            
661
1
        assert_eq!(errors[2].line_number, 9);
662
1
        assert!(errors[2].message.contains("Too many fields"));
663
1
    }
664

            
665
    #[test]
666
1
    fn test_inline_errors_into_editor_content() {
667
1
        let content = indoc! {"
668
1
            # A comment before anything else.
669
1

            
670
1
            db1 user1 Y Y Y Y Y Y Y Y Y Y Y Y Y Y
671
1
            db2 user2 X Y Y Y Y Y Y Y Y Y Y Y Y Y
672
1

            
673
1
            # A comment between the two invalid lines.
674
1
            db3 user3 too few fields
675
1
            db4 user4 Y N Y N Y N Y N Y N Y N Y N
676
1
        "};
677

            
678
1
        let errors = vec![
679
1
            PrivilegeLineError {
680
1
                line_number: 3,
681
1
                message: "Expected Y or N, found X".to_string(),
682
1
            },
683
1
            PrivilegeLineError {
684
1
                line_number: 6,
685
1
                message: "Expected 16 fields, found 5".to_string(),
686
1
            },
687
1
            PrivilegeLineError {
688
1
                line_number: 7,
689
1
                message: "Could not parse privilege row:\nExpected Y or N, found Q".to_string(),
690
1
            },
691
        ];
692

            
693
1
        let result = inline_errors_into_editor_content(content, &errors);
694

            
695
1
        let expected = indoc! {"
696
1
            # A comment before anything else.
697
1

            
698
1
            db1 user1 Y Y Y Y Y Y Y Y Y Y Y Y Y Y
699
1
            db2 user2 X Y Y Y Y Y Y Y Y Y Y Y Y Y
700
1
            # ^ ERROR: Expected Y or N, found X
701
1

            
702
1
            # A comment between the two invalid lines.
703
1
            db3 user3 too few fields
704
1
            # ^ ERROR: Expected 16 fields, found 5
705
1
            db4 user4 Y N Y N Y N Y N Y N Y N Y N
706
1
            # ^ ERROR: Could not parse privilege row:
707
1
            #          Expected Y or N, found Q
708
1
        "};
709

            
710
1
        assert_eq!(result, expected.trim_end());
711
1
    }
712

            
713
    #[test]
714
1
    fn test_strip_inlined_errors_recovers_original_content() {
715
1
        let content = indoc! {"
716
1
            # A comment before anything else.
717
1

            
718
1
            db1 user1 Y Y Y Y Y Y Y Y Y Y Y Y Y Y
719
1
            db2 user2 X Y Y Y Y Y Y Y Y Y Y Y Y Y
720
1

            
721
1
            # A comment between the two invalid lines.
722
1
            db3 user3 too few fields
723
1
            db4 user4 Y N Y N Y N Y N Y N Y N Y N
724
1
        "};
725
1
        let content = content.trim_end();
726

            
727
1
        let errors = vec![
728
1
            PrivilegeLineError {
729
1
                line_number: 3,
730
1
                message: "Expected Y or N, found X".to_string(),
731
1
            },
732
1
            PrivilegeLineError {
733
1
                line_number: 6,
734
1
                message: "Could not parse privilege row:\nExpected Y or N, found Q".to_string(),
735
1
            },
736
        ];
737

            
738
1
        let inlined = inline_errors_into_editor_content(content, &errors);
739
1
        assert_ne!(inlined, content);
740
1
        assert_eq!(strip_inlined_errors(&inlined), content);
741
1
    }
742

            
743
    #[test]
744
1
    fn test_map_privilege_lines_by_target() {
745
1
        let content = indoc! {"
746
1
            db1 user1 Y Y Y Y Y Y Y Y Y Y Y Y Y Y
747
1
            db2 user2 Y Y Y Y Y Y Y Y Y Y Y Y Y Y
748
1
            db2 user2 N N N N N N N N N N N N N N
749
1
        "};
750
1
        let content = content.trim_end();
751

            
752
1
        let lines = map_privilege_lines_by_target(content);
753

            
754
1
        assert_eq!(lines.get(&("db1".into(), "user1".into())), Some(&vec![0]));
755
1
        assert_eq!(
756
1
            lines.get(&("db2".into(), "user2".into())),
757
1
            Some(&vec![1, 2])
758
        );
759
1
        assert_eq!(lines.get(&("db3".into(), "user3".into())), None);
760
1
    }
761

            
762
    #[test]
763
1
    fn test_parse_privilege_data_from_editor_content_ignores_identical_duplicates() {
764
1
        let content = indoc! {"
765
1
            db1 user1 Y Y Y Y Y Y Y Y Y Y Y Y Y Y
766
1
            db1 user1 Y Y Y Y Y Y Y Y Y Y Y Y Y Y
767
1
        "};
768

            
769
1
        let (rows, errors) = parse_privilege_data_from_editor_content(content);
770

            
771
1
        assert!(errors.is_empty(), "{errors:?}");
772
1
        assert_eq!(rows.len(), 1);
773
1
    }
774

            
775
    #[test]
776
1
    fn test_parse_privilege_data_from_editor_content_flags_conflicting_duplicates() {
777
1
        let content = indoc! {"
778
1
            db1 user1 Y Y Y Y Y Y Y Y Y Y Y Y Y Y
779
1
            db1 user1 N N N N N N N N N N N N N N
780
1
        "};
781

            
782
1
        let (rows, errors) = parse_privilege_data_from_editor_content(content);
783

            
784
1
        assert!(
785
1
            rows.is_empty(),
786
            "neither occurrence should be applied while the conflict is unresolved: {rows:?}"
787
        );
788

            
789
1
        assert_eq!(errors.len(), 1);
790
1
        assert_eq!(errors[0].line_number, 1);
791
1
        assert!(errors[0].message.contains("Duplicate entry"));
792
1
    }
793

            
794
    #[test]
795
1
    fn test_parse_privilege_data_from_editor_content_flags_all_but_first_when_any_conflict() {
796
1
        let content = indoc! {"
797
1
            db1 user1 Y Y Y Y Y Y Y Y Y Y Y Y Y Y
798
1
            db1 user1 Y Y Y Y Y Y Y Y Y Y Y Y Y Y
799
1
            db1 user1 N N N N N N N N N N N N N N
800
1
        "};
801

            
802
1
        let (rows, errors) = parse_privilege_data_from_editor_content(content);
803

            
804
1
        assert!(
805
1
            rows.is_empty(),
806
            "none of the conflicting occurrences should be applied: {rows:?}"
807
        );
808

            
809
1
        assert_eq!(errors.len(), 2);
810
1
        assert_eq!(errors[0].line_number, 1);
811
1
        assert_eq!(errors[1].line_number, 2);
812
1
    }
813
}