a2kit/
lib.rs

1//! # `a2kit` main library
2//! 
3//! This library manipulates retro language files and disk images, with emphasis on Apple II.
4//! 
5//! ## Language Services
6//! 
7//! Language modules are designed to be complementary to the needs of language servers that
8//! use the language server protocol (LSP).
9//! Specific language services are in modules named after the language, at present:
10//! * `lang::applesoft` handles Applesoft BASIC
11//! * `lang::integer` handles Integer BASIC
12//! * `lang::merlin` handles Merlin assembly language
13//! 
14//! The language servers are in `bin` and compile to separate executables.  The language servers
15//! and CLI both depend on `lang`, but do not depend on each other.
16//! 
17//! ## Disk Images
18//! 
19//! Disk image operations are built around three trait objects:
20//! * `img::DiskImage` encodes/decodes disk tracks, does not try to interpret a file system
21//! * `fs::DiskFS` imposes a file system on the already decoded track data
22//!     - don't confuse `std::fs` and `a2kit::fs`
23//! * `fs::FileImage` provides a representation of a file that can be restored to a disk image
24//! 
25//! When a `DiskFS` object is created it takes ownership of some `DiskImage`.
26//! It then uses this owned image as storage.  Any changes are not permanent until the
27//! image is saved to whatever file system is hosting a2kit.
28//! 
29//! ### File Systems
30//! 
31//! In order to manipulate files, `a2kit` must understand the file system it finds on the disk image.
32//! As of this writing `a2kit` supports
33//! * CP/M 1,2,3
34//! * Apple DOS 3.x
35//! * FAT (e.g. MS-DOS)
36//! * ProDOS
37//! * Pascal File System
38//! 
39//! A simple example follows:
40//! ```rs
41//! // DiskFS is always mutable because the underlying image can be stateful.
42//! let mut disk = a2kit::create_fs_from_file("disk.woz",None)?;
43//! // Get a text file from the disk image as a String.
44//! let text = disk.read_text("README")?;
45//! ```
46//! 
47//! ### Tracks and Sectors
48//! 
49//! In order to manipulate tracks and sectors, `a2kit` must understand the way the track data is packed
50//! into a disk image.  As of this writing `a2kit` supports
51//! 
52//! format | platforms | aliases
53//! -------|-----------|--------
54//! 2MG | Apple II |
55//! D13 | Apple II |
56//! DO | Apple II | DSK
57//! PO | Apple II | DSK
58//! IMD | CP/M, FAT |
59//! IMG | FAT | DSK, IMA
60//! NIB | Apple II |
61//! TD0 | CP/M, FAT |
62//! WOZ | Apple II |
63//! 
64//! A simple example follows:
65//! ```rs
66//! use a2kit::img::{Track,Sector};
67//! // DiskImage can be stateful and therefore is always mutable
68//! let mut img = a2kit::create_img_from_file("disk.woz")?;
69//! // Unlike DiskFS, we cannot access files, only tracks and sectors
70//! let sector_data = img.read_sector(Track::Num(0),Sector::Num(0))?;
71//! // Disk images are *always* buffered, so writing only affects memory
72//! img.write_sector(Track::Num(0),Sector::Num(1),&sector_data)?;
73//! // Save the changes to local storage
74//! a2kit::save_img(&mut img,"disk.woz")?;
75//! ```
76//!
77//! ### Disk Kinds
78//! 
79//! Disk kinds are a classification scheme wherein each kind represents the set of all formats
80//! that can be handled by a specific hardware or emulation subsystem.  The kinds `a2kit` supports include
81//! * Logical ProDOS volumes
82//! * 3 inch CP/M formats (Amstrad 184K)
83//! * 3.5 inch Apple formats (400K/800K)
84//! * 3.5 inch IBM formats(720K through 2880K)
85//! * 5.25 inch Apple formats (114K/140K)
86//! * 5.25 inch IBM formats (160K through 1200K)
87//! * 5.25 inch CP/M formats (Osborne 100K/200K, Kaypro 200K/400K)
88//! * 8 inch CP/M formats (IBM 250K, Nabu 1M, TRS-80 600K)
89//! 
90//! The way a disk kind is identified is by looking for matches to
91//! the physical package and track layout, e.g.:
92//! ```rs
93//! fn test_disk(kind: DiskKind) {
94//!     match kind {
95//!         DiskKind::D3(_) => panic!("not looking for 3 inch disks"),
96//!         DiskKind::D35(_) => panic!("not looking for 3.5 inch disks"),
97//!         DiskKind::D525(layout) => println!("layout of 5.25 inch disk is {}",layout),
98//!         _ => panic!("something else")
99//!     };
100//! }
101//! ```
102
103pub mod fs;
104pub mod lang;
105pub mod bios;
106pub mod img;
107pub mod commands;
108
109use img::DiskImage;
110use img::tracks::DiskFormat;
111use fs::DiskFS;
112use std::io::Read;
113use std::fmt::Write;
114use log::{warn,info,debug,error};
115use regex::Regex;
116use hex;
117
118type DYNERR = Box<dyn std::error::Error>;
119type STDRESULT = Result<(),Box<dyn std::error::Error>>;
120
121const KNOWN_FILE_EXTENSIONS: &str = "2mg,2img,dsk,d13,do,nib,po,woz,imd,td0,img,ima";
122const MAX_FILE_SIZE: u64 = 1 << 26;
123
124/// Save the image file (make changes permanent)
125pub fn save_img(disk: &mut Box<dyn DiskFS>,img_path: &str) -> STDRESULT {
126    std::fs::write(img_path,disk.get_img().to_bytes())?;
127    Ok(())
128}
129
130/// Return the file system on a disk image, if all goes well we have `Ok(Some(fs))`.
131/// If the file system cannot be identified we have `Ok(None)`.
132/// If the file system is identified, but broken, we have `Err(_)`.
133/// If `Ok(Some(_))`, the file system takes ownership of the disk image.
134fn try_img(mut img: Box<dyn DiskImage>,maybe_fmt: Option<&DiskFormat>) -> Result<Option<Box<dyn DiskFS>>,DYNERR> {
135    if let Some(fmt) = maybe_fmt {
136        img.change_format(fmt.clone())?;
137    }
138    if fs::dos3x::Disk::test_img(&mut img,maybe_fmt) {
139        info!("identified DOS 3.x file system");
140        return Ok(Some(Box::new(fs::dos3x::Disk::from_img(img)?)));
141    }
142    if fs::prodos::Disk::test_img(&mut img) {
143        info!("identified ProDOS file system");
144        return Ok(Some(Box::new(fs::prodos::Disk::from_img(img)?)));
145    }
146    if fs::pascal::Disk::test_img(&mut img) {
147        info!("identified Pascal file system");
148        return Ok(Some(Box::new(fs::pascal::Disk::from_img(img)?)));
149    }
150    if fs::fat::Disk::test_img(&mut img) {
151        info!("identified FAT file system");
152        return Ok(Some(Box::new(fs::fat::Disk::from_img(img,None)?)));
153    }
154    if fs::fat::Disk::test_img_dos1x(&mut img) {
155        info!("identified MS-DOS 1.x file system");
156        return Ok(Some(Box::new(fs::fat::Disk::from_img_dos1x(img)?)));
157    }
158    // For CP/M we have to try various DPB
159    let dpb_list = match img.what_am_i() {
160        img::DiskImageType::DO | img::DiskImageType::NIB | img::DiskImageType::DOT2MG | img::DiskImageType::WOZ1 => vec![
161            bios::dpb::A2_525],
162        img::DiskImageType::WOZ2 => vec![
163            // 800K CP/M could go here someday
164            bios::dpb::A2_525],
165        img::DiskImageType::IMD | img::DiskImageType::TD0 => vec![
166            bios::dpb::CPM1,
167            bios::dpb::SSSD_525,
168            bios::dpb::SSDD_525_OFF1,
169            bios::dpb::SSDD_525_OFF3,
170            bios::dpb::SSDD_3,
171            bios::dpb::DSDD_525_OFF1,
172            bios::dpb::TRS80_M2,
173            bios::dpb::NABU],
174        _ => vec![]
175    };
176    for dpb in &dpb_list {
177        if fs::cpm::Disk::test_img(&mut img,dpb,[3,1,0]) {
178            info!("identified CP/M file system on {}",dpb);
179            return Ok(Some(Box::new(fs::cpm::Disk::from_img(img,dpb.clone(),[3,1,0])?)));
180        }
181    }
182   return Ok(None);
183}
184
185/// Given a bytestream return a DiskFS, or Err if the bytestream cannot be interpreted.
186/// Optional `maybe_ext` restricts the image types that will be tried based on file extension.
187/// Optional `maybe_fmt` can be used to specify a proprietary format (if `None` standard formats will be tried).
188pub fn create_fs_from_bytestream(disk_img_data: &Vec<u8>,maybe_ext: Option<&str>,maybe_fmt: Option<&DiskFormat>) -> Result<Box<dyn DiskFS>,DYNERR> {
189    let ext = match maybe_ext {
190        Some(x) => x.to_string().to_lowercase(),
191        None => "".to_string()
192    };
193    if disk_img_data.len() < 100 {
194        return Err(Box::new(img::Error::ImageSizeMismatch));
195    }
196    debug!("matching image type {}",ext);
197    if img::imd::file_extensions().contains(&ext) || ext=="" {
198        if let Ok(img) = img::imd::Imd::from_bytes(disk_img_data) {
199            info!("identified IMD image");
200            if let Some(disk) = try_img(Box::new(img),maybe_fmt)? {
201                return Ok(disk);
202            }
203        }
204    }
205    if img::woz1::file_extensions().contains(&ext) || ext=="" {
206        if let Ok(img) = img::woz1::Woz1::from_bytes(disk_img_data) {
207            info!("identified woz1 image");
208            if let Some(disk) = try_img(Box::new(img),maybe_fmt)? {
209                return Ok(disk);
210            }
211        }
212    }
213    if img::woz2::file_extensions().contains(&ext) || ext=="" {
214        if let Ok(img) = img::woz2::Woz2::from_bytes(disk_img_data) {
215            info!("identified woz2 image");
216            if let Some(disk) = try_img(Box::new(img),maybe_fmt)? {
217                return Ok(disk);
218            }
219        }
220    }
221    if img::dot2mg::file_extensions().contains(&ext) || ext=="" {
222        if let Ok(img) = img::dot2mg::Dot2mg::from_bytes(disk_img_data) {
223            info!("identified 2mg image");
224            if let Some(disk) = try_img(Box::new(img),maybe_fmt)? {
225                return Ok(disk);
226            }
227        }
228    }
229    if img::td0::file_extensions().contains(&ext) || ext=="" {
230        if let Ok(img) = img::td0::Td0::from_bytes(disk_img_data) {
231            info!("identified td0 image");
232            if let Some(disk) = try_img(Box::new(img),maybe_fmt)? {
233                return Ok(disk);
234            }
235        }
236    }
237    if img::nib::file_extensions().contains(&ext) || ext=="" {
238        if let Ok(img) = img::nib::Nib::from_bytes(disk_img_data) {
239            info!("Possible nib/nb2 image");
240            if let Some(disk) = try_img(Box::new(img),maybe_fmt)? {
241                return Ok(disk);
242            }
243        }
244    }
245    if img::dsk_d13::file_extensions().contains(&ext) || ext=="" {
246        if let Ok(img) = img::dsk_d13::D13::from_bytes(disk_img_data) {
247            info!("Possible D13 image");
248            if let Some(disk) = try_img(Box::new(img),maybe_fmt)? {
249                return Ok(disk);
250            }
251        }
252    }
253    if img::dsk_do::file_extensions().contains(&ext) || ext=="" {
254        if let Ok(img) = img::dsk_do::DO::from_bytes(disk_img_data) {
255            info!("Possible DO image");
256            if let Some(disk) = try_img(Box::new(img),maybe_fmt)? {
257                return Ok(disk);
258            }
259        }
260    }
261    if img::dsk_po::file_extensions().contains(&ext) || ext=="" {
262        if let Ok(img) = img::dsk_po::PO::from_bytes(disk_img_data) {
263            info!("Possible PO image");
264            if let Some(disk) = try_img(Box::new(img),maybe_fmt)? {
265                return Ok(disk);
266            }
267        }
268    }
269    if img::dsk_img::file_extensions().contains(&ext) || ext=="" {
270        if let Ok(img) = img::dsk_img::Img::from_bytes(disk_img_data) {
271            info!("Possible IMG image");
272            if let Some(disk) = try_img(Box::new(img),maybe_fmt)? {
273                return Ok(disk);
274            }
275        }
276    }
277    warn!("cannot match any file system");
278    return Err(Box::new(fs::Error::FileSystemMismatch));
279}
280
281/// Given a bytestream return a disk image without any file system.
282/// Optional `maybe_ext` restricts the image types that will be tried based on file extension.
283/// FS heuristics might be invoked if the image type is ambiguous.
284pub fn create_img_from_bytestream(disk_img_data: &Vec<u8>,maybe_ext: Option<&str>) -> Result<Box<dyn DiskImage>,DYNERR> {
285    let ext = match maybe_ext {
286        Some(x) => x.to_string().to_lowercase(),
287        None => "".to_string()
288    };
289    if disk_img_data.len() < 100 {
290        return Err(Box::new(img::Error::ImageSizeMismatch));
291    }
292    debug!("matching image type {}",ext);
293    if img::imd::file_extensions().contains(&ext) || ext=="" {
294        if let Ok(img) = img::imd::Imd::from_bytes(disk_img_data) {
295            info!("identified IMD image");
296            return Ok(Box::new(img));
297        }
298    }
299    if img::woz1::file_extensions().contains(&ext) || ext=="" {
300        if let Ok(img) = img::woz1::Woz1::from_bytes(disk_img_data) {
301            info!("identified woz1 image");
302            return Ok(Box::new(img));
303        }
304    }
305    if img::woz2::file_extensions().contains(&ext) || ext=="" {
306        if let Ok(img) = img::woz2::Woz2::from_bytes(disk_img_data) {
307            info!("identified woz2 image");
308            return Ok(Box::new(img));
309        }
310    }
311    if img::dot2mg::file_extensions().contains(&ext) || ext=="" {
312        if let Ok(img) = img::dot2mg::Dot2mg::from_bytes(disk_img_data) {
313            info!("identified 2mg image");
314            return Ok(Box::new(img));
315        }
316    }
317    if img::td0::file_extensions().contains(&ext) || ext=="" {
318        if let Ok(img) = img::td0::Td0::from_bytes(disk_img_data) {
319            info!("identified td0 image");
320            return Ok(Box::new(img));
321        }
322    }
323    if img::nib::file_extensions().contains(&ext) || ext=="" {
324        if let Ok(img) = img::nib::Nib::from_bytes(disk_img_data) {
325            info!("Possible nib/nb2 image");
326            return Ok(Box::new(img));
327        }
328    }
329    if img::dsk_d13::file_extensions().contains(&ext) || ext=="" {
330        if let Ok(img) = img::dsk_d13::D13::from_bytes(disk_img_data) {
331            info!("Possible D13 image");
332            return Ok(Box::new(img));
333        }
334    }
335    // For DO we need to run the FS heuristics to distinguish from PO,
336    // in case the extension hint is missing or vague.
337    if img::dsk_do::file_extensions().contains(&ext) || ext=="" {
338        if let Ok(img) = img::dsk_do::DO::from_bytes(disk_img_data) {
339            info!("Possible DO image");
340            if ext=="do" {
341                return Ok(Box::new(img));
342            }
343            if let Ok(Some(_)) = try_img(Box::new(img),None) {
344                if let Ok(copy) = img::dsk_do::DO::from_bytes(disk_img_data) {
345                    return Ok(Box::new(copy));
346                }
347            }
348            debug!("reject DO based on FS heuristics")
349        }
350    }
351    if img::dsk_po::file_extensions().contains(&ext) || ext=="" {
352        if let Ok(img) = img::dsk_po::PO::from_bytes(disk_img_data) {
353            info!("Possible PO image");
354            if ext=="po" {
355                return Ok(Box::new(img));
356            }
357            if let Ok(Some(_)) = try_img(Box::new(img),None) {
358                if let Ok(copy) = img::dsk_po::PO::from_bytes(disk_img_data) {
359                    return Ok(Box::new(copy));
360                }
361            }
362            debug!("reject PO based on FS heuristics")
363        }
364    }
365    if img::dsk_img::file_extensions().contains(&ext) || ext=="" {
366        if let Ok(img) = img::dsk_img::Img::from_bytes(disk_img_data) {
367            info!("Possible IMG image");
368            return Ok(Box::new(img));
369        }
370    }
371    warn!("cannot match any image format");
372    return Err(Box::new(img::Error::ImageTypeMismatch));
373}
374
375/// buffer a file if its EOF < `max`, otherwise return an error
376fn buffer_file(path: &str,max: u64) -> Result<Vec<u8>,DYNERR> {
377    let mut f = std::fs::OpenOptions::new().read(true).open(path)?;
378    match f.metadata()?.len() <= max {
379        true => {
380            let mut buf = Vec::new();
381            f.read_to_end(&mut buf)?;
382            Ok(buf)
383        },
384        false => Err(Box::new(img::Error::ImageSizeMismatch))
385    }
386}
387
388/// Calls `create_img_from_bytestream` getting the bytes from stdin.
389/// All image types will be tried heuristically.
390pub fn create_img_from_stdin() -> Result<Box<dyn DiskImage>,DYNERR> {
391    let mut disk_img_data = Vec::new();
392    if atty::is(atty::Stream::Stdin) {
393        error!("pipe a disk image or use `-d` option");
394        return Err(Box::new(commands::CommandError::InvalidCommand));
395    }
396    std::io::stdin().read_to_end(&mut disk_img_data)?;
397    create_img_from_bytestream(&disk_img_data,None)
398}
399
400/// Calls `create_img_from_bytestream` getting the bytes from a file.
401/// The pathname must already be in the right format for the file system.
402/// File extension will be used to restrict image types that are tried,
403/// unless the extension is unknown, in which case all will be tried.
404pub fn create_img_from_file(img_path: &str) -> Result<Box<dyn DiskImage>,DYNERR> {
405    let disk_img_data = buffer_file(img_path,MAX_FILE_SIZE)?;
406    let maybe_ext = match img_path.split('.').last() {
407        Some(ext) if KNOWN_FILE_EXTENSIONS.contains(&ext.to_lowercase()) => Some(ext),
408        _ => None
409    };
410    create_img_from_bytestream(&disk_img_data,maybe_ext)
411}
412
413/// If the path is given call `create_img_from_file`, otherwise call `create_img_from_stdin`
414pub fn create_img_from_file_or_stdin(maybe_img_path: Option<&String>) -> Result<Box<dyn DiskImage>,DYNERR> {
415    match maybe_img_path {
416        Some(img_path) => create_img_from_file(img_path),
417        None => create_img_from_stdin()
418    }
419}
420
421/// Calls `create_fs_from_bytestream` getting the bytes from stdin.
422/// All image types and file systems will be tried heuristically.
423/// If `maybe_fmt` is `None` deduce a standard format.
424pub fn create_fs_from_stdin(maybe_fmt: Option<&DiskFormat>) -> Result<Box<dyn DiskFS>,DYNERR> {
425    let mut disk_img_data = Vec::new();
426    if atty::is(atty::Stream::Stdin) {
427        error!("pipe a disk image or use `-d` option");
428        return Err(Box::new(commands::CommandError::InvalidCommand));
429    }
430    std::io::stdin().read_to_end(&mut disk_img_data)?;
431    create_fs_from_bytestream(&disk_img_data, None, maybe_fmt)
432}
433
434/// Calls `create_fs_from_bytestream` getting the bytes from a file.
435/// The pathname must already be in the right format for the file system.
436/// File extension will be used to restrict image types that are tried,
437/// unless the extension is unknown, in which case all will be tried.
438/// If `maybe_fmt` is `None` deduce a standard format.
439pub fn create_fs_from_file(img_path: &str,maybe_fmt: Option<&DiskFormat>) -> Result<Box<dyn DiskFS>,DYNERR> {
440    let disk_img_data = buffer_file(img_path,MAX_FILE_SIZE)?;
441    let maybe_ext = match img_path.split('.').last() {
442        Some(ext) if KNOWN_FILE_EXTENSIONS.contains(&ext.to_lowercase()) => Some(ext),
443        _ => None
444    };
445    create_fs_from_bytestream(&disk_img_data,maybe_ext,maybe_fmt)
446}
447
448/// If the path is given call `create_fs_from_file`, otherwise call `create_fs_from_stdin`
449fn create_fs_from_file_or_stdin(maybe_img_path: Option<&String>,maybe_fmt: Option<&DiskFormat>) -> Result<Box<dyn DiskFS>,DYNERR> {
450    match maybe_img_path {
451        Some(img_path) => create_fs_from_file(img_path,maybe_fmt),
452        None => create_fs_from_stdin(maybe_fmt)
453    }
454}
455
456/// Display binary to stdout in columns of hex, +ascii, and -ascii
457pub fn display_block(start_addr: usize,block: &Vec<u8>) {
458    let mut slice_start = 0;
459    loop {
460        let row_label = start_addr + slice_start;
461        let mut slice_end = slice_start + 16;
462        if slice_end > block.len() {
463            slice_end = block.len();
464        }
465        let slice = block[slice_start..slice_end].to_vec();
466        let txt: Vec<u8> = slice.iter().map(|c| match *c {
467            x if x<32 => '.' as u8,
468            x if x<127 => x,
469            _ => '.' as u8
470        }).collect();
471        let neg_txt: Vec<u8> = slice.iter().map(|c| match *c {
472            x if x>=160 && x<255 => x - 128,
473            _ => 46
474        }).collect();
475        print!("{:04X} : ",row_label);
476        for byte in slice {
477            print!("{:02X} ",byte);
478        }
479        for _blank in slice_end..slice_start+16 {
480            print!("   ");
481        }
482        print!("|+| {} ",String::from_utf8_lossy(&txt));
483        for _blank in slice_end..slice_start+16 {
484            print!(" ");
485        }
486        println!("|-| {}",String::from_utf8_lossy(&neg_txt));
487        slice_start += 16;
488        if slice_end==block.len() {
489            break;
490        }
491    }
492}
493
494/// This takes any bytes and makes an ascii friendly string
495/// by using hex escapes, e.g., `\xFF`.
496/// if `escape_cc` is true, ascii control characters are also escaped.
497/// if `inverted` is true, assume we have negative ascii bytes.
498/// This is intended for directory strings, for language files use `lang::bytes_to_escaped_string`
499pub fn escaped_ascii_from_bytes(bytes: &Vec<u8>,escape_cc: bool,inverted: bool) -> String {
500    let mut result = String::new();
501    let (lb,ub) = match (escape_cc,inverted) {
502        (true,false) => (0x20,0x7e),
503        (false,false) => (0x00,0x7f),
504        (true,true) => (0xa0,0xfe),
505        (false,true) => (0x80,0xff)
506    };
507    for i in 0..bytes.len() {
508        if bytes[i]>=lb && bytes[i]<=ub {
509            if inverted {
510                result += std::str::from_utf8(&[bytes[i]-0x80]).expect("unreachable");
511            } else {
512                result += std::str::from_utf8(&[bytes[i]]).expect("unreachable");
513            }
514        } else {
515            let mut temp = String::new();
516            write!(&mut temp,"\\x{:02X}",bytes[i]).expect("unreachable");
517            result += &temp;
518        }
519    }
520    return result;
521}
522
523/// Interpret a UTF8 string as pure ascii and put into bytes.
524/// Non-ascii characters are omitted from the result, but arbitrary
525/// bytes can be introduced using escapes, e.g., `\xFF`.
526/// Literal hex escapes are created by coding the backslash, e.g., `\x5CxFF`.
527/// if `inverted` is true the sign of the non-escaped bytes is flipped.
528/// if `caps` is true the ascii is put in upper case.
529/// This is suitable for either languages or directory strings.
530pub fn escaped_ascii_to_bytes(s: &str,inverted: bool,caps: bool) -> Vec<u8> {
531    let mut ans: Vec<u8> = Vec::new();
532    let hex_patt = Regex::new(r"\\x[0-9A-Fa-f][0-9A-Fa-f]").expect("unreachable");
533    let mut hexes = hex_patt.find_iter(s);
534    let mut maybe_hex = hexes.next();
535    let mut curs = 0;
536    let mut skip = 0;
537    for c in s.chars() {
538    
539        if skip>0 {
540            skip -= 1;
541            continue;
542        }
543        if let Some(hex) = maybe_hex {
544            if curs==hex.start() {
545                ans.append(&mut hex::decode(s.get(curs+2..curs+4).unwrap()).expect("unreachable"));
546                curs += 4;
547                maybe_hex = hexes.next();
548                skip = 3;
549                continue;
550            }
551        }
552        
553        if c.is_ascii() {
554            let mut buf: [u8;1] = [0;1];
555            if caps {
556                c.to_uppercase().next().unwrap().encode_utf8(&mut buf);
557            } else {
558                c.encode_utf8(&mut buf);
559            }
560            ans.push(buf[0] + match inverted { true => 128, false => 0 });
561        }
562        curs += 1;
563    }
564    return ans;
565}
566
567/// Cursor to walk a JSON tree.
568pub struct JsonCursor {
569    key: Vec<String>,
570    sibling: Vec<usize>,
571    leaf_key: String
572}
573
574impl JsonCursor {
575    pub fn new() -> Self {
576        Self {
577            key: Vec::new(),
578            sibling: vec![0],
579            leaf_key: String::new()
580        }
581    }
582    /// Walk the tree of a JSON object finding all the leaves.
583    /// Any value that is not an object is considered a leaf.
584    /// This may be called recursively.
585    pub fn next<'a>(&mut self,obj: &'a json::JsonValue) -> Option<(String,&'a json::JsonValue)> {
586        assert!(self.key.len()+1==self.sibling.len());
587        let depth = self.key.len();
588        let pos = self.sibling[depth];
589        let mut curr = obj;
590        for i in 0..depth {
591            curr = &curr[&self.key[i]];
592        }
593        let mut entry = curr.entries();
594        for _i in 0..pos {
595            entry.next();
596        }
597        match entry.next() {
598            None => {
599                if depth==0 {
600                    return None;
601                }
602                self.key.pop();
603                self.sibling.pop();
604                return self.next(obj);
605            }
606            Some((key,val)) => {
607                self.sibling[depth] += 1;
608                if val.is_object() {
609                    self.sibling.push(0);
610                    self.key.push(key.to_string());
611                    return self.next(obj);
612                }
613                self.leaf_key = key.to_string();
614                return Some((key.to_string(),val));
615            }
616        }
617    }
618    pub fn parent<'a>(&self,obj: &'a json::JsonValue) -> Option<&'a json::JsonValue> {
619        assert!(self.key.len()+1==self.sibling.len());
620        let depth = self.key.len();
621        if depth==0 {
622            return None;
623        }
624        let mut curr = obj;
625        for i in 0..depth {
626            curr = &curr[&self.key[i]];
627        }
628        Some(curr)
629    }
630    /// Return key to current leaf as list of strings.
631    /// Note this includes the key that is returned with `next`.
632    pub fn key_path(&self) -> Vec<String> {
633        let mut ans: Vec<String> = Vec::new();
634        for i in 0..self.key.len() {
635            ans.push(self.key[i].clone())
636        }
637        ans.push(self.leaf_key.clone());
638        ans
639    }
640    /// Return key to current leaf as a path string.
641    /// This can have problems in case there are keys containing `/`,
642    /// so `key_path` should always be preferred.
643    pub fn key_path_string(&self) -> String {
644        let mut ans = String::new();
645        for i in 0..self.key.len() {
646            ans += "/";
647            ans += &self.key[i];
648        }
649        ans += "/";
650        ans += &self.leaf_key;
651        ans
652    }
653}
654
655#[test]
656fn test_json_cursor() {
657    let mut curs = JsonCursor::new();
658    let s = "{
659        \"root_str\": \"01\",
660        \"obj1\": {
661            \"list1\": [1,3,5],
662            \"str1\": \"hello\"
663        },
664        \"num1\": 1000,
665        \"obj2\": {
666            \"null1\": null
667        }
668    }";
669    let obj = json::parse(s).expect("could not parse test string");
670    let mut leaves: Vec<(String,&json::JsonValue,Vec<String>)> = Vec::new();
671    while let Some((key,leaf)) = curs.next(&obj) {
672        leaves.push((key,leaf,curs.key_path()));
673    }
674    assert_eq!(leaves.len(),5);
675
676    assert_eq!(leaves[0].0,"root_str");
677    assert_eq!(leaves[0].1.as_str().unwrap(),"01");
678    assert_eq!(leaves[0].2,vec!["root_str"]);
679
680    assert_eq!(leaves[1].0,"list1");
681    assert!(leaves[1].1.is_array());
682    assert_eq!(leaves[1].2,vec!["obj1","list1"]);
683
684    assert_eq!(leaves[2].0,"str1");
685    assert_eq!(leaves[2].1.as_str().unwrap(),"hello");
686    assert_eq!(leaves[2].2,vec!["obj1","str1"]);
687
688    assert_eq!(leaves[3].0,"num1");
689    assert_eq!(leaves[3].1.as_u16().unwrap(),1000);
690    assert_eq!(leaves[3].2,vec!["num1"]);
691
692    assert_eq!(leaves[4].0,"null1");
693    assert!(leaves[4].1.is_null());
694    assert_eq!(leaves[4].2,vec!["obj2","null1"]);
695}
696