one-brackets 0.2.0

Reader and writer for the 1C:Enterprise bracket format: lossless tree, positions in errors, serde mapping
Documentation
# one-brackets


**English** | [Русский]README.ru.md

Reader and writer for the 1C:Enterprise bracket format: `{1,"text",{nested,list}}`.

The format is used across the 1C platform: configuration files, forms, cluster
settings (`1cv8wsrv.lst`), event logs (`*.lgp`) and `ValueToStringInternal()` output.

- **Lossless values.** Quoted strings and unquoted values are kept apart, so text written
  by the platform is written back byte for byte. Checked against files written by the platform
  itself. Whitespace between items is not kept: writing uses the platform layout, so
  `{ 1 , 2 }` becomes `{1,2}`.
- **Any source.** Reads from any `std::io::Read`, streaming.
- **Any depth.** Parsing, writing, cloning, comparison, hashing and dropping need no recursion,
  so deeply nested input does not overflow the stack. serde mapping is recursive, as serde is.
- **Clear errors.** Parse and access errors carry the line, column and path to the node.
- **Editable.** Change nodes in place and write the file back.
- **serde.** Map lists to your types by field order (`serde` feature).

## Usage


```toml
[dependencies]
one-brackets = "0.2"
```

Parse, read and edit:

```rust
use one_brackets::Node;

fn main() -> one_brackets::Result<()> {
    let mut node: Node = "{2,c2f34349-b1e0-4060-8abf-3c59f38ac69f,\"Demo\"}".parse()?;

    assert_eq!(node.try_get(0)?.try_parse::<u32>()?, 2);
    assert!(node[1].as_guid().is_some());
    assert_eq!(node[2].as_quoted(), Some("Demo"));

    node[2] = Node::string("Renamed");
    assert_eq!(node.to_string(), "{2,c2f34349-b1e0-4060-8abf-3c59f38ac69f,\"Renamed\"}");

    let err = Node::try_from("{1,\n{2").unwrap_err();
    assert_eq!(
        err.to_string(),
        "unexpected end of input at line 2, column 3: list opened at line 2, column 1 is not closed",
    );
    Ok(())
}
```

Files are read with `Node::try_from(Path::new("Form.txt"))` or `Node::from_reader(reader)`
and written with `node.write_to(writer)` or `node.to_string()`.

### Mapping to your types


With `features = ["serde"]` list items map to fields in declaration order.
`Rest` keeps the items after the last field, so unknown data survives a round trip:

```rust
use one_brackets::Rest;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]

struct Cluster {
    #[serde(with = "one_brackets::atom")]
    id: String,
    name: String,
    port: u16,
    #[serde(default)]
    rest: Rest,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let text = "{e75cc2cd-9060-434e-972f-e42cc4cba705,\"Local cluster\",1541,\"SRV1C\",0}";
    let cluster: Cluster = one_brackets::from_str(text)?;
    assert_eq!(cluster.port, 1541);
    assert_eq!(cluster.rest.len(), 2);
    assert_eq!(one_brackets::to_string(&cluster)?, text);
    Ok(())
}
```

`Counted<T>` reads the common `{count,item1,item2,...}` lists. Errors point at the node:

```text
expected u8, found value `300` at line 2, column 3 (path [1, 0, 0]): number too large to fit in target type
```

### Event logs and other value sequences


`Values` streams comma-separated top-level values, such as the records of an event log,
keeping only the current one in memory:

```rust
use one_brackets::Values;

fn main() -> Result<(), one_brackets::ParseError> {
    let log = "1CV8LOG(ver 2.0)\r\n794d72bc-c31d-4245-b5a3-c765d53177ea\r\n\r\n{20260908233443,N},\r\n{20260908233445,U}";
    let mut values = Values::new(log.as_bytes());
    for _ in 0..3 {
        values.read_line()?;
    }
    for record in values {
        let record = record?;
        println!("{} at {}", record[0], record.position().unwrap());
    }
    Ok(())
}
```

## Examples


- [`cluster`]examples/cluster/main.rs: the cluster registry `1cv8wsrv.lst` with serde.
- [`v8log`]examples/v8log/main.rs: event log records, names from the `1Cv8.lgf` dictionary.

```sh
cargo run --example v8log --features serde -- path/to/20260908000000.lgp
```

## Command-line tool


[`one-brackets-cli`](cli/README.md) converts bracket files to JSON, YAML and XML and back
without losses, checks them and prints nodes by path:

```sh
cargo install one-brackets-cli
one-brackets export Form.txt -o Form.yaml
one-brackets import Form.yaml -o Form.txt
```

## Performance


Parsing allocates a `String` per value and a `Vec` per list, so large inputs are bound
by the memory allocator. The library does not pick one; the application can switch to
[mimalloc](https://crates.io/crates/mimalloc), which works on Windows, Linux and macOS:

```toml
[dependencies]
mimalloc = "0.1"
```

```rust,ignore
#[global_allocator]

static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
```

Best of 7 runs, 3 MB of forms and 4 MB of event log records:

| OS | Allocator | Forms | Event log |
|---|---|---|---|
| Windows | system | 85 ms | 67 ms |
| Windows | mimalloc | 32 ms (2.7×) | 44 ms (1.5×) |
| Linux (glibc 2.39) | system | 48 ms | 49 ms |
| Linux (glibc 2.39) | mimalloc | 32 ms (1.5×) | 40 ms (1.2×) |
| Linux (glibc 2.39) | jemalloc | 36 ms (1.3×) | 39 ms (1.25×) |

The gain is largest on Windows, whose system allocator is the slowest of these;
on Linux jemalloc is on par with mimalloc but does not build for Windows MSVC.
For event logs prefer `Values` over parsing one huge list, and process files
in parallel: logs are split into files by period. `cargo bench` measures reading and writing.

## Minimum Rust version


1.85 (edition 2024).

## License


Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion
in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above,
without any additional terms or conditions.