Skip to main content

btrfs_cli/filesystem/
du.rs

1//! Implementation of `btrfs filesystem du`.
2//!
3//! Uses [`btrfs_uapi::fiemap::file_extents`] to query the physical extent
4//! layout of each file.  For each file we report:
5//!
6//! * **Total** — sum of all non-skipped extent lengths
7//! * **Exclusive** — bytes not marked `FIEMAP_EXTENT_SHARED`
8//! * **Set shared** — at the top-level only: physical bytes shared with at
9//!   least one other file in the same argument's subtree, computed by
10//!   collecting all shared physical ranges, sorting, and merging overlaps.
11//!   Always `-` for non-top-level lines.
12//!
13//! The output format and semantics match `btrfs-progs filesystem du`.
14
15use super::UnitMode;
16use crate::{
17    Format, RunContext, Runnable,
18    util::{SizeFormat, fmt_size},
19};
20use anyhow::{Context, Result};
21use btrfs_uapi::fiemap::file_extents;
22use clap::Parser;
23use cols::Cols;
24use serde::Serialize;
25use std::{
26    collections::HashSet,
27    fs::{self, File},
28    os::unix::{fs::MetadataExt, io::AsFd},
29    path::{Path, PathBuf},
30};
31
32/// Summarize disk usage of each file, showing shared extents
33///
34/// For each path, prints three columns:
35///
36///   Total      — logical bytes used by non-inline extents
37///   Exclusive  — bytes not shared with any other file
38///   Set shared — (top-level only) physical bytes shared within this subtree
39#[derive(Parser, Debug)]
40pub struct FilesystemDuCommand {
41    /// Display only a total for each argument, not per-file lines
42    #[clap(long, short, conflicts_with = "depth")]
43    pub summarize: bool,
44
45    /// Maximum depth of entries to display.
46    ///
47    /// 0 is equivalent to --summarize, 1 shows only immediate children, etc.
48    /// All levels are still computed for totals.
49    #[clap(long, short = 'd', conflicts_with = "summarize")]
50    pub depth: Option<usize>,
51
52    /// Sort entries within each directory (modern output only). [default: path]
53    ///
54    /// Size keys sort largest first, path sorts alphabetically.
55    #[clap(long, value_enum)]
56    pub sort: Option<DuSort>,
57
58    #[clap(flatten)]
59    pub units: UnitMode,
60
61    /// One or more files or directories to summarize
62    #[clap(required = true)]
63    pub paths: Vec<PathBuf>,
64}
65
66/// Sort order for entries within each directory.
67#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
68pub enum DuSort {
69    /// Alphabetical by path (ascending)
70    #[default]
71    Path,
72    /// By total size (largest first)
73    Total,
74    /// By exclusive size (largest first)
75    Exclusive,
76    /// By shared size (largest first)
77    Shared,
78}
79
80/// Collected size info for a single file or directory, used for sorting.
81struct DuEntry {
82    path: PathBuf,
83    total: u64,
84    shared: u64,
85    /// Shared extent physical ranges (files only).
86    shared_extents: Vec<(u64, u64)>,
87    /// Children rows for modern tree output (directories only).
88    tree_children: Vec<DuRow>,
89}
90
91impl DuEntry {
92    fn exclusive(&self) -> u64 {
93        self.total.saturating_sub(self.shared)
94    }
95}
96
97fn sort_entries(entries: &mut [DuEntry], sort: DuSort) {
98    entries.sort_by(|a, b| match sort {
99        DuSort::Path => a.path.cmp(&b.path),
100        DuSort::Total => b.total.cmp(&a.total),
101        DuSort::Exclusive => b.exclusive().cmp(&a.exclusive()),
102        DuSort::Shared => b.shared.cmp(&a.shared),
103    });
104}
105
106impl Runnable for FilesystemDuCommand {
107    fn supported_formats(&self) -> &[Format] {
108        &[Format::Text, Format::Modern, Format::Json]
109    }
110
111    fn run(&self, ctx: &RunContext) -> Result<()> {
112        let mode = self.units.resolve();
113        let max_depth = if self.summarize { Some(0) } else { self.depth };
114
115        if self.sort.is_some() && ctx.format != Format::Modern {
116            anyhow::bail!("--sort is only supported with --format modern");
117        }
118        let sort = self.sort.unwrap_or(DuSort::Path);
119
120        match ctx.format {
121            Format::Modern => {
122                let mut trees: Vec<DuRow> = Vec::new();
123                for path in &self.paths {
124                    let row = collect_tree(path, max_depth, sort, &mode)
125                        .with_context(|| {
126                            format!(
127                                "cannot check space of '{}'",
128                                path.display()
129                            )
130                        })?;
131                    trees.push(row);
132                }
133                let mut out = std::io::stdout().lock();
134                let _ = DuRow::print_table(&trees, &mut out);
135            }
136            Format::Text => {
137                println!(
138                    "{:>10}  {:>10}  {:>10}  Filename",
139                    "Total", "Exclusive", "Set shared"
140                );
141                for path in &self.paths {
142                    print_top_level(path, max_depth, &mode).with_context(
143                        || {
144                            format!(
145                                "cannot check space of '{}'",
146                                path.display()
147                            )
148                        },
149                    )?;
150                }
151            }
152            Format::Json => {
153                let mut all: Vec<DuEntryJson> = Vec::new();
154                for path in &self.paths {
155                    collect_json(path, max_depth, &mut all)?;
156                }
157                crate::util::print_json("filesystem-du", &all)?;
158            }
159        }
160
161        Ok(())
162    }
163}
164
165// ── Modern output (cols tree) ──────────────────────────────────────────
166
167#[derive(Clone, Cols)]
168struct DuRow {
169    #[column(header = "TOTAL", right)]
170    total: String,
171    #[column(header = "EXCL", right)]
172    exclusive: String,
173    #[column(header = "SET SHARED", right)]
174    set_shared: String,
175    #[column(tree, wrap)]
176    path: String,
177    #[column(children)]
178    children: Vec<Self>,
179}
180
181/// Build a `DuRow` tree for a top-level path.
182fn collect_tree(
183    path: &Path,
184    max_depth: Option<usize>,
185    sort: DuSort,
186    mode: &SizeFormat,
187) -> Result<DuRow> {
188    let mut seen: HashSet<(u64, u64)> = HashSet::new();
189    let mut shared_ranges: Vec<(u64, u64)> = Vec::new();
190
191    let meta = fs::symlink_metadata(path)
192        .with_context(|| format!("cannot stat '{}'", path.display()))?;
193
194    let root_dev = meta.dev();
195
196    let (total, file_shared, children) = if meta.is_file() {
197        // Skip open() + FIEMAP for zero-length files (no extents).
198        if meta.len() == 0 {
199            (0, 0, Vec::new())
200        } else {
201            let file = File::open(path)
202                .with_context(|| format!("cannot open '{}'", path.display()))?;
203            let info = file_extents(file.as_fd()).map_err(|e| {
204                anyhow::anyhow!("fiemap failed on '{}': {e}", path.display())
205            })?;
206            shared_ranges.extend_from_slice(&info.shared_extents);
207            (info.total_bytes, info.shared_bytes, Vec::new())
208        }
209    } else if meta.is_dir() {
210        collect_dir_tree(
211            path,
212            root_dev,
213            &mut seen,
214            &mut shared_ranges,
215            max_depth,
216            0,
217            sort,
218            mode,
219        )?
220    } else {
221        (0, 0, Vec::new())
222    };
223
224    let set_shared = compute_set_shared(&mut shared_ranges);
225    let exclusive = total.saturating_sub(file_shared);
226
227    Ok(DuRow {
228        total: fmt_size(total, mode),
229        exclusive: fmt_size(exclusive, mode),
230        set_shared: fmt_size(set_shared, mode),
231        path: path.display().to_string(),
232        children,
233    })
234}
235
236/// Recursively collect a directory into `DuRow` children.
237///
238/// Returns `(total_bytes, shared_bytes, child_rows)`. Children are only
239/// included in the output when `depth < max_depth` (or `max_depth` is
240/// `None` for unlimited). The walk always descends fully for correct totals.
241#[allow(clippy::too_many_arguments)]
242fn collect_dir_tree(
243    dir: &Path,
244    root_dev: u64,
245    seen: &mut HashSet<(u64, u64)>,
246    shared_ranges: &mut Vec<(u64, u64)>,
247    max_depth: Option<usize>,
248    depth: usize,
249    sort: DuSort,
250    mode: &SizeFormat,
251) -> Result<(u64, u64, Vec<DuRow>)> {
252    let mut dir_total: u64 = 0;
253    let mut dir_shared: u64 = 0;
254    let show = max_depth.is_none_or(|m| depth < m);
255
256    let raw_items = collect_dir_entries(dir, root_dev, seen)?;
257    let mut entries = collect_entry_sizes(
258        &raw_items,
259        root_dev,
260        seen,
261        shared_ranges,
262        max_depth,
263        depth,
264        sort,
265        mode,
266    )?;
267
268    sort_entries(&mut entries, sort);
269
270    let mut children: Vec<DuRow> = Vec::new();
271    for e in &entries {
272        shared_ranges.extend_from_slice(&e.shared_extents);
273        dir_total += e.total;
274        dir_shared += e.shared;
275
276        if show {
277            let excl = e.exclusive();
278            children.push(DuRow {
279                total: fmt_size(e.total, mode),
280                exclusive: fmt_size(excl, mode),
281                set_shared: "-".to_string(),
282                path: path_name(&e.path),
283                children: e.tree_children.clone(),
284            });
285        }
286    }
287
288    Ok((dir_total, dir_shared, children))
289}
290
291/// Stat and FIEMAP each entry, returning `DuEntry` items with raw sizes.
292#[allow(clippy::too_many_arguments)]
293fn collect_entry_sizes(
294    items: &[(PathBuf, fs::Metadata)],
295    root_dev: u64,
296    seen: &mut HashSet<(u64, u64)>,
297    shared_ranges: &mut Vec<(u64, u64)>,
298    max_depth: Option<usize>,
299    depth: usize,
300    sort: DuSort,
301    mode: &SizeFormat,
302) -> Result<Vec<DuEntry>> {
303    let mut entries = Vec::new();
304
305    for (entry_path, meta) in items {
306        if meta.is_file() {
307            // Zero-length files have no extents, so skip the open() +
308            // FIEMAP syscalls. This avoids two syscalls per empty file,
309            // which adds up on large trees with many small/empty files.
310            if meta.len() == 0 {
311                entries.push(DuEntry {
312                    path: entry_path.clone(),
313
314                    total: 0,
315                    shared: 0,
316                    shared_extents: Vec::new(),
317                    tree_children: Vec::new(),
318                });
319                continue;
320            }
321
322            let file = match File::open(entry_path) {
323                Ok(f) => f,
324                Err(e) => {
325                    eprintln!(
326                        "warning: cannot open '{}': {e}",
327                        entry_path.display()
328                    );
329                    continue;
330                }
331            };
332
333            let info = match file_extents(file.as_fd()) {
334                Ok(v) => v,
335                Err(e) => {
336                    eprintln!(
337                        "warning: fiemap failed on '{}': {e}",
338                        entry_path.display()
339                    );
340                    continue;
341                }
342            };
343
344            entries.push(DuEntry {
345                path: entry_path.clone(),
346                total: info.total_bytes,
347                shared: info.shared_bytes,
348                shared_extents: info.shared_extents,
349                tree_children: Vec::new(),
350            });
351        } else {
352            let (sub_total, sub_shared, sub_children) = collect_dir_tree(
353                entry_path,
354                root_dev,
355                seen,
356                shared_ranges,
357                max_depth,
358                depth + 1,
359                sort,
360                mode,
361            )?;
362
363            entries.push(DuEntry {
364                path: entry_path.clone(),
365                total: sub_total,
366                shared: sub_shared,
367                shared_extents: Vec::new(),
368                tree_children: sub_children,
369            });
370        }
371    }
372
373    Ok(entries)
374}
375
376// ── Legacy text output ─────────────────────────────────────────────────
377
378fn print_top_level(
379    path: &Path,
380    max_depth: Option<usize>,
381    mode: &SizeFormat,
382) -> Result<()> {
383    let mut seen: HashSet<(u64, u64)> = HashSet::new();
384    let mut shared_ranges: Vec<(u64, u64)> = Vec::new();
385
386    let meta = fs::symlink_metadata(path)
387        .with_context(|| format!("cannot stat '{}'", path.display()))?;
388
389    let root_dev = meta.dev();
390
391    let (total, file_shared) = if meta.is_file() {
392        // Skip open() + FIEMAP for zero-length files (no extents).
393        if meta.len() == 0 {
394            (0, 0)
395        } else {
396            let file = File::open(path)
397                .with_context(|| format!("cannot open '{}'", path.display()))?;
398            let info = file_extents(file.as_fd()).map_err(|e| {
399                anyhow::anyhow!("fiemap failed on '{}': {e}", path.display())
400            })?;
401            shared_ranges.extend_from_slice(&info.shared_extents);
402            (info.total_bytes, info.shared_bytes)
403        }
404    } else if meta.is_dir() {
405        print_walk_dir(
406            path,
407            root_dev,
408            &mut seen,
409            &mut shared_ranges,
410            max_depth,
411            0,
412            mode,
413        )?
414    } else {
415        (0, 0)
416    };
417
418    let set_shared = compute_set_shared(&mut shared_ranges);
419    let exclusive = total.saturating_sub(file_shared);
420
421    println!(
422        "{:>10}  {:>10}  {:>10}  {}",
423        fmt_size(total, mode),
424        fmt_size(exclusive, mode),
425        fmt_size(set_shared, mode),
426        path.display()
427    );
428
429    Ok(())
430}
431
432#[allow(clippy::too_many_lines)]
433fn print_walk_dir(
434    dir: &Path,
435    root_dev: u64,
436    seen: &mut HashSet<(u64, u64)>,
437    shared_ranges: &mut Vec<(u64, u64)>,
438    max_depth: Option<usize>,
439    depth: usize,
440    mode: &SizeFormat,
441) -> Result<(u64, u64)> {
442    let mut dir_total: u64 = 0;
443    let mut dir_shared: u64 = 0;
444    let show = max_depth.is_none_or(|m| depth < m);
445
446    let entries = fs::read_dir(dir).with_context(|| {
447        format!("cannot read directory '{}'", dir.display())
448    })?;
449
450    for entry in entries {
451        let entry = entry.with_context(|| {
452            format!("error reading entry in '{}'", dir.display())
453        })?;
454        let entry_path = entry.path();
455
456        let meta = match fs::symlink_metadata(&entry_path) {
457            Ok(m) => m,
458            Err(e) => {
459                eprintln!(
460                    "warning: cannot stat '{}': {e}",
461                    entry_path.display()
462                );
463                continue;
464            }
465        };
466
467        if !meta.is_file() && !meta.is_dir() {
468            continue;
469        }
470
471        if meta.dev() != root_dev {
472            continue;
473        }
474
475        let key = (meta.dev(), meta.ino());
476        if !seen.insert(key) {
477            continue;
478        }
479
480        if meta.is_file() {
481            // Zero-length files have no extents, so skip the open() +
482            // FIEMAP syscalls. This avoids two syscalls per empty file,
483            // which adds up on large trees with many small/empty files.
484            if meta.len() == 0 {
485                if show {
486                    println!(
487                        "{:>10}  {:>10}  {:>10}  {}",
488                        fmt_size(0, mode),
489                        fmt_size(0, mode),
490                        "-",
491                        entry_path.display()
492                    );
493                }
494                continue;
495            }
496
497            let file = match File::open(&entry_path) {
498                Ok(f) => f,
499                Err(e) => {
500                    eprintln!(
501                        "warning: cannot open '{}': {e}",
502                        entry_path.display()
503                    );
504                    continue;
505                }
506            };
507
508            let info = match file_extents(file.as_fd()) {
509                Ok(v) => v,
510                Err(e) => {
511                    eprintln!(
512                        "warning: fiemap failed on '{}': {e}",
513                        entry_path.display()
514                    );
515                    continue;
516                }
517            };
518
519            if show {
520                let excl = info.total_bytes.saturating_sub(info.shared_bytes);
521                println!(
522                    "{:>10}  {:>10}  {:>10}  {}",
523                    fmt_size(info.total_bytes, mode),
524                    fmt_size(excl, mode),
525                    "-",
526                    entry_path.display()
527                );
528            }
529
530            shared_ranges.extend_from_slice(&info.shared_extents);
531            dir_total += info.total_bytes;
532            dir_shared += info.shared_bytes;
533        } else {
534            let (sub_total, sub_shared) = print_walk_dir(
535                &entry_path,
536                root_dev,
537                seen,
538                shared_ranges,
539                max_depth,
540                depth + 1,
541                mode,
542            )?;
543
544            if show {
545                let excl = sub_total.saturating_sub(sub_shared);
546                println!(
547                    "{:>10}  {:>10}  {:>10}  {}",
548                    fmt_size(sub_total, mode),
549                    fmt_size(excl, mode),
550                    "-",
551                    entry_path.display()
552                );
553            }
554
555            dir_total += sub_total;
556            dir_shared += sub_shared;
557        }
558    }
559
560    Ok((dir_total, dir_shared))
561}
562
563// ── Shared helpers ─────────────────────────────────────────────────────
564
565/// Collect and filter directory entries, returning sorted (path, metadata) pairs.
566fn collect_dir_entries(
567    dir: &Path,
568    root_dev: u64,
569    seen: &mut HashSet<(u64, u64)>,
570) -> Result<Vec<(PathBuf, fs::Metadata)>> {
571    let mut items = Vec::new();
572    let entries = fs::read_dir(dir).with_context(|| {
573        format!("cannot read directory '{}'", dir.display())
574    })?;
575
576    for entry in entries {
577        let entry = entry.with_context(|| {
578            format!("error reading entry in '{}'", dir.display())
579        })?;
580        let entry_path = entry.path();
581
582        let meta = match fs::symlink_metadata(&entry_path) {
583            Ok(m) => m,
584            Err(e) => {
585                eprintln!(
586                    "warning: cannot stat '{}': {e}",
587                    entry_path.display()
588                );
589                continue;
590            }
591        };
592
593        if !meta.is_file() && !meta.is_dir() {
594            continue;
595        }
596
597        if meta.dev() != root_dev {
598            continue;
599        }
600
601        let key = (meta.dev(), meta.ino());
602        if !seen.insert(key) {
603            continue;
604        }
605
606        items.push((entry_path, meta));
607    }
608
609    items.sort_by(|(a, _), (b, _)| a.cmp(b));
610    Ok(items)
611}
612
613/// Extract the file/directory name from a path (last component).
614fn path_name(path: &Path) -> String {
615    path.file_name().map_or_else(
616        || path.display().to_string(),
617        |n| n.to_string_lossy().into_owned(),
618    )
619}
620
621// ── JSON output ────────────────────────────────────────────────────────
622
623#[derive(Serialize)]
624struct DuEntryJson {
625    path: String,
626    total: u64,
627    exclusive: u64,
628    /// `None` for non-top-level entries (rendered as JSON `null`).
629    set_shared: Option<u64>,
630}
631
632/// Collect JSON entries for a top-level path.
633fn collect_json(
634    path: &Path,
635    max_depth: Option<usize>,
636    out: &mut Vec<DuEntryJson>,
637) -> Result<()> {
638    let mut seen: HashSet<(u64, u64)> = HashSet::new();
639    let mut shared_ranges: Vec<(u64, u64)> = Vec::new();
640
641    let meta = fs::symlink_metadata(path)
642        .with_context(|| format!("cannot stat '{}'", path.display()))?;
643
644    let root_dev = meta.dev();
645
646    let (total, file_shared) = if meta.is_file() {
647        if meta.len() == 0 {
648            (0, 0)
649        } else {
650            let file = File::open(path)
651                .with_context(|| format!("cannot open '{}'", path.display()))?;
652            let info = file_extents(file.as_fd()).map_err(|e| {
653                anyhow::anyhow!("fiemap failed on '{}': {e}", path.display())
654            })?;
655            shared_ranges.extend_from_slice(&info.shared_extents);
656            (info.total_bytes, info.shared_bytes)
657        }
658    } else if meta.is_dir() {
659        collect_json_walk(
660            path,
661            root_dev,
662            &mut seen,
663            &mut shared_ranges,
664            max_depth,
665            0,
666            out,
667        )?
668    } else {
669        (0, 0)
670    };
671
672    let set_shared = compute_set_shared(&mut shared_ranges);
673    let exclusive = total.saturating_sub(file_shared);
674
675    out.push(DuEntryJson {
676        path: path.display().to_string(),
677        total,
678        exclusive,
679        set_shared: Some(set_shared),
680    });
681
682    Ok(())
683}
684
685#[allow(clippy::too_many_arguments)]
686fn collect_json_walk(
687    dir: &Path,
688    root_dev: u64,
689    seen: &mut HashSet<(u64, u64)>,
690    shared_ranges: &mut Vec<(u64, u64)>,
691    max_depth: Option<usize>,
692    depth: usize,
693    out: &mut Vec<DuEntryJson>,
694) -> Result<(u64, u64)> {
695    let mut dir_total: u64 = 0;
696    let mut dir_shared: u64 = 0;
697    let show = max_depth.is_none_or(|m| depth < m);
698
699    let entries = fs::read_dir(dir).with_context(|| {
700        format!("cannot read directory '{}'", dir.display())
701    })?;
702
703    for entry in entries {
704        let entry = entry.with_context(|| {
705            format!("error reading entry in '{}'", dir.display())
706        })?;
707        let entry_path = entry.path();
708
709        let meta = match fs::symlink_metadata(&entry_path) {
710            Ok(m) => m,
711            Err(e) => {
712                eprintln!(
713                    "warning: cannot stat '{}': {e}",
714                    entry_path.display()
715                );
716                continue;
717            }
718        };
719
720        if !meta.is_file() && !meta.is_dir() {
721            continue;
722        }
723
724        if meta.dev() != root_dev {
725            continue;
726        }
727
728        let key = (meta.dev(), meta.ino());
729        if !seen.insert(key) {
730            continue;
731        }
732
733        if meta.is_file() {
734            // Zero-length files have no extents, so skip the open() +
735            // FIEMAP syscalls. This avoids two syscalls per empty file,
736            // which adds up on large trees with many small/empty files.
737            if meta.len() == 0 {
738                if show {
739                    out.push(DuEntryJson {
740                        path: entry_path.display().to_string(),
741                        total: 0,
742                        exclusive: 0,
743                        set_shared: None,
744                    });
745                }
746                continue;
747            }
748
749            let file = match File::open(&entry_path) {
750                Ok(f) => f,
751                Err(e) => {
752                    eprintln!(
753                        "warning: cannot open '{}': {e}",
754                        entry_path.display()
755                    );
756                    continue;
757                }
758            };
759
760            let info = match file_extents(file.as_fd()) {
761                Ok(v) => v,
762                Err(e) => {
763                    eprintln!(
764                        "warning: fiemap failed on '{}': {e}",
765                        entry_path.display()
766                    );
767                    continue;
768                }
769            };
770
771            if show {
772                let excl = info.total_bytes.saturating_sub(info.shared_bytes);
773                out.push(DuEntryJson {
774                    path: entry_path.display().to_string(),
775                    total: info.total_bytes,
776                    exclusive: excl,
777                    set_shared: None,
778                });
779            }
780
781            shared_ranges.extend_from_slice(&info.shared_extents);
782            dir_total += info.total_bytes;
783            dir_shared += info.shared_bytes;
784        } else {
785            let (sub_total, sub_shared) = collect_json_walk(
786                &entry_path,
787                root_dev,
788                seen,
789                shared_ranges,
790                max_depth,
791                depth + 1,
792                out,
793            )?;
794
795            if show {
796                let excl = sub_total.saturating_sub(sub_shared);
797                out.push(DuEntryJson {
798                    path: entry_path.display().to_string(),
799                    total: sub_total,
800                    exclusive: excl,
801                    set_shared: None,
802                });
803            }
804
805            dir_total += sub_total;
806            dir_shared += sub_shared;
807        }
808    }
809
810    Ok((dir_total, dir_shared))
811}
812
813/// Merge `ranges` in place and return the total bytes covered by the union.
814///
815/// This gives the "set shared" value: physical bytes referenced by at least
816/// one `FIEMAP_EXTENT_SHARED` extent anywhere in the subtree.
817fn compute_set_shared(ranges: &mut [(u64, u64)]) -> u64 {
818    if ranges.is_empty() {
819        return 0;
820    }
821    ranges.sort_unstable();
822
823    let mut total = 0u64;
824    let (mut cur_start, mut cur_end) = ranges[0];
825
826    for &(start, end) in &ranges[1..] {
827        if start <= cur_end {
828            if end > cur_end {
829                cur_end = end;
830            }
831        } else {
832            total += cur_end - cur_start;
833            cur_start = start;
834            cur_end = end;
835        }
836    }
837    total += cur_end - cur_start;
838    total
839}