Skip to main content

igraph/
foreign.rs

1//! Reading and writing graphs in foreign file formats (`igraph_foreign.h`).
2//!
3//! igraph can exchange graphs with other software through a number of
4//! textual (and one binary) file formats. This module wraps all of them, in
5//! two flavours:
6//!
7//! - **file based**: `Graph::read_graph_*(path, ...)` and
8//!   `graph.write_graph_*(path, ...)` take anything implementing
9//!   [`AsRef<Path>`](std::path::Path) and open/close the file themselves
10//!   (the file handle is always closed, also on errors, and write errors
11//!   detected when flushing are reported);
12//! - **in memory**: `Graph::read_graph_*_from_str(&str, ...)` parses a string
13//!   and `graph.write_graph_*_to_string(...)` returns the serialization as a
14//!   [`String`], without touching the file system (they use the POSIX
15//!   `fmemopen`/`open_memstream` streams under the hood). igraph copies
16//!   string attributes byte by byte, so a `_to_string` writer replaces
17//!   invalid UTF-8 (only possible in attributes read from files in another
18//!   encoding, e.g. Latin-1 Pajek labels) with U+FFFD: use the file based
19//!   writer to keep the exact bytes.
20//!
21//! On top of that, [`GraphFormat`] together with [`Graph::read_graph`] and
22//! [`Graph::write_graph`] offers a format-agnostic entry point, able to guess
23//! the format from the file extension ([`GraphFormat::from_path`]).
24//!
25//! # Example
26//!
27//! ```
28//! use igraph::{foreign::GmlWriteOptions, prelude::*};
29//!
30//! // A directed triangle with a pendant vertex, serialized as an edge list...
31//! let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (2, 3)], 4, true).unwrap();
32//! let text = g.write_graph_edgelist_to_string().unwrap();
33//! assert_eq!(text, "0 1\n1 2\n2 0\n2 3\n");
34//!
35//! // ...and parsed back: exactly the same graph.
36//! let h = Graph::read_graph_edgelist_from_str(&text, 0, true).unwrap();
37//! assert!(g.is_same_graph(&h).unwrap());
38//!
39//! // GML round trip of Zachary's karate club, through a temporary file.
40//! let karate = Graph::famous("Zachary").unwrap();
41//! let path = std::env::temp_dir().join(format!("igraph-doc-foreign-{}.gml", std::process::id()));
42//! karate.write_graph_gml(&path, &GmlWriteOptions::default()).unwrap();
43//! let k = Graph::read_graph_gml(&path).unwrap();
44//! std::fs::remove_file(&path).unwrap();
45//! assert!(!k.is_directed());
46//! assert_eq!(k, karate); // GML keeps vertex ids and edges
47//! ```
48//!
49//! # Supported formats
50//!
51//! | Format | Read | Write | Attributes (after [`attributes::enable`](crate::attributes::enable)) | Notes |
52//! |---|---|---|---|---|
53//! | Edge list | [`read_graph_edgelist`](Graph::read_graph_edgelist) | [`write_graph_edgelist`](Graph::write_graph_edgelist) | none | whitespace separated 0-based vertex ids |
54//! | NCOL | [`read_graph_ncol`](Graph::read_graph_ncol) | [`write_graph_ncol`](Graph::write_graph_ncol) | vertex `name`, edge `weight` | symbolic (named) weighted edge list of the LGL software |
55//! | LGL | [`read_graph_lgl`](Graph::read_graph_lgl) | [`write_graph_lgl`](Graph::write_graph_lgl) | vertex `name`, edge `weight` | adjacency-list-like, `# vertex` headers |
56//! | Pajek | [`read_graph_pajek`](Graph::read_graph_pajek) | [`write_graph_pajek`](Graph::write_graph_pajek) | `name`, `x`/`y`/`z`, `color`, bipartite `type`, edge `weight`, ... | `.net` files, 1-based ids |
57//! | GraphML | [`read_graph_graphml`](Graph::read_graph_graphml) | [`write_graph_graphml`](Graph::write_graph_graphml), [`write_graph_graphml_with`](Graph::write_graph_graphml_with) | all (typed, with defaults), node ids as vertex `id` | XML; reading requires igraph built with libxml2; the in-memory writer [`write_graph_graphml_to_string`](Graph::write_graph_graphml_to_string) takes the `prefixattr` flag of `write_graph_graphml_with` |
58//! | GML | [`read_graph_gml`](Graph::read_graph_gml) | [`write_graph_gml`](Graph::write_graph_gml) | numeric and string ones, numeric vertex `id` | see [`GmlWriteOptions`] |
59//! | DIMACS flow | [`read_graph_dimacs_flow`](Graph::read_graph_dimacs_flow) | [`write_graph_dimacs_flow`](Graph::write_graph_dimacs_flow) | none (capacities in [`DimacsFlow`]) | max-flow / edge problems |
60//! | graph database | [`read_graph_graphdb`](Graph::read_graph_graphdb) | — | none | binary, see [`read_graph_graphdb_from_bytes`](Graph::read_graph_graphdb_from_bytes) |
61//! | UCINET DL | [`read_graph_dl`](Graph::read_graph_dl) | — | vertex `name`, edge `weight` | full matrix, edge list and node list forms |
62//! | Graphviz DOT | — | [`write_graph_dot`](Graph::write_graph_dot) | all | output only |
63//! | LEDA | — | [`write_graph_leda`](Graph::write_graph_leda) | one vertex and one edge attribute | output only |
64//!
65//! Every file based function has an in-memory twin with the `_from_str` /
66//! `_to_string` suffix (`_from_bytes` for the binary graph database format),
67//! taking the same arguments but the path. The one exception is GraphML:
68//! [`write_graph_graphml_to_string(prefixattr)`](Graph::write_graph_graphml_to_string)
69//! is the twin of
70//! [`write_graph_graphml_with(path, prefixattr)`](Graph::write_graph_graphml_with)
71//! (pass `false` for the behaviour of
72//! [`write_graph_graphml(path)`](Graph::write_graph_graphml)).
73//! [`SafeLocale`] and [`with_safe_locale`] bind igraph's locale helpers.
74//!
75//! # Attributes
76//!
77//! Many formats carry vertex names, edge weights and other attributes. igraph
78//! stores them through a pluggable, **process-wide** *attribute handler*,
79//! which is off by default and is turned on by
80//! [`attributes::enable`](crate::attributes::enable) (see the
81//! [`attributes`](crate::attributes) module for the typed accessors):
82//!
83//! - **without** the handler, readers parse attributes (validating their
84//!   syntax) but discard them: only the structure of the graph is returned.
85//!   The options asking to store names or weights ([`NcolLglOptions`]) are
86//!   accepted and harmless. Writers see no attribute: those asked to export a
87//!   named one (e.g. the `names`/`weights` arguments of
88//!   [`write_graph_ncol`](Graph::write_graph_ncol)) emit an igraph *warning*
89//!   (see [`take_warnings`](crate::error::take_warnings)) and fall back to
90//!   plain vertex ids, and GraphML/GML/DOT files are written without
91//!   attributes;
92//! - **with** the handler (call [`enable`](crate::attributes::enable)
93//!   *before* reading), readers store what they find in the file as graph,
94//!   vertex and edge attributes (the "Attributes" column above), and the
95//!   writers export the attributes of the graph. Values missing for some
96//!   elements are NaN for numeric attributes, `""` for strings and `false`
97//!   for booleans, unless the format has its own defaults (a missing
98//!   NCOL/LGL weight is 1, a GraphML `<key>` may declare a `<default>`,
99//!   Pajek parameters take Pajek's defaults, see the readers); GraphML and
100//!   GML omit NaN values when writing.
101//!
102//! What each format keeps, with the handler on (the individual readers and
103//! writers have the details):
104//!
105//! | Format | Read into attributes | Written from attributes |
106//! |---|---|---|
107//! | Edge list, DIMACS, graph database | nothing | nothing |
108//! | NCOL, LGL | vertex `name` (string), edge `weight` (numeric), as selected by [`NcolLglOptions`] | the vertex / edge attributes named by the `names` / `weights` arguments |
109//! | UCINET DL | vertex `name` (labels), edge `weight` (values) | — |
110//! | Pajek | vertex `name`, `x`/`y`/`z`, `color`, shapes and the other Pajek parameters, bipartite `type` (boolean), edge `weight` and edge parameters | the attributes named after Pajek parameters; other attributes are ignored |
111//! | GraphML | every `<key>`, typed (boolean, numeric, string), for graph, vertices and edges; the node `id`s as the string vertex attribute `id` | every graph, vertex and edge attribute (typed keys), optionally prefixed (`g_`/`v_`/`e_`, [`write_graph_graphml_with`](Graph::write_graph_graphml_with)) |
112//! | GML | every numeric or string field of the graph, node and edge records, including the numeric node `id` | numeric and string attributes (booleans as 0/1); a numeric vertex `id` supplies the node ids |
113//! | DOT | — | every graph, vertex and edge attribute |
114//! | LEDA | — | one vertex and one edge attribute, named in the call |
115//!
116//! Two GraphML caveats: the node ids land in a string `id` vertex
117//! attribute (unless a vertex key already defines an attribute named `id`), which the
118//! GraphML writer then exports as an ordinary `<key>`, so a second round
119//! trip carries it as data; and when the `<edge>` elements have `id`s, igraph
120//! 1.0.0 and 1.0.1 also create a string `id` *edge* attribute but, because
121//! of a bug in `src/io/graphml.c`, fill it with the *node* ids (in node
122//! order, padded with `""` if there are more edges than nodes): don't rely on it
123//! (delete it with
124//! [`remove_edge_attr`](Graph::remove_edge_attr) if it gets in the way). See
125//! [`read_graph_graphml`](Graph::read_graph_graphml).
126//!
127//! ```
128//! use igraph::{attributes, foreign::NcolLglOptions, prelude::*};
129//!
130//! attributes::enable().unwrap(); // before reading!
131//! let text = "rome paris 1420\nparis london 460\nlondon rome\n";
132//! let g = Graph::read_graph_ncol_from_str(text, &[], &NcolLglOptions::default()).unwrap();
133//! assert_eq!(g.vertex_attr_str_values("name", ..).unwrap(), ["rome", "paris", "london"]);
134//! // A missing weight defaults to 1 when at least one edge has an explicit weight.
135//! assert_eq!(g.edge_attr_numeric_values("weight", ..).unwrap(), [1420.0, 460.0, 1.0]);
136//! // Writers use the attributes they are asked for.
137//! let out = g.write_graph_ncol_to_string(Some("name"), Some("weight")).unwrap();
138//! assert_eq!(out, "rome paris 1420\nparis london 460\nrome london 1\n");
139//! ```
140//!
141//! Data that igraph returns through explicit output arguments does not need
142//! the handler, e.g. the capacities and source/target vertices of a DIMACS
143//! file, returned in [`DimacsFlow`]. When only the vertex naming of an NCOL
144//! file matters, the `predefnames` argument of
145//! [`read_graph_ncol`](Graph::read_graph_ncol) fixes it without attributes:
146//! vertex `i` then is the `i`-th predefined name.
147//!
148//! # Locale and threads
149//!
150//! The parsers and writers assume that the C locale uses a decimal *point*.
151//! Rust programs start in the `"C"` locale, so nothing needs to be done unless
152//! some library called `setlocale`; in that case wrap the I/O with
153//! [`SafeLocale`] or [`with_safe_locale`].
154//!
155//! All the functions can be called from several threads at once (but see
156//! [`SafeLocale`] for platforms without per-thread locales). The GML
157//! reader of igraph 1.0.0 and 1.0.1 is not reentrant (it uses static
158//! buffers), and so is the default GML `Creator` line (it uses `ctime`): these
159//! wrappers serialize them internally with a process-wide lock.
160//!
161//! # See also
162//!
163//! - [`Graph::from_edges`] and [`Graph::famous`] (in
164//!   [`constructors`](crate::constructors)) to build graphs in code;
165//! - [`Graph::get_adjacency`] and the rest of the
166//!   [`conversion`](crate::conversion) module to export graphs as matrices;
167//! - [`Graph::is_same_graph`] to check that a round trip kept the vertex ids,
168//!   [`Graph::isomorphic`] when a format (NCOL, LGL, bipartite Pajek)
169//!   relabels the vertices;
170//! - [`Graph::maxflow`] and [`Graph::st_mincut`] to solve the problems read
171//!   from DIMACS files.
172//!
173//! The C documentation of the whole chapter is at
174//! <https://igraph.org/c/html/latest/igraph-Foreign.html>.
175
176use crate::{
177    constants::AddWeights,
178    error::{Error, ErrorKind, Result},
179    ffi::*,
180    graph::{Graph, VertexId},
181    igraph_call,
182    strvector::StrVector,
183    vector::{Vector, VectorInt},
184};
185use std::{
186    ffi::{CStr, CString, c_char, c_void},
187    marker::PhantomData,
188    path::Path,
189    sync::Mutex,
190};
191
192/// Serializes the non-reentrant parts of igraph's GML code (still present in
193/// igraph 1.0.0 and 1.0.1):
194///
195/// - `igraph_read_graph_gml` keeps intermediate strings in function-local
196///   `static` buffers (`strid` and `igraph_i_gml_tostring` in
197///   `src/io/gml.c`), so concurrent calls from different threads corrupt each
198///   other (and may crash);
199/// - `igraph_write_graph_gml` with a `NULL` creator formats the current time
200///   with `ctime`, which returns (and igraph then modifies in place) a
201///   process-wide static buffer.
202///
203/// All the other readers and writers are reentrant.
204static GML_LOCK: Mutex<()> = Mutex::new(());
205
206/// Acquires [`GML_LOCK`].
207fn gml_lock() -> std::sync::MutexGuard<'static, ()> {
208    // A poisoned lock only means another thread panicked: the C state is fine.
209    GML_LOCK.lock().unwrap_or_else(|e| e.into_inner())
210}
211
212/// Runs the GML reader while holding [`GML_LOCK`].
213fn read_gml(source: Source<'_>) -> Result<Graph> {
214    let _guard = gml_lock();
215    read_with(source, |g, f| unsafe { igraph_read_graph_gml(g, f) })
216}
217
218/// Converts a Rust count/index into an `igraph_int_t`, rejecting values that
219/// would wrap around to negative numbers.
220fn to_int(value: usize, what: &str) -> Result<igraph_int_t> {
221    igraph_int_t::try_from(value)
222        .map_err(|_| Error::invalid(format!("{what} {value} is too large")))
223}
224
225/// Encodes `&` (unless `only_quot`) and `"` as XML entities, as igraph does
226/// for GML string values.
227fn gml_entity_encode(s: &str, only_quot: bool) -> String {
228    let mut out = String::with_capacity(s.len());
229    for c in s.chars() {
230        match c {
231            '&' if !only_quot => out.push_str("&amp;"),
232            '"' => out.push_str("&quot;"),
233            c => out.push(c),
234        }
235    }
236    out
237}
238
239// ---------------------------------------------------------------------------
240// C stream plumbing
241// ---------------------------------------------------------------------------
242
243/// Converts a path into a NUL terminated C string.
244fn path_to_cstring(path: &Path) -> Result<CString> {
245    #[cfg(unix)]
246    let bytes = {
247        use std::os::unix::ffi::OsStrExt;
248        path.as_os_str().as_bytes().to_vec()
249    };
250    #[cfg(not(unix))]
251    let bytes = path
252        .to_str()
253        .ok_or_else(|| Error::invalid(format!("path {} is not valid UTF-8", path.display())))?
254        .as_bytes()
255        .to_vec();
256    CString::new(bytes).map_err(|_| {
257        Error::invalid(format!(
258            "path {} contains an interior NUL byte",
259            path.display()
260        ))
261    })
262}
263
264/// Converts an optional attribute name into an optional C string.
265fn opt_cstring(name: Option<&str>, what: &str) -> Result<Option<CString>> {
266    name.map(|s| {
267        CString::new(s).map_err(|_| Error::invalid(format!("{what} contains an interior NUL byte")))
268    })
269    .transpose()
270}
271
272/// Pointer of an optional C string (`NULL` for `None`).
273fn opt_ptr(s: &Option<CString>) -> *const c_char {
274    s.as_ref().map_or(std::ptr::null(), |c| c.as_ptr())
275}
276
277/// An owned C `FILE *`, closed on drop. The lifetime ties it to the memory
278/// buffer it may read from (for `fmemopen` streams).
279struct CFile<'a> {
280    ptr: *mut FILE,
281    _buffer: PhantomData<&'a [u8]>,
282}
283
284impl<'a> CFile<'a> {
285    /// Opens a file with `fopen`.
286    fn open(path: &Path, mode: &CStr) -> Result<CFile<'static>> {
287        let c_path = path_to_cstring(path)?;
288        let ptr = unsafe { fopen(c_path.as_ptr(), mode.as_ptr()) };
289        if ptr.is_null() {
290            let os = std::io::Error::last_os_error();
291            return Err(Error::new(
292                ErrorKind::File,
293                format!("cannot open {}: {os}", path.display()),
294            ));
295        }
296        Ok(CFile {
297            ptr,
298            _buffer: PhantomData,
299        })
300    }
301
302    /// Opens a read-only stream over an in-memory buffer (`fmemopen`).
303    fn from_bytes(data: &'a [u8]) -> Result<CFile<'a>> {
304        let ptr = if data.is_empty() {
305            // `fmemopen` may reject zero-sized buffers: an empty temporary
306            // file is an equivalent empty stream.
307            unsafe { tmpfile() }
308        } else {
309            // In read mode the buffer is never written to.
310            unsafe { fmemopen(data.as_ptr() as *mut c_void, data.len(), c"rb".as_ptr()) }
311        };
312        if ptr.is_null() {
313            let os = std::io::Error::last_os_error();
314            return Err(Error::new(
315                ErrorKind::File,
316                format!("cannot open a memory stream: {os}"),
317            ));
318        }
319        Ok(CFile {
320            ptr,
321            _buffer: PhantomData,
322        })
323    }
324
325    /// Closes the stream with `fclose`, reporting failures: earlier write
326    /// errors that igraph did not check (its error indicator is set) and
327    /// buffered writes that could not be completed.
328    fn close(mut self) -> Result<()> {
329        let ptr = std::mem::replace(&mut self.ptr, std::ptr::null_mut());
330        let had_error = unsafe { ferror(ptr) } != 0;
331        if unsafe { fclose(ptr) } != 0 || had_error {
332            let os = std::io::Error::last_os_error();
333            return Err(Error::new(
334                ErrorKind::File,
335                format!("cannot close the stream: {os}"),
336            ));
337        }
338        Ok(())
339    }
340}
341
342impl Drop for CFile<'_> {
343    fn drop(&mut self) {
344        if !self.ptr.is_null() {
345            unsafe { fclose(self.ptr) };
346        }
347    }
348}
349
350/// A growable in-memory output stream (`open_memstream`).
351struct MemSink {
352    file: *mut FILE,
353    /// Heap cell (from `Box::into_raw`) whose two fields `open_memstream`
354    /// updates with the buffer address and size. It is only accessed through
355    /// this raw pointer, so the pointers held by the C library stay valid
356    /// until `Drop` reclaims it.
357    loc: *mut (*mut c_char, usize),
358}
359
360impl MemSink {
361    fn new() -> Result<Self> {
362        let loc = Box::into_raw(Box::new((std::ptr::null_mut::<c_char>(), 0usize)));
363        // SAFETY: `loc` is a valid, uniquely owned allocation.
364        let file = unsafe { open_memstream(&raw mut (*loc).0, &raw mut (*loc).1) };
365        if file.is_null() {
366            let os = std::io::Error::last_os_error();
367            // SAFETY: the stream was not created, nobody else points to `loc`.
368            drop(unsafe { Box::from_raw(loc) });
369            return Err(Error::new(
370                ErrorKind::File,
371                format!("cannot open a memory stream: {os}"),
372            ));
373        }
374        Ok(Self { file, loc })
375    }
376
377    /// Closes the stream and returns what was written.
378    ///
379    /// Bytes that are not valid UTF-8 (possible only in string attributes
380    /// that igraph read from non-UTF-8 files) are replaced by U+FFFD.
381    fn finish(mut self) -> Result<String> {
382        let file = std::mem::replace(&mut self.file, std::ptr::null_mut());
383        let had_error = unsafe { ferror(file) } != 0;
384        if unsafe { fclose(file) } != 0 || had_error {
385            return Err(Error::new(
386                ErrorKind::File,
387                "cannot finalize the memory stream",
388            ));
389        }
390        // SAFETY: `fclose` stored the final buffer address and size.
391        let (buf, size) = unsafe { *self.loc };
392        if buf.is_null() {
393            return Ok(String::new());
394        }
395        let bytes = unsafe { std::slice::from_raw_parts(buf as *const u8, size) };
396        Ok(String::from_utf8_lossy(bytes).into_owned())
397        // `self` is dropped here, freeing the buffer.
398    }
399}
400
401impl Drop for MemSink {
402    fn drop(&mut self) {
403        if !self.file.is_null() {
404            unsafe { fclose(self.file) };
405        }
406        // SAFETY: the stream is closed, so the C library no longer uses
407        // `loc`; the buffer it holds was allocated by the C library and must
408        // be freed with `free` (`free(NULL)` is a no-op).
409        let loc = unsafe { Box::from_raw(self.loc) };
410        unsafe { free(loc.0 as *mut c_void) };
411    }
412}
413
414unsafe extern "C" {
415    /// `ferror` from `<stdio.h>` (not in the generated bindings): whether
416    /// the error indicator of the stream is set.
417    fn ferror(stream: *mut FILE) -> std::ffi::c_int;
418}
419
420/// Where a reader takes its input from.
421enum Source<'a> {
422    Path(&'a Path),
423    Bytes(&'a [u8]),
424}
425
426impl<'a> Source<'a> {
427    fn open(&self) -> Result<CFile<'a>> {
428        match *self {
429            Source::Path(p) => CFile::open(p, c"rb"),
430            Source::Bytes(b) => CFile::from_bytes(b),
431        }
432    }
433}
434
435/// Runs a reader (a C function initializing a graph from a stream).
436fn read_with(
437    source: Source<'_>,
438    f: impl FnOnce(*mut igraph_t, *mut FILE) -> igraph_error_t,
439) -> Result<Graph> {
440    let file = source.open()?;
441    let graph = Graph::init_with(|g| f(g, file.ptr))?;
442    // Closing a read stream can't lose data: ignore its outcome.
443    let _ = file.close();
444    Ok(graph)
445}
446
447/// Runs a writer on a newly created (truncated) file.
448fn write_to_path(path: &Path, f: impl FnOnce(*mut FILE) -> igraph_error_t) -> Result<()> {
449    let file = CFile::open(path, c"wb")?;
450    igraph_call!(f(file.ptr))?;
451    file.close()
452}
453
454/// Runs a writer on an in-memory stream and returns the output.
455fn write_to_string(f: impl FnOnce(*mut FILE) -> igraph_error_t) -> Result<String> {
456    let sink = MemSink::new()?;
457    igraph_call!(f(sink.file))?;
458    sink.finish()
459}
460
461// ---------------------------------------------------------------------------
462// Options and results
463// ---------------------------------------------------------------------------
464
465/// Options of the NCOL and LGL readers ([`Graph::read_graph_ncol`],
466/// [`Graph::read_graph_lgl`]).
467///
468/// The defaults are: store names, add weights only if present in the file,
469/// undirected graph (the LGL software only handles undirected graphs).
470///
471/// `names` and `weights` only matter once the attribute handler is enabled
472/// with [`attributes::enable`](crate::attributes::enable) (see the
473/// [module docs](self#attributes)); without it they are harmless.
474///
475/// # Examples
476/// ```
477/// use igraph::{foreign::NcolLglOptions, prelude::*};
478/// let opts = NcolLglOptions::default().with_directed(true).with_weights(AddWeights::No);
479/// assert!(opts.names && opts.directed);
480/// let g = Graph::read_graph_ncol_from_str("a b 2\nb c 3\n", &[], &opts).unwrap();
481/// assert!(g.is_directed());
482/// assert_eq!(g.edge_list(), vec![(0, 1), (1, 2)]);
483/// ```
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485pub struct NcolLglOptions {
486    /// Whether to store the symbolic vertex names as the `name` string vertex
487    /// attribute.
488    pub names: bool,
489    /// Whether to store the edge weights as the `weight` numeric edge
490    /// attribute: [`AddWeights::Yes`] always (edges without a weight get 1),
491    /// [`AddWeights::IfPresent`] only if at least one weight is given in the
492    /// file (again with 1 for the others), [`AddWeights::No`] never.
493    pub weights: AddWeights,
494    /// Whether to create a directed graph: the formats carry no information
495    /// about directedness.
496    pub directed: bool,
497}
498
499impl Default for NcolLglOptions {
500    fn default() -> Self {
501        Self {
502            names: true,
503            weights: AddWeights::IfPresent,
504            directed: false,
505        }
506    }
507}
508
509impl NcolLglOptions {
510    /// Sets [`directed`](Self::directed).
511    pub fn with_directed(mut self, directed: bool) -> Self {
512        self.directed = directed;
513        self
514    }
515
516    /// Sets [`names`](Self::names).
517    pub fn with_names(mut self, names: bool) -> Self {
518        self.names = names;
519        self
520    }
521
522    /// Sets [`weights`](Self::weights).
523    pub fn with_weights(mut self, weights: AddWeights) -> Self {
524        self.weights = weights;
525        self
526    }
527}
528
529/// Options of the GML writer ([`Graph::write_graph_gml`]).
530///
531/// The default writes igraph's own vertex ids (or the numeric `id` vertex
532/// attribute, if present), encodes all special characters as entities, and
533/// writes a `Creator` line mentioning the igraph version and the current date
534/// and time (use [`with_creator`](Self::with_creator)`("")` for reproducible
535/// output).
536///
537/// # Examples
538/// ```
539/// use igraph::{foreign::GmlWriteOptions, prelude::*};
540/// let g = Graph::from_edges(&[(0, 1)], 2, true).unwrap();
541/// let opts = GmlWriteOptions::default().with_creator("").with_ids(&[7.0, 9.0]);
542/// let gml = g.write_graph_gml_to_string(&opts).unwrap();
543/// let lines: Vec<&str> = gml.lines().map(str::trim).collect();
544/// assert_eq!(
545///     lines,
546///     [
547///         "Version 1", "graph", "[", "directed 1",
548///         "node", "[", "id 7", "]",
549///         "node", "[", "id 9", "]",
550///         "edge", "[", "source 7", "target 9", "]",
551///         "]",
552///     ]
553/// );
554/// ```
555#[derive(Debug, Clone, Copy, PartialEq, Default)]
556pub struct GmlWriteOptions<'a> {
557    /// Encode only `"` characters as entities, nothing else
558    /// (`IGRAPH_WRITE_GML_ENCODE_ONLY_QUOT_SW`); useful to re-export files
559    /// whose entities igraph passed through undecoded.
560    pub encode_only_quot: bool,
561    /// Numeric vertex ids to write in the `id` fields instead of igraph's
562    /// vertex ids (one per vertex). With `None`, a numeric `id` vertex
563    /// attribute is used if there is one (the GML reader creates it when the
564    /// [attribute handler](crate::attributes::enable) is on, so a GML round
565    /// trip keeps the original ids), otherwise igraph's vertex ids. If some
566    /// value is not an integer (or is NaN/infinite), or some value is
567    /// repeated, igraph emits a warning (see
568    /// [`take_warnings`](crate::error::take_warnings)) and ignores all of
569    /// them, writing its own vertex ids instead.
570    pub ids: Option<&'a [f64]>,
571    /// Text of the `Creator` line: `None` writes the igraph version plus the
572    /// current date and time, `Some("")` omits the line, anything else is
573    /// written as a GML string, with `"` (and `&`, unless
574    /// [`encode_only_quot`](Self::encode_only_quot)) encoded as XML entities.
575    pub creator: Option<&'a str>,
576}
577
578impl<'a> GmlWriteOptions<'a> {
579    /// Sets [`encode_only_quot`](Self::encode_only_quot).
580    pub fn with_encode_only_quot(mut self, only_quot: bool) -> Self {
581        self.encode_only_quot = only_quot;
582        self
583    }
584
585    /// Sets [`ids`](Self::ids).
586    pub fn with_ids(mut self, ids: &'a [f64]) -> Self {
587        self.ids = Some(ids);
588        self
589    }
590
591    /// Sets [`creator`](Self::creator).
592    pub fn with_creator(mut self, creator: &'a str) -> Self {
593        self.creator = Some(creator);
594        self
595    }
596}
597
598/// The problem described by a DIMACS file, see [`DimacsFlow`].
599#[derive(Debug, Clone, PartialEq)]
600pub enum DimacsProblem {
601    /// A maximum flow problem (`p max`).
602    Max {
603        /// The source vertex (0-based igraph id; DIMACS ids start from 1),
604        /// `None` if the file has no `n <id> s` line.
605        source: Option<VertexId>,
606        /// The target vertex (0-based igraph id), `None` if the file has no
607        /// `n <id> t` line.
608        target: Option<VertexId>,
609        /// The capacity of each edge, in edge id order.
610        capacity: Vec<f64>,
611    },
612    /// An "edge" problem (`p edge`), e.g. a graph coloring instance (see
613    /// [`Graph::vertex_coloring_greedy`] to solve one).
614    Edge {
615        /// The integer label of each vertex: its 1-based DIMACS index (igraph
616        /// 1.0.0 and 1.0.1 cannot parse the `n` lines that would change it).
617        labels: Vec<i64>,
618    },
619}
620
621/// The content of a DIMACS flow file, as returned by
622/// [`Graph::read_graph_dimacs_flow`].
623///
624/// For a [`DimacsProblem::Max`] instance, pass the capacities to
625/// [`Graph::maxflow`], [`Graph::maxflow_value`] or [`Graph::st_mincut`] to
626/// solve it.
627#[derive(Debug, Clone, PartialEq)]
628pub struct DimacsFlow {
629    /// The graph.
630    pub graph: Graph,
631    /// The problem type string of the `p` line (`"max"` or `"edge"`).
632    pub problem_name: String,
633    /// The problem data.
634    pub problem: DimacsProblem,
635}
636
637/// Graph file formats known to igraph, for the format-agnostic
638/// [`Graph::read_graph`] and [`Graph::write_graph`].
639///
640/// The variants list the file extensions recognized by
641/// [`from_path`](Self::from_path); [`can_read`](Self::can_read) and
642/// [`can_write`](Self::can_write) tell which direction the generic functions
643/// support.
644#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
645pub enum GraphFormat {
646    /// Plain edge list of 0-based vertex ids (`.txt`, `.edgelist`, `.edges`, `.el`).
647    Edgelist,
648    /// NCOL symbolic edge list (`.ncol`).
649    Ncol,
650    /// LGL format (`.lgl`).
651    Lgl,
652    /// Pajek (`.net`, `.pajek`).
653    Pajek,
654    /// GraphML (`.graphml`, `.xml`).
655    GraphMl,
656    /// GML (`.gml`).
657    Gml,
658    /// DIMACS flow format (`.dimacs`, `.max`); only [`Graph::read_graph`]
659    /// supports it generically (it discards the problem data).
660    DimacsFlow,
661    /// Binary ARG graph database format (`.graphdb`, and the `.A00`...`.A99`,
662    /// `.B00`...`.B99` suffixes of the database's graph pairs); read only.
663    GraphDb,
664    /// UCINET DL (`.dl`); read only.
665    Dl,
666    /// Graphviz DOT (`.dot`, `.gv`); write only.
667    Dot,
668    /// LEDA native format (`.gw`, `.leda`); write only.
669    Leda,
670}
671
672impl GraphFormat {
673    /// Guesses the format from the extension of `path` (case insensitive),
674    /// or `None` if the extension is unknown.
675    ///
676    /// ```
677    /// use igraph::foreign::GraphFormat;
678    /// assert_eq!(GraphFormat::from_path("karate.GML"), Some(GraphFormat::Gml));
679    /// assert_eq!(GraphFormat::from_path("/tmp/g.net"), Some(GraphFormat::Pajek));
680    /// assert_eq!(GraphFormat::from_path("noext"), None);
681    /// ```
682    pub fn from_path(path: impl AsRef<Path>) -> Option<Self> {
683        let ext = path.as_ref().extension()?.to_str()?.to_ascii_lowercase();
684        Some(match ext.as_str() {
685            "txt" | "edgelist" | "edges" | "el" => Self::Edgelist,
686            "ncol" => Self::Ncol,
687            "lgl" => Self::Lgl,
688            "net" | "pajek" => Self::Pajek,
689            "graphml" | "xml" => Self::GraphMl,
690            "gml" => Self::Gml,
691            "dimacs" | "max" => Self::DimacsFlow,
692            "graphdb" => Self::GraphDb,
693            e if e.len() == 3
694                && (e.starts_with('a') || e.starts_with('b'))
695                && e[1..].chars().all(|c| c.is_ascii_digit()) =>
696            {
697                Self::GraphDb
698            }
699            "dl" => Self::Dl,
700            "dot" | "gv" => Self::Dot,
701            "gw" | "leda" => Self::Leda,
702            _ => return None,
703        })
704    }
705
706    /// Whether igraph can read this format.
707    pub fn can_read(self) -> bool {
708        !matches!(self, Self::Dot | Self::Leda)
709    }
710
711    /// Whether igraph can write this format (DIMACS needs extra data: use
712    /// [`Graph::write_graph_dimacs_flow`] directly).
713    pub fn can_write(self) -> bool {
714        !matches!(self, Self::GraphDb | Self::Dl | Self::DimacsFlow)
715    }
716}
717
718// ---------------------------------------------------------------------------
719// Readers
720// ---------------------------------------------------------------------------
721
722impl igraph_t {
723    /// Reads a graph from a file in the given `format`, with default options.
724    ///
725    /// `directed` is used by the formats that don't encode directedness
726    /// (edge list, NCOL, LGL, DIMACS, graph database, DL); Pajek, GraphML and
727    /// GML files specify it themselves. Use [`GraphFormat::from_path`] to
728    /// guess the format from the extension. The defaults are: no extra
729    /// isolated vertices for edge lists, [`NcolLglOptions::default`] (plus
730    /// `directed`) for NCOL/LGL, the first graph of a GraphML document; the
731    /// problem data of a DIMACS file is discarded (use
732    /// [`read_graph_dimacs_flow`](Self::read_graph_dimacs_flow) to get it).
733    ///
734    /// # Errors
735    /// [`ErrorKind::Unimplemented`] if the format can't be read (DOT, LEDA),
736    /// plus the errors of the specific reader.
737    ///
738    /// # Examples
739    /// ```
740    /// use igraph::{foreign::GraphFormat, prelude::*};
741    /// let path = std::env::temp_dir().join(format!("igraph-doc-read-{}.net", std::process::id()));
742    /// std::fs::write(&path, "*Vertices 3\n*Edges\n1 2\n2 3\n").unwrap();
743    /// let format = GraphFormat::from_path(&path).unwrap();
744    /// let g = Graph::read_graph(&path, format, false).unwrap();
745    /// std::fs::remove_file(&path).unwrap();
746    /// assert_eq!(g.edge_list(), vec![(0, 1), (1, 2)]);
747    /// ```
748    pub fn read_graph(
749        path: impl AsRef<Path>,
750        format: GraphFormat,
751        directed: bool,
752    ) -> Result<Graph> {
753        let path = path.as_ref();
754        match format {
755            GraphFormat::Edgelist => Self::read_graph_edgelist(path, 0, directed),
756            GraphFormat::Ncol => Self::read_graph_ncol(
757                path,
758                &[],
759                &NcolLglOptions::default().with_directed(directed),
760            ),
761            GraphFormat::Lgl => {
762                Self::read_graph_lgl(path, &NcolLglOptions::default().with_directed(directed))
763            }
764            GraphFormat::Pajek => Self::read_graph_pajek(path),
765            GraphFormat::GraphMl => Self::read_graph_graphml(path, 0),
766            GraphFormat::Gml => Self::read_graph_gml(path),
767            GraphFormat::DimacsFlow => Ok(Self::read_graph_dimacs_flow(path, directed)?.graph),
768            GraphFormat::GraphDb => Self::read_graph_graphdb(path, directed),
769            GraphFormat::Dl => Self::read_graph_dl(path, directed),
770            GraphFormat::Dot | GraphFormat::Leda => Err(Error::new(
771                ErrorKind::Unimplemented,
772                format!("igraph cannot read the {format:?} format"),
773            )),
774        }
775    }
776
777    /// Reads an edge list file: an even number of non-negative integers
778    /// (0-based vertex ids) separated by whitespace, conventionally one
779    /// `from to` pair per line.
780    ///
781    /// The graph has `max(n, largest id + 1)` vertices, so `n = 0` is always
782    /// safe; a larger `n` adds isolated vertices. See
783    /// [`read_graph_ncol`](Self::read_graph_ncol) for files with symbolic
784    /// vertex names. Time complexity: O(|V| + |E|).
785    ///
786    /// Binds [`igraph_read_graph_edgelist`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_edgelist).
787    ///
788    /// # Errors
789    /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
790    /// for syntax errors (non-integers, odd number of ids...).
791    pub fn read_graph_edgelist(path: impl AsRef<Path>, n: usize, directed: bool) -> Result<Graph> {
792        let n = to_int(n, "vertex count")?;
793        read_with(Source::Path(path.as_ref()), |g, f| unsafe {
794            igraph_read_graph_edgelist(g, f, n, directed)
795        })
796    }
797
798    /// Parses an edge list from a string, see
799    /// [`read_graph_edgelist`](Self::read_graph_edgelist).
800    ///
801    /// # Examples
802    /// ```
803    /// use igraph::prelude::*;
804    /// let g = Graph::read_graph_edgelist_from_str("0 1\n1 2\n2 0\n", 5, false).unwrap();
805    /// assert_eq!((g.vcount(), g.ecount()), (5, 3)); // two isolated vertices from `n`
806    /// let bad = Graph::read_graph_edgelist_from_str("0 1 2", 0, false).unwrap_err();
807    /// assert_eq!(bad.kind(), ErrorKind::Parse);
808    /// ```
809    pub fn read_graph_edgelist_from_str(text: &str, n: usize, directed: bool) -> Result<Graph> {
810        let n = to_int(n, "vertex count")?;
811        read_with(Source::Bytes(text.as_bytes()), |g, f| unsafe {
812            igraph_read_graph_edgelist(g, f, n, directed)
813        })
814    }
815
816    fn read_ncol_impl(
817        source: Source<'_>,
818        predefnames: &[&str],
819        options: &NcolLglOptions,
820    ) -> Result<Graph> {
821        if predefnames.iter().any(|s| s.contains('\0')) {
822            return Err(Error::invalid(
823                "predefined vertex names must not contain NUL bytes",
824            ));
825        }
826        let names: Option<StrVector> =
827            (!predefnames.is_empty()).then(|| predefnames.iter().collect());
828        let names_ptr = names
829            .as_ref()
830            .map_or(std::ptr::null(), |n| n as *const StrVector);
831        read_with(source, |g, f| unsafe {
832            igraph_read_graph_ncol(
833                g,
834                f,
835                names_ptr,
836                options.names,
837                options.weights.into(),
838                options.directed,
839            )
840        })
841    }
842
843    /// Reads an NCOL file, the symbolic weighted edge list format of the
844    /// Large Graph Layout software.
845    ///
846    /// Each line is `name1 name2 [weight]`: two vertex names without
847    /// whitespace, optionally followed by a (possibly negative, possibly
848    /// scientific notation) weight. Vertex ids are assigned in the order the
849    /// names first appear, after the names listed in `predefnames` (which get
850    /// ids `0..predefnames.len()`; unknown names found in the file extend
851    /// them, and duplicate predefined names are accepted, each with an igraph
852    /// warning). An empty `predefnames` means none.
853    /// Multi-edges and loops are accepted. Time complexity:
854    /// O(|V| + |E| log |V|) ignoring parsing.
855    ///
856    /// With the [attribute handler](crate::attributes::enable) on, the names
857    /// are stored in the `name` vertex attribute and the weights in the
858    /// `weight` edge attribute, as requested by `options`; otherwise they are
859    /// discarded ([module docs](self#attributes)).
860    ///
861    /// Binds [`igraph_read_graph_ncol`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_ncol).
862    ///
863    /// # Errors
864    /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
865    /// for syntax errors.
866    pub fn read_graph_ncol(
867        path: impl AsRef<Path>,
868        predefnames: &[&str],
869        options: &NcolLglOptions,
870    ) -> Result<Graph> {
871        Self::read_ncol_impl(Source::Path(path.as_ref()), predefnames, options)
872    }
873
874    /// Parses an NCOL graph from a string, see
875    /// [`read_graph_ncol`](Self::read_graph_ncol).
876    ///
877    /// # Examples
878    /// ```
879    /// use igraph::{foreign::NcolLglOptions, prelude::*};
880    /// let text = "alice bob 2.5\nbob carol\ncarol alice -1e2\n";
881    /// // Fix the vertex ids through the predefined names: carol=0, bob=1, alice=2.
882    /// let g = Graph::read_graph_ncol_from_str(text, &["carol", "bob", "alice"], &NcolLglOptions::default())
883    ///     .unwrap();
884    /// assert_eq!(g.edge_list(), vec![(1, 2), (0, 1), (0, 2)]);
885    /// ```
886    pub fn read_graph_ncol_from_str(
887        text: &str,
888        predefnames: &[&str],
889        options: &NcolLglOptions,
890    ) -> Result<Graph> {
891        Self::read_ncol_impl(Source::Bytes(text.as_bytes()), predefnames, options)
892    }
893
894    fn read_lgl_impl(source: Source<'_>, options: &NcolLglOptions) -> Result<Graph> {
895        read_with(source, |g, f| unsafe {
896            igraph_read_graph_lgl(
897                g,
898                f,
899                options.names,
900                options.weights.into(),
901                options.directed,
902            )
903        })
904    }
905
906    /// Reads an LGL file (Large Graph Layout format).
907    ///
908    /// The file is a sequence of blocks: a line `# name` introduces a vertex,
909    /// and each following line `other [weight]` adds an edge from it to
910    /// `other`, until the next `#` line. A `#` line with no following lines
911    /// defines an isolated vertex. Vertex ids are assigned in the order the
912    /// names first appear. Time complexity: O(|V| + |E| log |V|) ignoring
913    /// parsing.
914    ///
915    /// With the [attribute handler](crate::attributes::enable) on, the names
916    /// are stored in the `name` vertex attribute and the weights in the
917    /// `weight` edge attribute, as requested by `options`; otherwise they are
918    /// discarded ([module docs](self#attributes)).
919    ///
920    /// Binds [`igraph_read_graph_lgl`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_lgl).
921    ///
922    /// # Errors
923    /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
924    /// for syntax errors.
925    pub fn read_graph_lgl(path: impl AsRef<Path>, options: &NcolLglOptions) -> Result<Graph> {
926        Self::read_lgl_impl(Source::Path(path.as_ref()), options)
927    }
928
929    /// Parses an LGL graph from a string, see [`read_graph_lgl`](Self::read_graph_lgl).
930    ///
931    /// # Examples
932    /// ```
933    /// use igraph::{foreign::NcolLglOptions, prelude::*};
934    /// let text = "# a\nb\nc 2.0\n# b\nc\n# lonely\n";
935    /// let g = Graph::read_graph_lgl_from_str(text, &NcolLglOptions::default()).unwrap();
936    /// assert_eq!(g.vcount(), 4);
937    /// assert_eq!(g.edge_list(), vec![(0, 1), (0, 2), (1, 2)]);
938    /// ```
939    pub fn read_graph_lgl_from_str(text: &str, options: &NcolLglOptions) -> Result<Graph> {
940        Self::read_lgl_impl(Source::Bytes(text.as_bytes()), options)
941    }
942
943    /// Reads a Pajek `.net` file.
944    ///
945    /// Only a subset of the format is supported: `.paj` project files,
946    /// temporal networks, graphs mixing directed and undirected edges,
947    /// permutations/hierarchies/clusters/vectors and multi-relational networks
948    /// are not. `*Arcs` sections create a directed graph, `*Edges` an
949    /// undirected one; `*Arcslist`/`*Edgeslist` and matrix sections are
950    /// understood, as well as bipartite (two-mode) networks. Vertex ids in the
951    /// file are 1-based. Time complexity: O(|V| + |E|).
952    ///
953    /// With the [attribute handler](crate::attributes::enable) on, vertex
954    /// labels become the `name` vertex attribute, coordinates `x`, `y` (and
955    /// `z`), edge weights (including the entries of `*Matrix` sections) the
956    /// `weight` edge attribute, and the other Pajek parameters get
957    /// descriptive names (`ic`/`c` → `color`, `bc` → `framecolor`,
958    /// `x_fact` → `xfact`, `l` → `label`, `w` → `edgewidth`, ...; unknown ones
959    /// are kept as string attributes). A parameter given for some elements
960    /// only takes Pajek's default elsewhere (e.g. `color` is `LightOrange`
961    /// for vertices and `MidnightBlue` for edges; plain numbers such as the
962    /// coordinates and weights are NaN). Two-mode networks get the boolean
963    /// `type` vertex attribute (`false` for the first mode), as expected by
964    /// the [`bipartite`](crate::bipartite) functions. Without the handler all
965    /// of this is discarded ([module docs](self#attributes)).
966    ///
967    /// Binds [`igraph_read_graph_pajek`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_pajek).
968    ///
969    /// # Errors
970    /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
971    /// for syntax errors.
972    pub fn read_graph_pajek(path: impl AsRef<Path>) -> Result<Graph> {
973        read_with(Source::Path(path.as_ref()), |g, f| unsafe {
974            igraph_read_graph_pajek(g, f)
975        })
976    }
977
978    /// Parses a Pajek graph from a string, see [`read_graph_pajek`](Self::read_graph_pajek).
979    ///
980    /// # Examples
981    /// ```
982    /// use igraph::prelude::*;
983    /// let text = "*Vertices 4\n1 \"A\"\n2 \"B\"\n3 \"C\"\n4 \"D\"\n*Arcs\n1 2 0.5\n2 3\n3 1\n";
984    /// let g = Graph::read_graph_pajek_from_str(text).unwrap();
985    /// assert!(g.is_directed());
986    /// assert_eq!((g.vcount(), g.ecount()), (4, 3));
987    /// assert_eq!(g.edge(0).unwrap(), (0, 1));
988    /// ```
989    pub fn read_graph_pajek_from_str(text: &str) -> Result<Graph> {
990        read_with(Source::Bytes(text.as_bytes()), |g, f| unsafe {
991            igraph_read_graph_pajek(g, f)
992        })
993    }
994
995    /// Reads a GraphML file.
996    ///
997    /// Only basic GraphML is supported: no nested graphs, no hyperedges.
998    /// Directedness comes from the `edgedefault` attribute of the `graph`
999    /// element. If the file contains several graphs, `index` selects which
1000    /// one to load (0 for the first); note that igraph 1.0.0 and 1.0.1 fail
1001    /// with "Graph index was too large" ([`ErrorKind::InvalidValue`]) for any
1002    /// index but 0, even when the document does contain more graphs.
1003    /// Vertices get ids in order of appearance, edges keep the document
1004    /// order.
1005    ///
1006    /// With the [attribute handler](crate::attributes::enable) on, every
1007    /// `<key>` becomes a graph, vertex or edge attribute named after its
1008    /// `attr.name` (or its `id` if `attr.name` is missing): `boolean` keys
1009    /// give boolean attributes, `int`/`long`/`float`/`double` numeric ones,
1010    /// `string` string ones (UTF-8). Missing values take the key's
1011    /// `<default>`, or NaN/`""`/`false`. The GraphML `id`s of the nodes are
1012    /// kept in the string `id` vertex attribute (unless a vertex key already
1013    /// defines an attribute named `id`, in which case igraph only warns).
1014    /// When the edges have ids, igraph 1.0.0 and 1.0.1 also create a string
1015    /// `id` *edge* attribute (unless an edge key defines one) but, because of
1016    /// a bug in `src/io/graphml.c`, fill it with the *node* ids, in order,
1017    /// instead of the edge ids (padded with `""` when there are more edges
1018    /// than nodes): don't rely on it. Without the handler all of this is discarded
1019    /// ([module docs](self#attributes)).
1020    ///
1021    /// Binds [`igraph_read_graph_graphml`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_graphml).
1022    ///
1023    /// # Errors
1024    /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
1025    /// for malformed files, [`ErrorKind::InvalidValue`] if `index` is too
1026    /// large, [`ErrorKind::Unimplemented`] if the igraph library was compiled
1027    /// without GraphML (libxml2) support.
1028    pub fn read_graph_graphml(path: impl AsRef<Path>, index: usize) -> Result<Graph> {
1029        let index = to_int(index, "graph index")?;
1030        read_with(Source::Path(path.as_ref()), |g, f| unsafe {
1031            igraph_read_graph_graphml(g, f, index)
1032        })
1033    }
1034
1035    /// Parses a GraphML document from a string, see
1036    /// [`read_graph_graphml`](Self::read_graph_graphml).
1037    ///
1038    /// # Examples
1039    /// ```
1040    /// use igraph::{attributes, prelude::*};
1041    /// attributes::enable().unwrap();
1042    /// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1043    /// <graphml xmlns="http://graphml.graphdrawing.org/xmlns">
1044    ///   <key id="d0" for="node" attr.name="age" attr.type="int"><default>20</default></key>
1045    ///   <key id="d1" for="edge" attr.name="since" attr.type="double"/>
1046    ///   <graph edgedefault="directed">
1047    ///     <node id="ann"><data key="d0">30</data></node>
1048    ///     <node id="bob"/>
1049    ///     <edge source="bob" target="ann"><data key="d1">2019</data></edge>
1050    ///   </graph>
1051    /// </graphml>"#;
1052    /// let g = Graph::read_graph_graphml_from_str(xml, 0).unwrap();
1053    /// assert!(g.is_directed());
1054    /// assert_eq!(g.edge_list(), vec![(1, 0)]);
1055    /// assert_eq!(g.vertex_attr_str_values("id", ..).unwrap(), ["ann", "bob"]);
1056    /// assert_eq!(g.vertex_attr_numeric_values("age", ..).unwrap(), [30.0, 20.0]); // bob: default
1057    /// assert_eq!(g.edge_attr_numeric("since", 0).unwrap(), 2019.0);
1058    /// ```
1059    pub fn read_graph_graphml_from_str(text: &str, index: usize) -> Result<Graph> {
1060        let index = to_int(index, "graph index")?;
1061        read_with(Source::Bytes(text.as_bytes()), |g, f| unsafe {
1062            igraph_read_graph_graphml(g, f, index)
1063        })
1064    }
1065
1066    fn read_dimacs_impl(source: Source<'_>, directed: bool) -> Result<DimacsFlow> {
1067        let mut problem = StrVector::new();
1068        let mut labels = VectorInt::new();
1069        let mut capacity = Vector::new();
1070        let (mut s, mut t) = (-1, -1);
1071        let graph = read_with(source, |g, f| unsafe {
1072            igraph_read_graph_dimacs_flow(
1073                g,
1074                f,
1075                &mut problem,
1076                &mut labels,
1077                &mut s,
1078                &mut t,
1079                &mut capacity,
1080                directed,
1081            )
1082        })?;
1083        let problem_name = problem.get(0).unwrap_or_default().to_owned();
1084        let problem = if problem_name == "edge" {
1085            DimacsProblem::Edge {
1086                labels: labels.into(),
1087            }
1088        } else {
1089            // igraph reports a missing `n` line as id -2 (0 - 1 - 1) and
1090            // doesn't check the ids it read against the vertex count.
1091            let n = graph.vcount() as VertexId;
1092            let check = |id: igraph_int_t, what: &str| -> Result<Option<VertexId>> {
1093                match id {
1094                    -2 => Ok(None),
1095                    id if (0..n).contains(&id) => Ok(Some(id)),
1096                    id => Err(Error::new(
1097                        ErrorKind::Parse,
1098                        format!(
1099                            "DIMACS {what} vertex {} out of range (the graph has {n} vertices)",
1100                            id + 1
1101                        ),
1102                    )),
1103                }
1104            };
1105            DimacsProblem::Max {
1106                source: check(s, "source")?,
1107                target: check(t, "target")?,
1108                capacity: capacity.into(),
1109            }
1110        };
1111        Ok(DimacsFlow {
1112            graph,
1113            problem_name,
1114            problem,
1115        })
1116    }
1117
1118    /// Reads a DIMACS network flow file.
1119    ///
1120    /// DIMACS is a line oriented format; the first character of each line
1121    /// gives its type: `c` comment, `p` the problem line (`p max|edge
1122    /// <vertices> <edges>`, before any node or arc line), `n` node lines and
1123    /// `a` arc lines (`a from to capacity`, max-flow problems) or `e` edge
1124    /// lines (`e from to`, edge problems). In max-flow problems exactly two
1125    /// node lines `n <id> s|t` mark the source and the target. In edge
1126    /// problems the labels are the 1-based vertex indices: `n <id> <label>`
1127    /// lines are meant to change them, but igraph 1.0.0 and 1.0.1 reject them
1128    /// with a parse error. Vertex ids in the file start from 1, the returned ids
1129    /// from 0. A max-flow file without the source (or target) `n` line is
1130    /// accepted, with `None` in [`DimacsProblem::Max`]. Time complexity:
1131    /// O(|V| + |E| + c), c being the file size. The capacities are returned
1132    /// in [`DimacsProblem::Max`], never as attributes.
1133    ///
1134    /// See [`Graph::maxflow`] and [`Graph::st_mincut`] to solve the problem.
1135    ///
1136    /// Binds [`igraph_read_graph_dimacs_flow`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_dimacs_flow).
1137    ///
1138    /// # Errors
1139    /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
1140    /// or [`ErrorKind::InvalidValue`] for malformed files, including a source
1141    /// or target `n` line naming a vertex that doesn't exist.
1142    pub fn read_graph_dimacs_flow(path: impl AsRef<Path>, directed: bool) -> Result<DimacsFlow> {
1143        Self::read_dimacs_impl(Source::Path(path.as_ref()), directed)
1144    }
1145
1146    /// Parses a DIMACS flow problem from a string, see
1147    /// [`read_graph_dimacs_flow`](Self::read_graph_dimacs_flow).
1148    ///
1149    /// # Examples
1150    /// ```
1151    /// use igraph::{foreign::DimacsProblem, prelude::*};
1152    /// let text = "c a tiny network\np max 3 2\nn 1 s\nn 3 t\na 1 2 4\na 2 3 7\n";
1153    /// let flow = Graph::read_graph_dimacs_flow_from_str(text, true).unwrap();
1154    /// assert_eq!(flow.problem_name, "max");
1155    /// assert_eq!(flow.graph.edge_list(), vec![(0, 1), (1, 2)]);
1156    /// let DimacsProblem::Max { source: Some(s), target: Some(t), capacity } = flow.problem else {
1157    ///     panic!("a max-flow problem with both terminals was expected");
1158    /// };
1159    /// assert_eq!((s, t, capacity.as_slice()), (0, 2, &[4.0, 7.0][..]));
1160    /// // Solve it: the bottleneck is the first arc.
1161    /// assert_eq!(flow.graph.maxflow_value(s, t, Some(&capacity)).unwrap(), 4.0);
1162    /// ```
1163    pub fn read_graph_dimacs_flow_from_str(text: &str, directed: bool) -> Result<DimacsFlow> {
1164        Self::read_dimacs_impl(Source::Bytes(text.as_bytes()), directed)
1165    }
1166
1167    /// Reads a graph in the binary format of the ARG graph database (used
1168    /// to benchmark isomorphism algorithms).
1169    ///
1170    /// The file is a sequence of 16-bit little-endian words: the number of
1171    /// vertices, then for each vertex the number of its out-edges followed by
1172    /// their (0-based) targets. Only unlabelled graphs are supported. Time
1173    /// complexity: O(|V| + |E|).
1174    ///
1175    /// The database is a benchmark for isomorphism algorithms: see
1176    /// [`Graph::isomorphic`] and the rest of the
1177    /// [`isomorphism`](crate::isomorphism) module.
1178    ///
1179    /// Binds [`igraph_read_graph_graphdb`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_graphdb).
1180    ///
1181    /// # Errors
1182    /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
1183    /// for truncated files or trailing bytes.
1184    pub fn read_graph_graphdb(path: impl AsRef<Path>, directed: bool) -> Result<Graph> {
1185        read_with(Source::Path(path.as_ref()), |g, f| unsafe {
1186            igraph_read_graph_graphdb(g, f, directed)
1187        })
1188    }
1189
1190    /// Parses a graph in the binary graph database format from bytes, see
1191    /// [`read_graph_graphdb`](Self::read_graph_graphdb).
1192    ///
1193    /// # Examples
1194    /// ```
1195    /// use igraph::prelude::*;
1196    /// // 4 vertices; 0 -> [2]; 1 -> [0]; 2 -> []; 3 -> [0, 2] (igraph's unit test file).
1197    /// let words: [u16; 9] = [4, 1, 2, 1, 0, 0, 2, 0, 2];
1198    /// let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
1199    /// let g = Graph::read_graph_graphdb_from_bytes(&bytes, true).unwrap();
1200    /// assert_eq!(g.edge_list(), vec![(0, 2), (1, 0), (3, 0), (3, 2)]);
1201    /// ```
1202    pub fn read_graph_graphdb_from_bytes(data: &[u8], directed: bool) -> Result<Graph> {
1203        read_with(Source::Bytes(data), |g, f| unsafe {
1204            igraph_read_graph_graphdb(g, f, directed)
1205        })
1206    }
1207
1208    /// Reads a GML file.
1209    ///
1210    /// Any syntactically correct GML is parsed, but only a subset is used:
1211    /// the first `graph` record, its `directed` flag, `node` records (with
1212    /// their `id`) and `edge` records (with `source` and `target`); other top
1213    /// level records are ignored. `inf`, `-inf` and `nan` are accepted as
1214    /// reals (case insensitively). Time complexity: proportional to the file
1215    /// length.
1216    ///
1217    /// With the [attribute handler](crate::attributes::enable) on, every
1218    /// field of simple type (integer, real, string) of the graph, node and
1219    /// edge records becomes a numeric or string attribute (including the
1220    /// numeric `id` of the nodes, and `comment` fields); composite fields
1221    /// (records) are ignored with a warning, and missing values are NaN or
1222    /// `""`. Only the `quot`, `amp`, `apos`, `lt` and `gt` entities are
1223    /// decoded. Without the handler all of this is discarded
1224    /// ([module docs](self#attributes)).
1225    ///
1226    /// The C parser of igraph 1.0.0 and 1.0.1 is not reentrant (it uses
1227    /// static buffers), so this wrapper serializes GML reads across threads
1228    /// with a process-wide lock; all the other readers run fully in parallel.
1229    ///
1230    /// Binds [`igraph_read_graph_gml`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_gml).
1231    ///
1232    /// # Errors
1233    /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
1234    /// for syntax errors and for structural problems: no `graph` record,
1235    /// duplicate or non-integer node ids, edges without `source`/`target` or
1236    /// referring to unknown node ids.
1237    pub fn read_graph_gml(path: impl AsRef<Path>) -> Result<Graph> {
1238        read_gml(Source::Path(path.as_ref()))
1239    }
1240
1241    /// Parses a GML graph from a string, see [`read_graph_gml`](Self::read_graph_gml).
1242    ///
1243    /// # Examples
1244    /// ```
1245    /// use igraph::prelude::*;
1246    /// let text = r#"graph [
1247    ///   directed 0
1248    ///   node [ id 10 label "x" ]
1249    ///   node [ id 20 ]
1250    ///   node [ id 30 ]
1251    ///   edge [ source 10 target 20 ]
1252    ///   edge [ source 30 target 10 weight 2.5 ]
1253    /// ]"#;
1254    /// let g = Graph::read_graph_gml_from_str(text).unwrap();
1255    /// assert!(!g.is_directed());
1256    /// // GML ids are mapped to consecutive vertex ids in order of appearance.
1257    /// assert_eq!(g.edge_list(), vec![(0, 1), (0, 2)]);
1258    /// ```
1259    pub fn read_graph_gml_from_str(text: &str) -> Result<Graph> {
1260        read_gml(Source::Bytes(text.as_bytes()))
1261    }
1262
1263    /// Reads a file in the DL format of UCINET.
1264    ///
1265    /// All the forms of the format are supported: full matrix, edge list
1266    /// (`format = edgelist1`) and node list (`format = nodelist1`), with or
1267    /// without labels. Labels are case sensitive. With the
1268    /// [attribute handler](crate::attributes::enable) on, labels are stored
1269    /// in the `name` vertex attribute and edge values in the `weight` edge
1270    /// attribute; otherwise they are discarded
1271    /// ([module docs](self#attributes)). Time complexity: linear in the
1272    /// number of vertices and edges, quadratic in the number of vertices for
1273    /// the full matrix form.
1274    ///
1275    /// Binds [`igraph_read_graph_dl`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_dl).
1276    ///
1277    /// # Errors
1278    /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
1279    /// for syntax errors.
1280    pub fn read_graph_dl(path: impl AsRef<Path>, directed: bool) -> Result<Graph> {
1281        read_with(Source::Path(path.as_ref()), |g, f| unsafe {
1282            igraph_read_graph_dl(g, f, directed)
1283        })
1284    }
1285
1286    /// Parses a UCINET DL graph from a string, see [`read_graph_dl`](Self::read_graph_dl).
1287    ///
1288    /// # Examples
1289    /// ```
1290    /// use igraph::prelude::*;
1291    /// // `fullmatrix1.dl` from igraph's examples.
1292    /// let text = "DL N = 5\nData:\n0 1 1 1 1\n1 0 1 0 0\n1 1 0 0 1\n1 0 0 0 0\n1 0 1 0 0\n";
1293    /// let g = Graph::read_graph_dl_from_str(text, true).unwrap();
1294    /// assert_eq!((g.vcount(), g.ecount()), (5, 12));
1295    /// assert_eq!(g.edge(0).unwrap(), (0, 1));
1296    /// ```
1297    pub fn read_graph_dl_from_str(text: &str, directed: bool) -> Result<Graph> {
1298        read_with(Source::Bytes(text.as_bytes()), |g, f| unsafe {
1299            igraph_read_graph_dl(g, f, directed)
1300        })
1301    }
1302}
1303
1304// ---------------------------------------------------------------------------
1305// Writers
1306// ---------------------------------------------------------------------------
1307
1308impl igraph_t {
1309    /// Writes the graph to a file in the given `format`, with default options
1310    /// (NCOL/LGL/LEDA without the optional named attributes, GraphML without
1311    /// prefixes, GML with the default [`GmlWriteOptions`], LGL without
1312    /// isolated vertices). GraphML, GML and DOT export all the attributes, and
1313    /// Pajek those naming Pajek parameters, when the
1314    /// [attribute handler](crate::attributes::enable) is on.
1315    ///
1316    /// # Errors
1317    /// [`ErrorKind::Unimplemented`] if the format can't be written generically
1318    /// (graph database, DL, DIMACS), plus the errors of the specific writer.
1319    ///
1320    /// # Examples
1321    /// ```
1322    /// use igraph::{foreign::GraphFormat, prelude::*};
1323    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, false).unwrap();
1324    /// let path = std::env::temp_dir().join(format!("igraph-doc-write-{}.dot", std::process::id()));
1325    /// g.write_graph(&path, GraphFormat::from_path(&path).unwrap()).unwrap();
1326    /// let dot = std::fs::read_to_string(&path).unwrap();
1327    /// std::fs::remove_file(&path).unwrap();
1328    /// assert!(dot.contains("graph {") && dot.contains("1 -- 0;") && dot.contains("2 -- 1;"));
1329    /// ```
1330    pub fn write_graph(&self, path: impl AsRef<Path>, format: GraphFormat) -> Result<()> {
1331        let path = path.as_ref();
1332        match format {
1333            GraphFormat::Edgelist => self.write_graph_edgelist(path),
1334            GraphFormat::Ncol => self.write_graph_ncol(path, None, None),
1335            GraphFormat::Lgl => self.write_graph_lgl(path, None, None, false),
1336            GraphFormat::Pajek => self.write_graph_pajek(path),
1337            GraphFormat::GraphMl => self.write_graph_graphml(path),
1338            GraphFormat::Gml => self.write_graph_gml(path, &GmlWriteOptions::default()),
1339            GraphFormat::Dot => self.write_graph_dot(path),
1340            GraphFormat::Leda => self.write_graph_leda(path, None, None),
1341            GraphFormat::DimacsFlow | GraphFormat::GraphDb | GraphFormat::Dl => Err(Error::new(
1342                ErrorKind::Unimplemented,
1343                format!("the {format:?} format can't be written generically"),
1344            )),
1345        }
1346    }
1347
1348    /// Writes the edge list of the graph to a file: one `from to` line per
1349    /// edge (0-based ids, a single space as separator). Lines are sorted by
1350    /// the first endpoint, so edge ids are preserved by a round trip only if
1351    /// the edges were already sorted that way; isolated vertices (with
1352    /// ids above the largest endpoint) are lost unless the vertex count is
1353    /// passed back to [`read_graph_edgelist`](Self::read_graph_edgelist).
1354    /// Time complexity: O(|E|).
1355    ///
1356    /// Binds [`igraph_write_graph_edgelist`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_edgelist).
1357    ///
1358    /// # Errors
1359    /// [`ErrorKind::File`] if the file can't be created or written.
1360    pub fn write_graph_edgelist(&self, path: impl AsRef<Path>) -> Result<()> {
1361        write_to_path(path.as_ref(), |f| unsafe {
1362            igraph_write_graph_edgelist(self, f)
1363        })
1364    }
1365
1366    /// The edge list serialization as a string, see
1367    /// [`write_graph_edgelist`](Self::write_graph_edgelist).
1368    ///
1369    /// # Examples
1370    /// ```
1371    /// use igraph::prelude::*;
1372    /// let g = Graph::from_edges(&[(2, 1), (0, 1)], 3, true).unwrap();
1373    /// // Sorted by the source vertex, not by edge id.
1374    /// assert_eq!(g.write_graph_edgelist_to_string().unwrap(), "0 1\n2 1\n");
1375    /// ```
1376    pub fn write_graph_edgelist_to_string(&self) -> Result<String> {
1377        write_to_string(|f| unsafe { igraph_write_graph_edgelist(self, f) })
1378    }
1379
1380    /// Writes the graph to an NCOL file (see
1381    /// [`read_graph_ncol`](Self::read_graph_ncol)): one `from to [weight]`
1382    /// line per edge.
1383    ///
1384    /// `names` is the name of a string vertex attribute to write instead of
1385    /// the vertex ids, `weights` the name of a numeric edge attribute to
1386    /// write as weights; `None` skips them. A requested attribute that does
1387    /// not exist (always the case without the
1388    /// [attribute handler](crate::attributes::enable)) is skipped with an
1389    /// igraph warning ([module docs](self#attributes)). NaN and infinite
1390    /// weights are written as `NaN`, `Inf` and `-Inf`, and read back as such.
1391    /// Names must be non-empty and contain no spaces or non-printable
1392    /// characters. The format can't represent
1393    /// isolated vertices; multi-edges and loops are written (though they
1394    /// break the LGL software). Time complexity: O(|E|).
1395    ///
1396    /// Binds [`igraph_write_graph_ncol`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_ncol).
1397    ///
1398    /// # Errors
1399    /// [`ErrorKind::File`] on I/O errors, [`ErrorKind::InvalidValue`] for
1400    /// attribute names with NUL bytes or invalid vertex names.
1401    pub fn write_graph_ncol(
1402        &self,
1403        path: impl AsRef<Path>,
1404        names: Option<&str>,
1405        weights: Option<&str>,
1406    ) -> Result<()> {
1407        let (n, w) = (
1408            opt_cstring(names, "names")?,
1409            opt_cstring(weights, "weights")?,
1410        );
1411        write_to_path(path.as_ref(), |f| unsafe {
1412            igraph_write_graph_ncol(self, f, opt_ptr(&n), opt_ptr(&w))
1413        })
1414    }
1415
1416    /// The NCOL serialization as a string, see
1417    /// [`write_graph_ncol`](Self::write_graph_ncol).
1418    ///
1419    /// # Examples
1420    /// ```
1421    /// use igraph::prelude::*;
1422    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 4, false).unwrap();
1423    /// assert_eq!(g.write_graph_ncol_to_string(None, None).unwrap(), "0 1\n1 2\n");
1424    /// ```
1425    pub fn write_graph_ncol_to_string(
1426        &self,
1427        names: Option<&str>,
1428        weights: Option<&str>,
1429    ) -> Result<String> {
1430        let (n, w) = (
1431            opt_cstring(names, "names")?,
1432            opt_cstring(weights, "weights")?,
1433        );
1434        write_to_string(|f| unsafe { igraph_write_graph_ncol(self, f, opt_ptr(&n), opt_ptr(&w)) })
1435    }
1436
1437    /// Writes the graph to an LGL file (see [`read_graph_lgl`](Self::read_graph_lgl)).
1438    ///
1439    /// Edges are grouped by their first endpoint: `# from` followed by one
1440    /// `to [weight]` line per edge. `names`/`weights` name a string vertex
1441    /// attribute and a numeric edge attribute to write (skipped with a
1442    /// warning when missing, as always without the
1443    /// [attribute handler](crate::attributes::enable), see the
1444    /// [module docs](self#attributes)); names must be non-empty and contain
1445    /// no spaces, `#` or non-printable characters. With `isolates = true` isolated
1446    /// vertices are written as lone `# v` lines. Time complexity: O(|E|), or
1447    /// O(|V| + |E|) with isolates.
1448    ///
1449    /// Binds [`igraph_write_graph_lgl`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_lgl).
1450    ///
1451    /// # Errors
1452    /// [`ErrorKind::File`] on I/O errors, [`ErrorKind::InvalidValue`] for
1453    /// attribute names with NUL bytes or invalid vertex names.
1454    pub fn write_graph_lgl(
1455        &self,
1456        path: impl AsRef<Path>,
1457        names: Option<&str>,
1458        weights: Option<&str>,
1459        isolates: bool,
1460    ) -> Result<()> {
1461        let (n, w) = (
1462            opt_cstring(names, "names")?,
1463            opt_cstring(weights, "weights")?,
1464        );
1465        write_to_path(path.as_ref(), |f| unsafe {
1466            igraph_write_graph_lgl(self, f, opt_ptr(&n), opt_ptr(&w), isolates)
1467        })
1468    }
1469
1470    /// The LGL serialization as a string, see [`write_graph_lgl`](Self::write_graph_lgl).
1471    ///
1472    /// # Examples
1473    /// ```
1474    /// use igraph::prelude::*;
1475    /// // The graph of igraph's `igraph_write_graph_lgl.c` example.
1476    /// let g = Graph::from_edges(&[(0, 1), (1, 3), (1, 2), (2, 0), (4, 2), (3, 4)], 7, false).unwrap();
1477    /// let lgl = g.write_graph_lgl_to_string(None, None, true).unwrap();
1478    /// assert_eq!(lgl, "# 0\n1\n2\n# 1\n2\n3\n# 2\n4\n# 3\n4\n# 5\n# 6\n");
1479    /// ```
1480    pub fn write_graph_lgl_to_string(
1481        &self,
1482        names: Option<&str>,
1483        weights: Option<&str>,
1484        isolates: bool,
1485    ) -> Result<String> {
1486        let (n, w) = (
1487            opt_cstring(names, "names")?,
1488            opt_cstring(weights, "weights")?,
1489        );
1490        write_to_string(|f| unsafe {
1491            igraph_write_graph_lgl(self, f, opt_ptr(&n), opt_ptr(&w), isolates)
1492        })
1493    }
1494
1495    /// Writes the graph to a GraphML file (without attribute prefixes).
1496    ///
1497    /// GraphML is an XML format, see the
1498    /// [GraphML primer](http://graphml.graphdrawing.org/primer/graphml-primer.html).
1499    /// Vertices are written as `n0, n1, ...`, edges in id order, and the
1500    /// `edgedefault` reflects the directedness. With the
1501    /// [attribute handler](crate::attributes::enable) on, all graph, vertex
1502    /// and edge attributes are written as typed `<key>`s (`boolean`,
1503    /// `double` or `string`; NaN values are omitted), so a GraphML round trip
1504    /// preserves them ([module docs](self#attributes)). Attribute names and
1505    /// string values should be UTF-8 (igraph copies the bytes as they are)
1506    /// and must not contain control characters other than tab, CR and LF. Use
1507    /// [`write_graph_graphml_with`](Self::write_graph_graphml_with) to
1508    /// control the attribute name prefixes. Time complexity: O(|V| + |E|).
1509    ///
1510    /// This replaces the former, infallible `write_graph_graphml(&self, &str)`
1511    /// that leaked its file handle: the file is now always closed and errors
1512    /// are reported.
1513    ///
1514    /// Binds [`igraph_write_graph_graphml`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_graphml).
1515    ///
1516    /// # Errors
1517    /// [`ErrorKind::File`] if the file can't be created or written,
1518    /// [`ErrorKind::InvalidValue`] if an attribute name or string value
1519    /// contains a control character other than tab, CR and LF.
1520    ///
1521    /// # Examples
1522    /// ```
1523    /// use igraph::prelude::*;
1524    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], 3, true).unwrap();
1525    /// let path = std::env::temp_dir().join(format!("igraph-doc-{}.graphml", std::process::id()));
1526    /// g.write_graph_graphml(&path).unwrap();
1527    /// let xml = std::fs::read_to_string(&path).unwrap();
1528    /// std::fs::remove_file(&path).unwrap();
1529    /// assert!(xml.contains(r#"edgedefault="directed""#));
1530    /// assert!(xml.contains(r#"<edge source="n2" target="n0">"#));
1531    /// ```
1532    pub fn write_graph_graphml(&self, path: impl AsRef<Path>) -> Result<()> {
1533        self.write_graph_graphml_with(path, false)
1534    }
1535
1536    /// Writes the graph to a GraphML file; with `prefixattr = true`
1537    /// attribute names get a `g_`, `v_` or `e_` prefix to keep graph, vertex
1538    /// and edge attributes with the same name distinct. See
1539    /// [`write_graph_graphml`](Self::write_graph_graphml).
1540    ///
1541    /// Binds [`igraph_write_graph_graphml`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_graphml).
1542    ///
1543    /// # Errors
1544    /// As [`write_graph_graphml`](Self::write_graph_graphml).
1545    pub fn write_graph_graphml_with(&self, path: impl AsRef<Path>, prefixattr: bool) -> Result<()> {
1546        write_to_path(path.as_ref(), |f| unsafe {
1547            igraph_write_graph_graphml(self, f, prefixattr)
1548        })
1549    }
1550
1551    /// The GraphML serialization as a string, see
1552    /// [`write_graph_graphml_with`](Self::write_graph_graphml_with).
1553    ///
1554    /// # Examples
1555    /// ```
1556    /// use igraph::prelude::*;
1557    /// let mut g = Graph::from_edges(&[(0, 1)], 2, false).unwrap();
1558    /// // Setting an attribute turns on the attribute handler.
1559    /// g.set_vertex_attr_str_values("name", &["ann", "bob"]).unwrap();
1560    /// g.set_edge_attr_numeric("weight", 0, 2.5).unwrap();
1561    /// let xml = g.write_graph_graphml_to_string(true).unwrap();
1562    /// assert!(xml.contains(r#"<key id="v_name" for="node" attr.name="name" attr.type="string"/>"#));
1563    /// assert!(xml.contains(r#"<data key="v_name">bob</data>"#));
1564    /// assert!(xml.contains(r#"<data key="e_weight">2.5</data>"#));
1565    /// // Reading it back restores the attributes (under their original names).
1566    /// let h = Graph::read_graph_graphml_from_str(&xml, 0).unwrap();
1567    /// assert_eq!(h.vertex_attr_str_values("name", ..).unwrap(), ["ann", "bob"]);
1568    /// assert_eq!(h.edge_attr_numeric("weight", 0).unwrap(), 2.5);
1569    /// ```
1570    pub fn write_graph_graphml_to_string(&self, prefixattr: bool) -> Result<String> {
1571        write_to_string(|f| unsafe { igraph_write_graph_graphml(self, f, prefixattr) })
1572    }
1573
1574    /// Writes the graph to a Pajek `.net` file.
1575    ///
1576    /// The format is meant for interoperability with the Pajek software, not
1577    /// for data exchange. Vertex ids are written 1-based; directed graphs use
1578    /// an `*Arcs` section, undirected ones `*Edges`. Vertex and edge
1579    /// parameters come from the attributes named as in
1580    /// [`read_graph_pajek`](Self::read_graph_pajek) (`name` as the label,
1581    /// `x`/`y`/`z`, `color`, edge `weight`, ...; other attributes are
1582    /// discarded), hence are only written with the
1583    /// [attribute handler](crate::attributes::enable) on
1584    /// ([module docs](self#attributes)). A boolean `type` vertex attribute
1585    /// makes a two-mode (bipartite) file: since Pajek needs the vertices of
1586    /// the first mode first, the vertices are then reordered, and their ids
1587    /// change on a round trip (without a `name` attribute the labels are then
1588    /// the original 1-based ids). igraph never writes a UTF-8 byte-order mark.
1589    /// Time complexity: O(|V| + |E|).
1590    ///
1591    /// Binds [`igraph_write_graph_pajek`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_pajek).
1592    ///
1593    /// # Errors
1594    /// [`ErrorKind::File`] if the file can't be created or written.
1595    pub fn write_graph_pajek(&self, path: impl AsRef<Path>) -> Result<()> {
1596        write_to_path(path.as_ref(), |f| unsafe {
1597            igraph_write_graph_pajek(self, f)
1598        })
1599    }
1600
1601    /// The Pajek serialization as a string, see
1602    /// [`write_graph_pajek`](Self::write_graph_pajek).
1603    ///
1604    /// # Examples
1605    /// ```
1606    /// use igraph::prelude::*;
1607    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], 3, true).unwrap();
1608    /// assert_eq!(g.write_graph_pajek_to_string().unwrap(), "*Vertices 3\n*Arcs\n1 2\n2 3\n3 1\n");
1609    /// ```
1610    pub fn write_graph_pajek_to_string(&self) -> Result<String> {
1611        write_to_string(|f| unsafe { igraph_write_graph_pajek(self, f) })
1612    }
1613
1614    /// Writes a maximum flow problem in DIMACS format (see
1615    /// [`read_graph_dimacs_flow`](Self::read_graph_dimacs_flow)): a comment,
1616    /// the `p max` line, the source and target `n` lines and one
1617    /// `a from to capacity` line per edge (1-based ids). `capacity` must have
1618    /// one entry per edge. Time complexity: O(|E|).
1619    ///
1620    /// See [`Graph::maxflow`] to solve the instance directly.
1621    ///
1622    /// Binds [`igraph_write_graph_dimacs_flow`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_dimacs_flow).
1623    ///
1624    /// # Errors
1625    /// [`ErrorKind::InvalidVertexId`] if `source` or `target` is not a vertex
1626    /// of the graph (igraph itself would write them unchecked),
1627    /// [`ErrorKind::InvalidValue`] if `capacity.len() != ecount`,
1628    /// [`ErrorKind::File`] on I/O errors.
1629    pub fn write_graph_dimacs_flow(
1630        &self,
1631        path: impl AsRef<Path>,
1632        source: VertexId,
1633        target: VertexId,
1634        capacity: &[f64],
1635    ) -> Result<()> {
1636        self.check_dimacs_args(source, target, capacity)?;
1637        let cap = Vector::view(capacity);
1638        write_to_path(path.as_ref(), |f| unsafe {
1639            igraph_write_graph_dimacs_flow(self, f, source, target, cap.as_ptr())
1640        })
1641    }
1642
1643    /// The DIMACS serialization as a string, see
1644    /// [`write_graph_dimacs_flow`](Self::write_graph_dimacs_flow).
1645    ///
1646    /// # Examples
1647    /// ```
1648    /// use igraph::prelude::*;
1649    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, true).unwrap();
1650    /// let text = g.write_graph_dimacs_flow_to_string(0, 2, &[4.0, 7.5]).unwrap();
1651    /// assert_eq!(text, "c created by igraph\np max 3 2\nn 1 s\nn 3 t\na 1 2 4\na 2 3 7.5\n");
1652    /// ```
1653    pub fn write_graph_dimacs_flow_to_string(
1654        &self,
1655        source: VertexId,
1656        target: VertexId,
1657        capacity: &[f64],
1658    ) -> Result<String> {
1659        self.check_dimacs_args(source, target, capacity)?;
1660        let cap = Vector::view(capacity);
1661        write_to_string(|f| unsafe {
1662            igraph_write_graph_dimacs_flow(self, f, source, target, cap.as_ptr())
1663        })
1664    }
1665
1666    fn check_dimacs_args(
1667        &self,
1668        source: VertexId,
1669        target: VertexId,
1670        capacity: &[f64],
1671    ) -> Result<()> {
1672        // igraph writes any id verbatim, producing files that can't be read back.
1673        for (what, v) in [("source", source), ("target", target)] {
1674            if !(0..self.vcount() as VertexId).contains(&v) {
1675                return Err(Error::new(
1676                    ErrorKind::InvalidVertexId,
1677                    format!(
1678                        "invalid {what} vertex {v} for a graph with {} vertices",
1679                        self.vcount()
1680                    ),
1681                ));
1682            }
1683        }
1684        if capacity.len() != self.ecount() {
1685            return Err(Error::invalid(format!(
1686                "the capacity vector has length {}, but the graph has {} edges",
1687                capacity.len(),
1688                self.ecount()
1689            )));
1690        }
1691        Ok(())
1692    }
1693
1694    /// Writes the graph to a GML file.
1695    ///
1696    /// The output lists the `directed` flag, one `node [ id ... ]` record per
1697    /// vertex and one `edge [ source ... target ... ]` record per edge. See
1698    /// [`GmlWriteOptions`] for the ids, the creator line and the entity
1699    /// encoding. Time complexity: proportional to the output size.
1700    ///
1701    /// With the [attribute handler](crate::attributes::enable) on, numeric
1702    /// and string attributes are written too (booleans as 0/1, NaN values
1703    /// skipped, infinite values kept with a warning since they are not
1704    /// standard GML). Attribute names are reduced to their alphanumeric
1705    /// characters (prefixed with `igraph` if they don't start with a
1706    /// letter); attributes whose name would clash with the GML structure
1707    /// (`source`/`target` on edges, `directed`, `node` and `edge` on the
1708    /// graph, `id` on vertices) are skipped with a warning. The `id` vertex
1709    /// attribute itself is never written as an attribute: a numeric one
1710    /// supplies the node ids (see [`GmlWriteOptions::ids`]), any other is
1711    /// dropped ([module docs](self#attributes)).
1712    ///
1713    /// Binds [`igraph_write_graph_gml`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_gml).
1714    ///
1715    /// # Errors
1716    /// [`ErrorKind::InvalidValue`] if `options.ids` doesn't have one entry per
1717    /// vertex, or the creator contains a NUL byte; [`ErrorKind::File`] on I/O
1718    /// errors.
1719    pub fn write_graph_gml(
1720        &self,
1721        path: impl AsRef<Path>,
1722        options: &GmlWriteOptions<'_>,
1723    ) -> Result<()> {
1724        let (opts, ids, creator) = self.gml_args(options)?;
1725        let ids_ptr = ids.as_ref().map_or(std::ptr::null(), |v| v.as_ptr());
1726        // `ctime` (used for the default creator line) is not reentrant.
1727        let _guard = options.creator.is_none().then(gml_lock);
1728        write_to_path(path.as_ref(), |f| unsafe {
1729            igraph_write_graph_gml(self, f, opts, ids_ptr, opt_ptr(&creator))
1730        })
1731    }
1732
1733    /// The GML serialization as a string, see
1734    /// [`write_graph_gml`](Self::write_graph_gml).
1735    ///
1736    /// # Examples
1737    /// ```
1738    /// use igraph::{foreign::GmlWriteOptions, prelude::*};
1739    /// let g = Graph::from_edges(&[(0, 1)], 2, false).unwrap();
1740    /// let opts = GmlWriteOptions::default().with_creator("").with_ids(&[10.0, 20.0]);
1741    /// let gml = g.write_graph_gml_to_string(&opts).unwrap();
1742    /// // igraph stores undirected edges with the larger endpoint first.
1743    /// assert!(gml.contains("id 10") && gml.contains("source 20") && gml.contains("target 10"));
1744    /// assert!(!gml.contains("Creator"));
1745    /// ```
1746    pub fn write_graph_gml_to_string(&self, options: &GmlWriteOptions<'_>) -> Result<String> {
1747        let (opts, ids, creator) = self.gml_args(options)?;
1748        let ids_ptr = ids.as_ref().map_or(std::ptr::null(), |v| v.as_ptr());
1749        // `ctime` (used for the default creator line) is not reentrant.
1750        let _guard = options.creator.is_none().then(gml_lock);
1751        write_to_string(|f| unsafe {
1752            igraph_write_graph_gml(self, f, opts, ids_ptr, opt_ptr(&creator))
1753        })
1754    }
1755
1756    /// Validates and converts the GML writer options.
1757    #[allow(clippy::type_complexity)]
1758    fn gml_args<'a>(
1759        &self,
1760        options: &GmlWriteOptions<'a>,
1761    ) -> Result<(
1762        igraph_write_gml_sw_t,
1763        Option<crate::vector::View<'a, Vector>>,
1764        Option<CString>,
1765    )> {
1766        if let Some(ids) = options.ids
1767            && ids.len() != self.vcount()
1768        {
1769            return Err(Error::invalid(format!(
1770                "the id vector has length {}, but the graph has {} vertices",
1771                ids.len(),
1772                self.vcount()
1773            )));
1774        }
1775        let flags = if options.encode_only_quot {
1776            IGRAPH_WRITE_GML_ENCODE_ONLY_QUOT_SW
1777        } else {
1778            IGRAPH_WRITE_GML_DEFAULT_SW
1779        } as igraph_write_gml_sw_t;
1780        // igraph 1.0.0 and 1.0.1 compute the entity-encoded creator but then
1781        // print the raw string, so a `"` would end the GML string early:
1782        // encode it here.
1783        let creator = options
1784            .creator
1785            .map(|c| gml_entity_encode(c, options.encode_only_quot));
1786        let creator = opt_cstring(creator.as_deref(), "the GML creator")?;
1787        Ok((flags, options.ids.map(Vector::view), creator))
1788    }
1789
1790    /// Writes the graph to a Graphviz DOT file.
1791    ///
1792    /// The structure is written as `a -- b;` (`a -> b;` for directed
1793    /// graphs) lines after the list of vertices; igraph adds no layout or
1794    /// visualization information of its own. With the
1795    /// [attribute handler](crate::attributes::enable) on, the graph
1796    /// attributes are written in a `graph [ ... ]` block and every vertex and
1797    /// edge attribute in the `[ ... ]` block of its element (numbers as
1798    /// written by igraph, `NaN` included, booleans as 0/1 with a warning,
1799    /// strings and names quoted and escaped when needed). The format is meant
1800    /// for interoperability with Graphviz, not for data exchange. Time
1801    /// complexity: proportional to the output size.
1802    ///
1803    /// See [`Graph::layout_circle`] and the [`layout`](crate::layout) module
1804    /// to compute coordinates to add as attributes (e.g. `pos`).
1805    ///
1806    /// Binds [`igraph_write_graph_dot`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_dot).
1807    ///
1808    /// # Errors
1809    /// [`ErrorKind::File`] if the file can't be created or written.
1810    pub fn write_graph_dot(&self, path: impl AsRef<Path>) -> Result<()> {
1811        write_to_path(path.as_ref(), |f| unsafe {
1812            igraph_write_graph_dot(self, f)
1813        })
1814    }
1815
1816    /// The DOT serialization as a string, see [`write_graph_dot`](Self::write_graph_dot).
1817    ///
1818    /// # Examples
1819    /// ```
1820    /// use igraph::prelude::*;
1821    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, true).unwrap();
1822    /// let dot = g.write_graph_dot_to_string().unwrap();
1823    /// assert!(dot.starts_with("/* Created by igraph"));
1824    /// assert!(dot.contains("digraph {") && dot.contains("  0 -> 1;\n") && dot.contains("  1 -> 2;\n"));
1825    /// ```
1826    pub fn write_graph_dot_to_string(&self) -> Result<String> {
1827        write_to_string(|f| unsafe { igraph_write_graph_dot(self, f) })
1828    }
1829
1830    /// Writes the graph in the LEDA native graph format.
1831    ///
1832    /// Only the graph section is written: the vertices, then the edges with
1833    /// 1-based endpoints (and, for undirected graphs, a `-2` direction flag).
1834    /// `vertex_attr`/`edge_attr` name one vertex and one edge attribute whose
1835    /// values are stored, with their LEDA type (`double`, `string` or `bool`)
1836    /// in the header; `void` is written for `None` and for attributes that
1837    /// don't exist (with a warning; always the case without the
1838    /// [attribute handler](crate::attributes::enable), see the
1839    /// [module docs](self#attributes)). Time complexity: O(|V| + |E|).
1840    ///
1841    /// Binds [`igraph_write_graph_leda`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_leda).
1842    ///
1843    /// # Errors
1844    /// [`ErrorKind::File`] on I/O errors, [`ErrorKind::InvalidValue`] for
1845    /// attribute names with NUL bytes or string values containing a newline.
1846    pub fn write_graph_leda(
1847        &self,
1848        path: impl AsRef<Path>,
1849        vertex_attr: Option<&str>,
1850        edge_attr: Option<&str>,
1851    ) -> Result<()> {
1852        let (v, e) = (
1853            opt_cstring(vertex_attr, "vertex_attr")?,
1854            opt_cstring(edge_attr, "edge_attr")?,
1855        );
1856        write_to_path(path.as_ref(), |f| unsafe {
1857            igraph_write_graph_leda(self, f, opt_ptr(&v), opt_ptr(&e))
1858        })
1859    }
1860
1861    /// The LEDA serialization as a string, see [`write_graph_leda`](Self::write_graph_leda).
1862    ///
1863    /// # Examples
1864    /// ```
1865    /// use igraph::prelude::*;
1866    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], 3, true).unwrap();
1867    /// let leda = g.write_graph_leda_to_string(None, None).unwrap();
1868    /// assert_eq!(
1869    ///     leda,
1870    ///     "LEDA.GRAPH\nvoid\nvoid\n-1\n# Vertices\n3\n|{}|\n|{}|\n|{}|\n# Edges\n3\n\
1871    ///      1 2 0 |{}|\n2 3 0 |{}|\n3 1 0 |{}|\n"
1872    /// );
1873    /// ```
1874    pub fn write_graph_leda_to_string(
1875        &self,
1876        vertex_attr: Option<&str>,
1877        edge_attr: Option<&str>,
1878    ) -> Result<String> {
1879        let (v, e) = (
1880            opt_cstring(vertex_attr, "vertex_attr")?,
1881            opt_cstring(edge_attr, "edge_attr")?,
1882        );
1883        write_to_string(|f| unsafe { igraph_write_graph_leda(self, f, opt_ptr(&v), opt_ptr(&e)) })
1884    }
1885}
1886
1887// ---------------------------------------------------------------------------
1888// Locale
1889// ---------------------------------------------------------------------------
1890
1891/// RAII guard temporarily switching the numeric locale to `"C"`
1892/// (`igraph_enter_safelocale` / `igraph_exit_safelocale`).
1893///
1894/// igraph's readers and writers need a locale with a decimal *point*. While
1895/// the guard is alive the `"C"` numeric locale is in effect for the current
1896/// thread (on platforms without per-thread locales igraph falls back to the
1897/// process-wide `setlocale`, which is not thread safe); dropping it restores
1898/// the previous locale. Rust programs start in the `"C"` locale, so this is
1899/// only needed when some other code changed it.
1900///
1901/// The guard is neither [`Send`] nor [`Sync`]: it must be dropped on the
1902/// thread that created it. Nested guards restore their locales correctly
1903/// when dropped in reverse order of creation (as scopes do).
1904///
1905/// Binds [`igraph_enter_safelocale`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_enter_safelocale)
1906/// and [`igraph_exit_safelocale`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_exit_safelocale).
1907///
1908/// ```
1909/// use igraph::{foreign::SafeLocale, prelude::*};
1910/// let g = Graph::from_edges(&[(0, 1)], 2, true).unwrap();
1911/// let text = {
1912///     let _locale = SafeLocale::enter().unwrap();
1913///     g.write_graph_dimacs_flow_to_string(0, 1, &[0.5]).unwrap()
1914/// }; // previous locale restored here
1915/// assert!(text.ends_with("a 1 2 0.5\n"));
1916/// ```
1917#[derive(Debug)]
1918pub struct SafeLocale {
1919    loc: igraph_safelocale_t,
1920}
1921
1922impl SafeLocale {
1923    /// Switches the current thread to the `"C"` numeric locale until the
1924    /// returned guard is dropped.
1925    ///
1926    /// # Errors
1927    /// [`ErrorKind::Failure`] if the `"C"` locale can't be created.
1928    pub fn enter() -> Result<Self> {
1929        let mut loc: igraph_safelocale_t = std::ptr::null_mut();
1930        if let Err(e) = igraph_call!(igraph_enter_safelocale(&mut loc)) {
1931            // igraph 1.0.0 and 1.0.1 return the error without freeing the
1932            // state they allocated (and never switched to): release it.
1933            if !loc.is_null() {
1934                unsafe { igraph_free(loc.cast()) };
1935            }
1936            return Err(e);
1937        }
1938        Ok(Self { loc })
1939    }
1940}
1941
1942impl Drop for SafeLocale {
1943    fn drop(&mut self) {
1944        if !self.loc.is_null() {
1945            unsafe { igraph_exit_safelocale(&mut self.loc) };
1946        }
1947    }
1948}
1949
1950/// Runs `f` with the `"C"` numeric locale in effect, see [`SafeLocale`].
1951///
1952/// # Errors
1953/// The errors of [`SafeLocale::enter`]; `f`'s own result is returned inside
1954/// the `Ok`.
1955///
1956/// ```
1957/// use igraph::{foreign::with_safe_locale, prelude::*};
1958/// let g = with_safe_locale(|| Graph::read_graph_edgelist_from_str("0 1", 0, false)).unwrap().unwrap();
1959/// assert_eq!(g.ecount(), 1);
1960/// ```
1961pub fn with_safe_locale<T>(f: impl FnOnce() -> T) -> Result<T> {
1962    let _guard = SafeLocale::enter()?;
1963    Ok(f())
1964}