Coverage Report

Created: 2025-12-31 06:22

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/logforth-append-file-0.3.0/src/rolling.rs
Line
Count
Source
1
// Copyright 2024 FastLabs Developers
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//     http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use std::fs;
16
use std::fs::File;
17
use std::fs::Metadata;
18
use std::fs::OpenOptions;
19
use std::io;
20
use std::io::Write;
21
use std::num::NonZeroUsize;
22
use std::path::Path;
23
use std::path::PathBuf;
24
use std::str::FromStr;
25
26
use jiff::Zoned;
27
use jiff::civil::DateTime;
28
use logforth_core::Error;
29
use logforth_core::Trap;
30
use logforth_core::trap::BestEffortTrap;
31
32
use crate::clock::Clock;
33
use crate::rotation::Rotation;
34
35
/// A writer for rolling files.
36
#[derive(Debug)]
37
pub struct RollingFileWriter {
38
    state: State,
39
    writer: File,
40
}
41
42
impl Drop for RollingFileWriter {
43
0
    fn drop(&mut self) {
44
0
        if let Err(err) = self.writer.flush() {
45
0
            let err = Error::new("failed to flush file writer on dropped").set_source(err);
46
0
            self.state.trap.trap(&err);
47
0
        }
48
0
    }
49
}
50
51
impl Write for RollingFileWriter {
52
0
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
53
0
        let now = self.state.clock.now();
54
0
        let writer = &mut self.writer;
55
56
0
        if self.state.should_rollover_on_date(&now) {
57
0
            self.state.current_filesize = 0;
58
0
            self.state.next_date_timestamp = self.state.rotation.next_date_timestamp(&now);
59
0
            let current = &self.state.this_date_timestamp;
60
0
            self.state.refresh_writer(current, writer);
61
0
        }
62
63
0
        if self.state.should_rollover_on_size() {
64
0
            self.state.current_filesize = 0;
65
0
            self.state.refresh_writer(&now, writer);
66
0
        }
67
68
0
        self.state.this_date_timestamp = now;
69
70
0
        writer
71
0
            .write(buf)
72
0
            .inspect(|&n| self.state.current_filesize += n)
73
0
    }
74
75
0
    fn flush(&mut self) -> io::Result<()> {
76
0
        self.writer.flush()
77
0
    }
78
}
79
80
/// A builder for configuring [`RollingFileWriter`].
81
#[derive(Debug)]
82
pub struct RollingFileWriterBuilder {
83
    // required
84
    basedir: PathBuf,
85
    filename: String,
86
87
    // has default
88
    rotation: Rotation,
89
    filename_suffix: Option<String>,
90
    max_size: Option<NonZeroUsize>,
91
    max_files: Option<NonZeroUsize>,
92
    clock: Clock,
93
    trap: Box<dyn Trap>,
94
}
95
96
impl RollingFileWriterBuilder {
97
    /// Creates a new [`RollingFileWriterBuilder`].
98
    #[must_use]
99
0
    pub fn new(basedir: impl Into<PathBuf>, filename: impl Into<String>) -> Self {
100
0
        Self {
101
0
            basedir: basedir.into(),
102
0
            filename: filename.into(),
103
0
            rotation: Rotation::Never,
104
0
            filename_suffix: None,
105
0
            max_size: None,
106
0
            max_files: None,
107
0
            clock: Clock::DefaultClock,
108
0
            trap: Box::new(BestEffortTrap::default()),
109
0
        }
110
0
    }
111
112
    /// Set the trap for the rolling file writer.
113
0
    pub fn trap(mut self, trap: impl Into<Box<dyn Trap>>) -> Self {
114
0
        self.trap = trap.into();
115
0
        self
116
0
    }
117
118
    /// Set the rotation policy.
119
    #[must_use]
120
0
    pub fn rotation(mut self, rotation: Rotation) -> Self {
121
0
        self.rotation = rotation;
122
0
        self
123
0
    }
124
125
    /// Set the filename suffix.
126
    #[must_use]
127
0
    pub fn filename_suffix(mut self, suffix: impl Into<String>) -> Self {
128
0
        let suffix = suffix.into();
129
0
        self.filename_suffix = if suffix.is_empty() {
130
0
            None
131
        } else {
132
0
            Some(suffix)
133
        };
134
0
        self
135
0
    }
136
137
    /// Set the maximum number of log files to keep.
138
    #[must_use]
139
0
    pub fn max_log_files(mut self, n: NonZeroUsize) -> Self {
140
0
        self.max_files = Some(n);
141
0
        self
142
0
    }
143
144
    /// Set the maximum size of a log file in bytes.
145
    #[must_use]
146
0
    pub fn max_file_size(mut self, n: NonZeroUsize) -> Self {
147
0
        self.max_size = Some(n);
148
0
        self
149
0
    }
150
151
    #[cfg(test)]
152
    fn clock(mut self, clock: Clock) -> Self {
153
        self.clock = clock;
154
        self
155
    }
156
157
    /// Builds the [`RollingFileWriter`].
158
0
    pub fn build(self) -> Result<RollingFileWriter, Error> {
159
        let Self {
160
0
            basedir,
161
0
            rotation,
162
0
            filename,
163
0
            filename_suffix,
164
0
            max_size,
165
0
            max_files,
166
0
            clock,
167
0
            trap,
168
0
        } = self;
169
170
0
        if filename.is_empty() {
171
0
            return Err(Error::new("filename must not be empty"));
172
0
        }
173
174
0
        let (state, writer) = State::new(
175
0
            rotation,
176
0
            basedir,
177
0
            filename,
178
0
            filename_suffix,
179
0
            max_size,
180
0
            max_files,
181
0
            clock,
182
0
            trap,
183
0
        )?;
184
185
0
        Ok(RollingFileWriter { state, writer })
186
0
    }
187
}
188
189
#[derive(Debug)]
190
struct LogFile {
191
    filepath: PathBuf,
192
    metadata: Metadata,
193
    datetime: DateTime,
194
    count: usize,
195
}
196
197
// oldest is the least
198
0
fn compare_logfile(a: &LogFile, b: &LogFile) -> std::cmp::Ordering {
199
0
    match a.datetime.cmp(&b.datetime) {
200
        std::cmp::Ordering::Equal => {
201
0
            let a_rev = usize::MAX - a.count;
202
0
            let b_rev = usize::MAX - b.count;
203
0
            a_rev.cmp(&b_rev)
204
        }
205
0
        ord => ord,
206
    }
207
0
}
208
209
#[derive(Debug)]
210
struct State {
211
    log_dir: PathBuf,
212
    log_filename: String,
213
    log_filename_suffix: Option<String>,
214
    date_format: &'static str,
215
    rotation: Rotation,
216
    current_filesize: usize,
217
    this_date_timestamp: Zoned,
218
    next_date_timestamp: Option<usize>,
219
    max_size: Option<NonZeroUsize>,
220
    max_files: Option<NonZeroUsize>,
221
    clock: Clock,
222
    trap: Box<dyn Trap>,
223
}
224
225
impl State {
226
    #[allow(clippy::too_many_arguments)]
227
0
    fn new(
228
0
        rotation: Rotation,
229
0
        dir: impl AsRef<Path>,
230
0
        log_filename: String,
231
0
        log_filename_suffix: Option<String>,
232
0
        max_size: Option<NonZeroUsize>,
233
0
        max_files: Option<NonZeroUsize>,
234
0
        clock: Clock,
235
0
        trap: Box<dyn Trap>,
236
0
    ) -> Result<(Self, File), Error> {
237
0
        let now = clock.now();
238
0
        let log_dir = dir.as_ref().to_path_buf();
239
0
        fs::create_dir_all(&log_dir)
240
0
            .map_err(|err| Error::new("failed to create log directory").set_source(err))?;
241
242
0
        let mut state = State {
243
0
            log_dir,
244
0
            log_filename,
245
0
            log_filename_suffix,
246
0
            date_format: rotation.date_format(),
247
0
            current_filesize: 0,
248
0
            this_date_timestamp: clock.now(),
249
0
            next_date_timestamp: rotation.next_date_timestamp(&now),
250
0
            rotation,
251
0
            max_size,
252
0
            max_files,
253
0
            clock,
254
0
            trap,
255
0
        };
256
257
0
        let files = {
258
0
            let mut files = state.list_logfiles()?;
259
0
            files.sort_by(compare_logfile);
260
0
            files
261
        };
262
263
0
        let file = match files.last() {
264
            None => {
265
                // brand-new directory
266
0
                state.create_log_writer()?
267
            }
268
0
            Some(last) => {
269
0
                let filename = state.current_filename();
270
0
                if last.filepath != filename {
271
                    // for some reason, the `filename.suffix` file does not exist; create a new one
272
0
                    state.create_log_writer()?
273
                } else {
274
0
                    state.current_filesize = last.metadata.len() as usize;
275
276
0
                    if let Ok(mtime) = last.metadata.modified() {
277
0
                        if let Ok(mtime) = Zoned::try_from(mtime) {
278
0
                            state.next_date_timestamp = state.rotation.next_date_timestamp(&mtime);
279
0
                            state.this_date_timestamp = mtime;
280
0
                        }
281
0
                    }
282
283
                    // continue to use the existing current log file
284
0
                    OpenOptions::new()
285
0
                        .append(true)
286
0
                        .open(&filename)
287
0
                        .map_err(|err| Error::new("failed to open current log").set_source(err))?
288
                }
289
            }
290
        };
291
292
0
        Ok((state, file))
293
0
    }
294
295
0
    fn current_filename(&self) -> PathBuf {
296
0
        let filename = &self.log_filename;
297
0
        match self.log_filename_suffix.as_ref() {
298
0
            None => self.log_dir.join(filename),
299
0
            Some(suffix) => self.log_dir.join(format!("{filename}.{suffix}")),
300
        }
301
0
    }
302
303
0
    fn create_log_writer(&self) -> Result<File, Error> {
304
0
        let filename = self.current_filename();
305
0
        OpenOptions::new()
306
0
            .write(true)
307
0
            .create_new(true)
308
0
            .open(&filename)
309
0
            .map_err(|err| Error::new("failed to create log file").set_source(err))
310
0
    }
311
312
0
    fn join_date(&self, date: &Zoned, cnt: usize) -> PathBuf {
313
0
        let date = date.strftime(self.date_format);
314
0
        let filename = match (
315
0
            &self.rotation,
316
0
            &self.log_filename,
317
0
            &self.log_filename_suffix,
318
        ) {
319
0
            (&Rotation::Never, filename, None) => format!("{filename}.{cnt}"),
320
0
            (&Rotation::Never, filename, Some(suffix)) => {
321
0
                format!("{filename}.{cnt}.{suffix}")
322
            }
323
0
            (_, filename, Some(suffix)) => format!("{filename}.{date}.{cnt}.{suffix}"),
324
0
            (_, filename, None) => format!("{filename}.{date}.{cnt}"),
325
        };
326
0
        self.log_dir.join(filename)
327
0
    }
328
329
0
    fn list_logfiles(&self) -> Result<Vec<LogFile>, Error> {
330
0
        let read_dir = fs::read_dir(&self.log_dir).map_err(|err| {
331
0
            Error::new(format!(
332
0
                "failed to read log dir: {}",
333
0
                self.log_dir.display()
334
            ))
335
0
            .set_source(err)
336
0
        })?;
337
338
0
        let files = read_dir
339
0
            .filter_map(|entry| {
340
0
                let entry = entry.ok()?;
341
0
                let filepath = entry.path();
342
343
0
                let metadata = entry.metadata().ok()?;
344
                // the appender only creates files, not directories or symlinks,
345
0
                if !metadata.is_file() {
346
0
                    return None;
347
0
                }
348
349
0
                let filename = entry.file_name();
350
                // if the filename is not a UTF-8 string, skip it.
351
0
                let mut filename = filename.to_str()?;
352
0
                if !filename.starts_with(&self.log_filename) {
353
0
                    return None;
354
0
                }
355
0
                filename = &filename[self.log_filename.len()..];
356
357
0
                if let Some(suffix) = &self.log_filename_suffix {
358
0
                    if !filename.ends_with(suffix) {
359
0
                        return None;
360
0
                    }
361
0
                    filename = &filename[..filename.len() - suffix.len() - 1];
362
0
                }
363
364
0
                if filename.is_empty() {
365
                    // the current log file is the largest
366
0
                    return Some(LogFile {
367
0
                        filepath,
368
0
                        metadata,
369
0
                        datetime: DateTime::MAX,
370
0
                        count: 0,
371
0
                    });
372
0
                }
373
374
0
                if filename.starts_with(".") {
375
0
                    filename = &filename[1..];
376
0
                } else {
377
0
                    return None;
378
                }
379
380
0
                let datetime = if self.rotation != Rotation::Never {
381
                    // mandatory datetime part
382
0
                    let pos = filename.find('.')?;
383
0
                    let datetime = DateTime::strptime(self.date_format, &filename[..pos]).ok()?;
384
0
                    filename = &filename[pos + 1..];
385
0
                    datetime
386
                } else {
387
0
                    DateTime::MAX
388
                };
389
390
0
                let count = usize::from_str(&filename[..filename.len()]).ok()?;
391
392
0
                Some(LogFile {
393
0
                    filepath,
394
0
                    metadata,
395
0
                    datetime,
396
0
                    count,
397
0
                })
398
0
            })
399
0
            .collect::<Vec<_>>();
400
401
0
        Ok(files)
402
0
    }
403
404
0
    fn delete_oldest_logs(&self, max_files: usize) -> Result<(), Error> {
405
0
        let mut files = self.list_logfiles()?;
406
0
        if files.len() < max_files {
407
0
            return Ok(());
408
0
        }
409
410
        // delete files, so that (n-1) files remain, because we will create another log file
411
0
        files.sort_by(compare_logfile);
412
0
        for file in files.iter().take(files.len() - (max_files - 1)) {
413
0
            let filepath = &file.filepath;
414
0
            fs::remove_file(filepath).map_err(|err| {
415
0
                Error::new(format!("failed to remove old log: {}", filepath.display()))
416
0
                    .set_source(err)
417
0
            })?;
418
        }
419
420
0
        Ok(())
421
0
    }
422
423
0
    fn rotate_log_writer(&self, now: &Zoned) -> Result<File, Error> {
424
0
        let mut renames = vec![];
425
0
        for i in 1..self.max_files.map_or(usize::MAX, |n| n.get()) {
426
0
            let filepath = self.join_date(now, i);
427
0
            if fs::exists(&filepath).is_ok_and(|ok| ok) {
428
0
                let next = self.join_date(now, i + 1);
429
0
                renames.push((filepath, next));
430
0
            } else {
431
0
                break;
432
            }
433
        }
434
435
0
        for (old, new) in renames.iter().rev() {
436
0
            fs::rename(old, new).map_err(|err| {
437
0
                Error::new(format!("failed to rotate log: {}", old.display())).set_source(err)
438
0
            })?
439
        }
440
441
0
        let archive_filepath = self.join_date(now, 1);
442
0
        let current_filepath = self.current_filename();
443
0
        fs::rename(&current_filepath, &archive_filepath).map_err(|err| {
444
0
            Error::new(format!(
445
0
                "failed to archive log: {}",
446
0
                current_filepath.display()
447
            ))
448
0
            .set_source(err)
449
0
        })?;
450
451
0
        if let Some(max_files) = self.max_files {
452
0
            if let Err(err) = self.delete_oldest_logs(max_files.get()) {
453
0
                let err = Error::new("failed to delete oldest logs").set_source(err);
454
0
                self.trap.trap(&err);
455
0
            }
456
0
        }
457
458
0
        self.create_log_writer()
459
0
    }
460
461
0
    fn refresh_writer(&self, now: &Zoned, file: &mut File) {
462
0
        match self.rotate_log_writer(now) {
463
0
            Ok(new_file) => {
464
0
                if let Err(err) = file.flush() {
465
0
                    let err = Error::new("failed to flush previous writer").set_source(err);
466
0
                    self.trap.trap(&err);
467
0
                }
468
0
                *file = new_file;
469
            }
470
0
            Err(err) => {
471
0
                let err = Error::new("failed to rotate log writer").set_source(err);
472
0
                self.trap.trap(&err);
473
0
            }
474
        }
475
0
    }
476
477
0
    fn should_rollover_on_date(&self, date: &Zoned) -> bool {
478
0
        self.next_date_timestamp
479
0
            .is_some_and(|ts| date.timestamp().as_millisecond() as usize >= ts)
480
0
    }
481
482
0
    fn should_rollover_on_size(&self) -> bool {
483
0
        self.max_size
484
0
            .is_some_and(|n| self.current_filesize >= n.get())
485
0
    }
486
}
487
488
#[cfg(test)]
489
mod tests {
490
    use std::cmp::min;
491
    use std::fs;
492
    use std::io::Write;
493
    use std::num::NonZeroUsize;
494
    use std::ops::Add;
495
    use std::str::FromStr;
496
497
    use jiff::Span;
498
    use jiff::Zoned;
499
    use rand::Rng;
500
    use rand::distr::Alphanumeric;
501
    use tempfile::TempDir;
502
503
    use crate::clock::Clock;
504
    use crate::clock::ManualClock;
505
    use crate::rolling::RollingFileWriterBuilder;
506
    use crate::rotation::Rotation;
507
508
    #[test]
509
    fn test_file_rolling_via_file_size() {
510
        test_file_rolling_for_specific_file_size(3, 1000);
511
        test_file_rolling_for_specific_file_size(3, 10000);
512
        test_file_rolling_for_specific_file_size(10, 8888);
513
        test_file_rolling_for_specific_file_size(10, 10000);
514
        test_file_rolling_for_specific_file_size(20, 6666);
515
        test_file_rolling_for_specific_file_size(20, 10000);
516
    }
517
518
    fn test_file_rolling_for_specific_file_size(max_files: usize, max_size: usize) {
519
        let max_files = NonZeroUsize::new(max_files).unwrap();
520
        let max_size = NonZeroUsize::new(max_size).unwrap();
521
        let temp_dir = TempDir::new().unwrap();
522
523
        let mut writer = RollingFileWriterBuilder::new(temp_dir.as_ref(), "test_file")
524
            .rotation(Rotation::Never)
525
            .filename_suffix("log")
526
            .max_log_files(max_files)
527
            .max_file_size(max_size)
528
            .build()
529
            .unwrap();
530
531
        for i in 1..=(max_files.get() * 2) {
532
            let mut expected_file_size = 0;
533
            while expected_file_size < max_size.get() {
534
                let rand_str = generate_random_string();
535
                expected_file_size += rand_str.len();
536
                assert_eq!(writer.write(rand_str.as_bytes()).unwrap(), rand_str.len());
537
                assert_eq!(writer.state.current_filesize, expected_file_size);
538
            }
539
540
            writer.flush().unwrap();
541
            assert_eq!(
542
                fs::read_dir(&writer.state.log_dir).unwrap().count(),
543
                min(i, max_files.get())
544
            );
545
        }
546
    }
547
548
    #[test]
549
    fn test_file_rolling_via_time_rotation() {
550
        test_file_rolling_for_specific_time_rotation(
551
            Rotation::Minutely,
552
            Span::new().minutes(1),
553
            Span::new().seconds(1),
554
        );
555
        test_file_rolling_for_specific_time_rotation(
556
            Rotation::Hourly,
557
            Span::new().hours(1),
558
            Span::new().minutes(1),
559
        );
560
        test_file_rolling_for_specific_time_rotation(
561
            Rotation::Daily,
562
            Span::new().days(1),
563
            Span::new().hours(1),
564
        );
565
    }
566
567
    fn test_file_rolling_for_specific_time_rotation(
568
        rotation: Rotation,
569
        rotation_duration: Span,
570
        write_interval: Span,
571
    ) {
572
        let max_files = NonZeroUsize::new(10).unwrap();
573
        let temp_dir = TempDir::new().unwrap();
574
575
        let start_time = Zoned::from_str("2024-08-10T00:00:00[UTC]").unwrap();
576
        let mut writer = RollingFileWriterBuilder::new(temp_dir.as_ref(), "test_file")
577
            .rotation(rotation)
578
            .filename_suffix("log")
579
            .max_log_files(max_files)
580
            .clock(Clock::ManualClock(ManualClock::new(start_time.clone())))
581
            .build()
582
            .unwrap();
583
584
        let mut cur_time = start_time;
585
586
        for i in 1..=(max_files.get() * 2) {
587
            let mut expected_file_size = 0;
588
            let end_time = cur_time.add(rotation_duration);
589
            while cur_time < end_time {
590
                writer.state.clock.set_now(cur_time.clone());
591
592
                let rand_str = generate_random_string();
593
                expected_file_size += rand_str.len();
594
595
                assert_eq!(writer.write(rand_str.as_bytes()).unwrap(), rand_str.len());
596
                assert_eq!(writer.state.current_filesize, expected_file_size);
597
598
                cur_time = cur_time.add(write_interval);
599
            }
600
601
            writer.flush().unwrap();
602
            assert_eq!(
603
                fs::read_dir(&writer.state.log_dir).unwrap().count(),
604
                min(i, max_files.get())
605
            );
606
        }
607
    }
608
609
    #[test]
610
    fn test_file_rolling_via_file_size_and_time_rotation() {
611
        test_file_size_and_time_rotation_for_specific_time_rotation(
612
            Rotation::Minutely,
613
            Span::new().minutes(1),
614
            Span::new().seconds(1),
615
        );
616
        test_file_size_and_time_rotation_for_specific_time_rotation(
617
            Rotation::Hourly,
618
            Span::new().hours(1),
619
            Span::new().minutes(1),
620
        );
621
        test_file_size_and_time_rotation_for_specific_time_rotation(
622
            Rotation::Daily,
623
            Span::new().days(1),
624
            Span::new().hours(1),
625
        );
626
    }
627
628
    fn test_file_size_and_time_rotation_for_specific_time_rotation(
629
        rotation: Rotation,
630
        rotation_duration: Span,
631
        write_interval: Span,
632
    ) {
633
        let max_files = NonZeroUsize::new(10).unwrap();
634
        let file_size = NonZeroUsize::new(500).unwrap();
635
        // Small file size and too many files to ensure both of file size and time rotation can be
636
        // triggered.
637
        let total_files = 100;
638
        let temp_dir = TempDir::new().unwrap();
639
640
        let start_time = Zoned::from_str("2024-08-10T00:00:00[UTC]").unwrap();
641
        let mut writer = RollingFileWriterBuilder::new(temp_dir.as_ref(), "test_file")
642
            .rotation(rotation)
643
            .filename_suffix("log")
644
            .max_log_files(max_files)
645
            .max_file_size(file_size)
646
            .clock(Clock::ManualClock(ManualClock::new(start_time.clone())))
647
            .build()
648
            .unwrap();
649
650
        let mut cur_time = start_time;
651
        let mut end_time = cur_time.add(rotation_duration);
652
        let mut time_rotation_trigger = false;
653
        let mut file_size_rotation_trigger = false;
654
655
        for i in 1..=total_files {
656
            let mut expected_file_size = 0;
657
            loop {
658
                writer.state.clock.set_now(cur_time.clone());
659
660
                let rand_str = generate_random_string();
661
                expected_file_size += rand_str.len();
662
663
                assert_eq!(writer.write(rand_str.as_bytes()).unwrap(), rand_str.len());
664
                assert_eq!(writer.state.current_filesize, expected_file_size);
665
666
                cur_time = cur_time.add(write_interval);
667
668
                if cur_time >= end_time {
669
                    end_time = end_time.add(rotation_duration);
670
                    time_rotation_trigger = true;
671
                    break;
672
                }
673
                if expected_file_size >= file_size.get() {
674
                    file_size_rotation_trigger = true;
675
                    break;
676
                }
677
            }
678
679
            writer.flush().unwrap();
680
            assert_eq!(
681
                fs::read_dir(&writer.state.log_dir).unwrap().count(),
682
                min(i, max_files.get())
683
            );
684
        }
685
        assert!(file_size_rotation_trigger);
686
        assert!(time_rotation_trigger);
687
    }
688
689
    fn generate_random_string() -> String {
690
        let mut rng = rand::rng();
691
        let len = rng.random_range(50..=100);
692
        let random_string: String = std::iter::repeat(())
693
            .map(|()| rng.sample(Alphanumeric))
694
            .map(char::from)
695
            .take(len)
696
            .collect();
697
698
        random_string
699
    }
700
}