fix(tvix): Represent strings as byte arrays

C++ nix uses C-style zero-terminated char pointers to represent strings
internally - however, up to this point, tvix has used Rust `String` and
`str` for string values. Since those are required to be valid utf-8, we
haven't been able to properly represent all the string values that Nix
supports.

To fix that, this change converts the internal representation of the
NixString struct from `Box<str>` to `BString`, from the `bstr` crate -
this is a wrapper around a `Vec<u8>` with extra functions for treating
that byte vector as a "morally string-like" value, which is basically
exactly what we need.

Since this changes a pretty fundamental assumption about a pretty core
type, there are a *lot* of changes in a lot of places to make this work,
but I've tried to keep the general philosophy and intent of most of the
code in most places intact. Most notably, there's nothing that's been
done to make the derivation stuff in //tvix/glue work with non-utf8
strings everywhere, instead opting to just convert to String/str when
passing things into that - there *might* be something to be done there,
but I don't know what the rules should be and I don't want to figure
them out in this change.

To deal with OS-native paths in a way that also works in WASM for
tvixbolt, this also adds a dependency on the "os_str_bytes" crate.

Fixes: b/189
Fixes: b/337
Change-Id: I5e6eb29c62f47dd91af954f5e12bfc3d186f5526
Reviewed-on: https://cl.tvl.fyi/c/depot/+/10200
Reviewed-by: tazjin <tazjin@tvl.su>
Reviewed-by: flokli <flokli@flokli.de>
Reviewed-by: sterni <sternenseemann@systemli.org>
Autosubmit: aspen <root@gws.fyi>
Tested-by: BuildkiteCI
This commit is contained in:
Aspen Smith 2023-12-05 17:25:52 -05:00 committed by aspen
parent 6f9e25943f
commit 201173afac
24 changed files with 427 additions and 223 deletions

View file

@ -41,6 +41,17 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
[[package]]
name = "bstr"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.14.0" version = "3.14.0"
@ -477,6 +488,15 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "os_str_bytes"
version = "6.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "path-clean" name = "path-clean"
version = "0.1.0" version = "0.1.0"
@ -833,6 +853,7 @@ dependencies = [
name = "tvix-eval" name = "tvix-eval"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bstr",
"bytes", "bytes",
"codemap", "codemap",
"codemap-diagnostic", "codemap-diagnostic",
@ -842,6 +863,7 @@ dependencies = [
"itertools", "itertools",
"lazy_static", "lazy_static",
"lexical-core", "lexical-core",
"os_str_bytes",
"path-clean", "path-clean",
"regex", "regex",
"rnix", "rnix",

12
tvix/Cargo.lock generated
View file

@ -1712,6 +1712,15 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "os_str_bytes"
version = "6.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -3320,6 +3329,7 @@ dependencies = [
name = "tvix-eval" name = "tvix-eval"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bstr",
"bytes", "bytes",
"codemap", "codemap",
"codemap-diagnostic", "codemap-diagnostic",
@ -3330,6 +3340,7 @@ dependencies = [
"itertools 0.12.0", "itertools 0.12.0",
"lazy_static", "lazy_static",
"lexical-core", "lexical-core",
"os_str_bytes",
"path-clean", "path-clean",
"pretty_assertions", "pretty_assertions",
"proptest", "proptest",
@ -3389,6 +3400,7 @@ dependencies = [
name = "tvix-serde" name = "tvix-serde"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bstr",
"serde", "serde",
"tvix-eval", "tvix-eval",
] ]

View file

@ -5163,6 +5163,30 @@ rec {
}; };
resolvedDefaultFeatures = [ "default" "std" ]; resolvedDefaultFeatures = [ "default" "std" ];
}; };
"os_str_bytes" = rec {
crateName = "os_str_bytes";
version = "6.6.1";
edition = "2021";
sha256 = "1885z1x4sm86v5p41ggrl49m58rbzzhd1kj72x46yy53p62msdg2";
authors = [
"dylni"
];
dependencies = [
{
name = "memchr";
packageId = "memchr";
optional = true;
}
];
features = {
"checked_conversions" = [ "conversions" ];
"default" = [ "memchr" "raw_os_str" ];
"memchr" = [ "dep:memchr" ];
"print_bytes" = [ "dep:print_bytes" ];
"uniquote" = [ "dep:uniquote" ];
};
resolvedDefaultFeatures = [ "conversions" "default" "memchr" "raw_os_str" ];
};
"overload" = rec { "overload" = rec {
crateName = "overload"; crateName = "overload";
version = "0.1.1"; version = "0.1.1";
@ -10402,6 +10426,11 @@ rec {
else ./eval; else ./eval;
libName = "tvix_eval"; libName = "tvix_eval";
dependencies = [ dependencies = [
{
name = "bstr";
packageId = "bstr";
features = [ "serde" ];
}
{ {
name = "bytes"; name = "bytes";
packageId = "bytes"; packageId = "bytes";
@ -10441,6 +10470,11 @@ rec {
packageId = "lexical-core"; packageId = "lexical-core";
features = [ "format" "parse-floats" ]; features = [ "format" "parse-floats" ];
} }
{
name = "os_str_bytes";
packageId = "os_str_bytes";
features = [ "conversions" ];
}
{ {
name = "path-clean"; name = "path-clean";
packageId = "path-clean"; packageId = "path-clean";
@ -10684,6 +10718,11 @@ rec {
then lib.cleanSourceWith { filter = sourceFilter; src = ./serde; } then lib.cleanSourceWith { filter = sourceFilter; src = ./serde; }
else ./serde; else ./serde;
dependencies = [ dependencies = [
{
name = "bstr";
packageId = "bstr";
features = [ "serde" ];
}
{ {
name = "serde"; name = "serde";
packageId = "serde"; packageId = "serde";

View file

@ -226,7 +226,7 @@ fn run_file(mut path: PathBuf, args: &Args) {
fn println_result(result: &Value, raw: bool) { fn println_result(result: &Value, raw: bool) {
if raw { if raw {
println!("{}", result.to_contextful_str().unwrap().as_str()) println!("{}", result.to_contextful_str().unwrap())
} else { } else {
println!("=> {} :: {}", result, result.type_of()) println!("=> {} :: {}", result, result.type_of())
} }

View file

@ -11,6 +11,7 @@ name = "tvix_eval"
[dependencies] [dependencies]
builtin-macros = { path = "./builtin-macros", package = "tvix-eval-builtin-macros" } builtin-macros = { path = "./builtin-macros", package = "tvix-eval-builtin-macros" }
bytes = "1.4.0" bytes = "1.4.0"
bstr = { version = "1.8.0", features = ["serde"] }
codemap = "0.1.3" codemap = "0.1.3"
codemap-diagnostic = "0.1.1" codemap-diagnostic = "0.1.1"
dirs = "4.0.0" dirs = "4.0.0"
@ -19,6 +20,7 @@ imbl = { version = "2.0", features = [ "serde" ] }
itertools = "0.12.0" itertools = "0.12.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
lexical-core = { version = "0.8.5", features = ["format", "parse-floats"] } lexical-core = { version = "0.8.5", features = ["format", "parse-floats"] }
os_str_bytes = { version = "6.3", features = ["conversions"] }
path-clean = "0.1" path-clean = "0.1"
proptest = { version = "1.3.0", default_features = false, features = ["std", "alloc", "tempfile"], optional = true } proptest = { version = "1.3.0", default_features = false, features = ["std", "alloc", "tempfile"], optional = true }
regex = "1.6.0" regex = "1.6.0"

View file

@ -17,12 +17,17 @@ use crate::{
#[builtins] #[builtins]
mod impure_builtins { mod impure_builtins {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use super::*; use super::*;
use crate::builtins::coerce_value_to_path; use crate::builtins::coerce_value_to_path;
#[builtin("getEnv")] #[builtin("getEnv")]
async fn builtin_get_env(co: GenCo, var: Value) -> Result<Value, ErrorKind> { async fn builtin_get_env(co: GenCo, var: Value) -> Result<Value, ErrorKind> {
Ok(env::var(var.to_str()?).unwrap_or_else(|_| "".into()).into()) Ok(env::var(OsStr::from_bytes(&var.to_str()?))
.unwrap_or_else(|_| "".into())
.into())
} }
#[builtin("pathExists")] #[builtin("pathExists")]

View file

@ -3,6 +3,7 @@
//! See //tvix/eval/docs/builtins.md for a some context on the //! See //tvix/eval/docs/builtins.md for a some context on the
//! available builtins in Nix. //! available builtins in Nix.
use bstr::ByteVec;
use builtin_macros::builtins; use builtin_macros::builtins;
use genawaiter::rc::Gen; use genawaiter::rc::Gen;
use imbl::OrdMap; use imbl::OrdMap;
@ -66,7 +67,7 @@ pub async fn coerce_value_to_path(
.await .await
{ {
Ok(vs) => { Ok(vs) => {
let path = PathBuf::from(vs.as_str()); let path = (**vs).clone().into_path_buf()?;
if path.is_absolute() { if path.is_absolute() {
Ok(Ok(path)) Ok(Ok(path))
} else { } else {
@ -79,8 +80,12 @@ pub async fn coerce_value_to_path(
#[builtins] #[builtins]
mod pure_builtins { mod pure_builtins {
use std::ffi::OsString;
use bstr::{BString, ByteSlice};
use imbl::Vector; use imbl::Vector;
use itertools::Itertools; use itertools::Itertools;
use os_str_bytes::OsStringBytes;
use crate::{value::PointerEquality, NixContext, NixContextElement}; use crate::{value::PointerEquality, NixContext, NixContextElement};
@ -187,7 +192,7 @@ mod pure_builtins {
#[builtin("baseNameOf")] #[builtin("baseNameOf")]
async fn builtin_base_name_of(co: GenCo, s: Value) -> Result<Value, ErrorKind> { async fn builtin_base_name_of(co: GenCo, s: Value) -> Result<Value, ErrorKind> {
let span = generators::request_span(&co).await; let span = generators::request_span(&co).await;
let s = match s { let mut s = match s {
val @ Value::Catchable(_) => return Ok(val), val @ Value::Catchable(_) => return Ok(val),
_ => s _ => s
.coerce_to_string( .coerce_to_string(
@ -201,11 +206,12 @@ mod pure_builtins {
.await? .await?
.to_contextful_str()?, .to_contextful_str()?,
}; };
let result: NixString = NixString::new_inherit_context_from(
&s, let bs = s.as_mut_bstring();
s.rsplit_once('/').map(|(_, x)| x).unwrap_or(&s), if let Some(last_slash) = bs.rfind_char('/') {
); *bs = bs[(last_slash + 1)..].into();
Ok(result.into()) }
Ok(s.into())
} }
#[builtin("bitAnd")] #[builtin("bitAnd")]
@ -240,7 +246,7 @@ mod pure_builtins {
for item in list.into_iter() { for item in list.into_iter() {
let set = generators::request_force(&co, item).await.to_attrs()?; let set = generators::request_force(&co, item).await.to_attrs()?;
if let Some(value) = set.select(key.as_str()) { if let Some(value) = set.select(&key) {
output.push(value.clone()); output.push(value.clone());
} }
} }
@ -256,9 +262,9 @@ mod pure_builtins {
#[builtin("compareVersions")] #[builtin("compareVersions")]
async fn builtin_compare_versions(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> { async fn builtin_compare_versions(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
let s1 = x.to_str()?; let s1 = x.to_str()?;
let s1 = VersionPartsIter::new_for_cmp(s1.as_str()); let s1 = VersionPartsIter::new_for_cmp((&s1).into());
let s2 = y.to_str()?; let s2 = y.to_str()?;
let s2 = VersionPartsIter::new_for_cmp(s2.as_str()); let s2 = VersionPartsIter::new_for_cmp((&s2).into());
match s1.cmp(s2) { match s1.cmp(s2) {
std::cmp::Ordering::Less => Ok(Value::Integer(-1)), std::cmp::Ordering::Less => Ok(Value::Integer(-1)),
@ -323,7 +329,7 @@ mod pure_builtins {
context = context.join(sep_context); context = context.join(sep_context);
} }
let list = list.to_list()?; let list = list.to_list()?;
let mut res = String::new(); let mut res = BString::default();
for (i, val) in list.into_iter().enumerate() { for (i, val) in list.into_iter().enumerate() {
if i != 0 { if i != 0 {
res.push_str(&separator); res.push_str(&separator);
@ -339,7 +345,7 @@ mod pure_builtins {
.await .await
{ {
Ok(mut s) => { Ok(mut s) => {
res.push_str(s.as_str()); res.push_str(&s);
if let Some(ref mut other_context) = s.context_mut() { if let Some(ref mut other_context) = s.context_mut() {
// It is safe to consume the other context here // It is safe to consume the other context here
// because the `list` and `separator` are originally // because the `list` and `separator` are originally
@ -353,7 +359,7 @@ mod pure_builtins {
} }
} }
// FIXME: pass immediately the string res. // FIXME: pass immediately the string res.
Ok(NixString::new_context_from(context, &res).into()) Ok(NixString::new_context_from(context, res).into())
} }
#[builtin("deepSeq")] #[builtin("deepSeq")]
@ -383,17 +389,24 @@ mod pure_builtins {
.await? .await?
.to_contextful_str()?; .to_contextful_str()?;
let result = str let result = str
.rsplit_once('/') .rfind_char('/')
.map(|(x, _)| match x { .map(|last_slash| {
"" => "/", let x = &str[..last_slash];
_ => x, if x.is_empty() {
b"/"
} else {
x
}
}) })
.unwrap_or("."); .unwrap_or(b".");
if is_path { if is_path {
Ok(Value::Path(Box::new(result.into()))) Ok(Value::Path(Box::new(PathBuf::from(
OsString::assert_from_raw_vec(result.to_owned()),
))))
} else { } else {
Ok(Value::String(NixString::new_inherit_context_from( Ok(Value::String(NixString::new_inherit_context_from(
&str, result, &str,
result.into(),
))) )))
} }
} }
@ -519,7 +532,7 @@ mod pure_builtins {
let json_str = json.to_str()?; let json_str = json.to_str()?;
serde_json::from_str(&json_str).map_err(|err| err.into()) serde_json::from_slice(&json_str).map_err(|err| err.into())
} }
#[builtin("toJSON")] #[builtin("toJSON")]
@ -537,7 +550,7 @@ mod pure_builtins {
async fn builtin_from_toml(co: GenCo, toml: Value) -> Result<Value, ErrorKind> { async fn builtin_from_toml(co: GenCo, toml: Value) -> Result<Value, ErrorKind> {
let toml_str = toml.to_str()?; let toml_str = toml.to_str()?;
toml::from_str(&toml_str).map_err(|err| err.into()) toml::from_str(toml_str.to_str()?).map_err(|err| err.into())
} }
#[builtin("filterSource")] #[builtin("filterSource")]
@ -632,7 +645,7 @@ mod pure_builtins {
let k = key.to_str()?; let k = key.to_str()?;
let xs = set.to_attrs()?; let xs = set.to_attrs()?;
match xs.select(k.as_str()) { match xs.select(&k) {
Some(x) => Ok(x.clone()), Some(x) => Ok(x.clone()),
None => Err(ErrorKind::AttributeNotFound { None => Err(ErrorKind::AttributeNotFound {
name: k.to_string(), name: k.to_string(),
@ -680,7 +693,7 @@ mod pure_builtins {
let k = key.to_str()?; let k = key.to_str()?;
let xs = set.to_attrs()?; let xs = set.to_attrs()?;
Ok(Value::Bool(xs.contains(k.as_str()))) Ok(Value::Bool(xs.contains(&k)))
} }
#[builtin("hasContext")] #[builtin("hasContext")]
@ -1069,8 +1082,8 @@ mod pure_builtins {
return Ok(re); return Ok(re);
} }
let re = re.to_str()?; let re = re.to_str()?;
let re: Regex = Regex::new(&format!("^{}$", re.as_str())).unwrap(); let re: Regex = Regex::new(&format!("^{}$", re.to_str()?)).unwrap();
match re.captures(&s) { match re.captures(s.to_str()?) {
Some(caps) => Ok(Value::List( Some(caps) => Ok(Value::List(
caps.iter() caps.iter()
.skip(1) .skip(1)
@ -1106,7 +1119,7 @@ mod pure_builtins {
// This replicates cppnix's (mis?)handling of codepoints // This replicates cppnix's (mis?)handling of codepoints
// above U+007f following 0x2d ('-') // above U+007f following 0x2d ('-')
let s = s.to_str()?; let s = s.to_str()?;
let slice: &[u8] = s.as_str().as_ref(); let slice: &[u8] = s.as_ref();
let (name, dash_and_version) = slice.split_at( let (name, dash_and_version) = slice.split_at(
slice slice
.windows(2) .windows(2)
@ -1219,7 +1232,7 @@ mod pure_builtins {
let mut string = s.to_contextful_str()?; let mut string = s.to_contextful_str()?;
let mut res = String::new(); let mut res = BString::default();
let mut i: usize = 0; let mut i: usize = 0;
let mut empty_string_replace = false; let mut empty_string_replace = false;
@ -1248,27 +1261,27 @@ mod pure_builtins {
// We already applied a from->to with an empty from // We already applied a from->to with an empty from
// transformation. // transformation.
// Let's skip it so that we don't loop infinitely // Let's skip it so that we don't loop infinitely
if empty_string_replace && from.as_str().is_empty() { if empty_string_replace && from.is_empty() {
continue; continue;
} }
// if we match the `from` string, let's replace // if we match the `from` string, let's replace
if &string[i..i + from.len()] == from.as_str() { if string[i..i + from.len()] == *from {
res += &to; res.push_str(&to);
i += from.len(); i += from.len();
if let Some(to_ctx) = to.context_mut() { if let Some(to_ctx) = to.context_mut() {
context = context.join(to_ctx); context = context.join(to_ctx);
} }
// remember if we applied the empty from->to // remember if we applied the empty from->to
empty_string_replace = from.as_str().is_empty(); empty_string_replace = from.is_empty();
continue 'outer; continue 'outer;
} }
} }
// If we don't match any `from`, we simply add a character // If we don't match any `from`, we simply add a character
res += &string[i..i + 1]; res.push_str(&string[i..i + 1]);
i += 1; i += 1;
// Since we didn't apply anything transformation, // Since we didn't apply anything transformation,
@ -1286,8 +1299,8 @@ mod pure_builtins {
// We don't need to merge again the context, it's already in the right state. // We don't need to merge again the context, it's already in the right state.
let mut to = elem.1.to_contextful_str()?; let mut to = elem.1.to_contextful_str()?;
if from.as_str().is_empty() { if from.is_empty() {
res += &to; res.push_str(&to);
if let Some(to_ctx) = to.context_mut() { if let Some(to_ctx) = to.context_mut() {
context = context.join(to_ctx); context = context.join(to_ctx);
} }
@ -1295,8 +1308,7 @@ mod pure_builtins {
} }
} }
// FIXME: consume directly the String. Ok(Value::String(NixString::new_context_from(context, res)))
Ok(Value::String(NixString::new_context_from(context, &res)))
} }
#[builtin("seq")] #[builtin("seq")]
@ -1317,9 +1329,9 @@ mod pure_builtins {
} }
let s = str.to_contextful_str()?; let s = str.to_contextful_str()?;
let text = s.as_str(); let text = s.to_str()?;
let re = regex.to_str()?; let re = regex.to_str()?;
let re: Regex = Regex::new(re.as_str()).unwrap(); let re = Regex::new(re.to_str()?).unwrap();
let mut capture_locations = re.capture_locations(); let mut capture_locations = re.capture_locations();
let num_captures = capture_locations.len(); let num_captures = capture_locations.len();
let mut ret = imbl::Vector::new(); let mut ret = imbl::Vector::new();
@ -1329,7 +1341,7 @@ mod pure_builtins {
// push the unmatched characters preceding the match // push the unmatched characters preceding the match
ret.push_back(Value::from(NixString::new_inherit_context_from( ret.push_back(Value::from(NixString::new_inherit_context_from(
&s, &s,
&text[pos..thematch.start()], (&text[pos..thematch.start()]).into(),
))); )));
// Push a list with one element for each capture // Push a list with one element for each capture
@ -1380,7 +1392,7 @@ mod pure_builtins {
return Ok(s); return Ok(s);
} }
let s = s.to_str()?; let s = s.to_str()?;
let s = VersionPartsIter::new(s.as_str()); let s = VersionPartsIter::new((&s).into());
let parts = s let parts = s
.map(|s| { .map(|s| {
@ -1412,7 +1424,7 @@ mod pure_builtins {
return Ok(s); return Ok(s);
} }
Ok(Value::Integer(s.to_contextful_str()?.as_str().len() as i64)) Ok(Value::Integer(s.to_contextful_str()?.len() as i64))
} }
#[builtin("sub")] #[builtin("sub")]
@ -1453,19 +1465,22 @@ mod pure_builtins {
// Nix doesn't assert that the length argument is // Nix doesn't assert that the length argument is
// non-negative when the starting index is GTE the // non-negative when the starting index is GTE the
// string's length. // string's length.
if beg >= x.as_str().len() { if beg >= x.len() {
return Ok(Value::String(NixString::new_inherit_context_from(&x, ""))); return Ok(Value::String(NixString::new_inherit_context_from(
&x,
BString::default(),
)));
} }
let end = if len < 0 { let end = if len < 0 {
x.as_str().len() x.len()
} else { } else {
cmp::min(beg + (len as usize), x.as_str().len()) cmp::min(beg + (len as usize), x.len())
}; };
Ok(Value::String(NixString::new_inherit_context_from( Ok(Value::String(NixString::new_inherit_context_from(
&x, &x,
&x[beg..end], (&x[beg..end]).into(),
))) )))
} }

View file

@ -2,6 +2,7 @@
//! of value information as well as internal tvix state that several //! of value information as well as internal tvix state that several
//! things in nixpkgs rely on. //! things in nixpkgs rely on.
use bstr::ByteSlice;
use std::{io::Write, rc::Rc}; use std::{io::Write, rc::Rc};
use xml::writer::events::XmlEvent; use xml::writer::events::XmlEvent;
use xml::writer::EmitterConfig; use xml::writer::EmitterConfig;
@ -60,7 +61,7 @@ fn value_variant_to_xml<W: Write>(w: &mut EventWriter<W>, value: &Value) -> Resu
Value::Bool(b) => return write_typed_value(w, "bool", b), Value::Bool(b) => return write_typed_value(w, "bool", b),
Value::Integer(i) => return write_typed_value(w, "int", i), Value::Integer(i) => return write_typed_value(w, "int", i),
Value::Float(f) => return write_typed_value(w, "float", f), Value::Float(f) => return write_typed_value(w, "float", f),
Value::String(s) => return write_typed_value(w, "string", s.as_str()), Value::String(s) => return write_typed_value(w, "string", s.to_str()?),
Value::Path(p) => return write_typed_value(w, "path", p.to_string_lossy()), Value::Path(p) => return write_typed_value(w, "path", p.to_string_lossy()),
Value::List(list) => { Value::List(list) => {
@ -77,7 +78,7 @@ fn value_variant_to_xml<W: Write>(w: &mut EventWriter<W>, value: &Value) -> Resu
w.write(XmlEvent::start_element("attrs"))?; w.write(XmlEvent::start_element("attrs"))?;
for elem in attrs.iter() { for elem in attrs.iter() {
w.write(XmlEvent::start_element("attr").attr("name", elem.0.as_str()))?; w.write(XmlEvent::start_element("attr").attr("name", &elem.0.to_str_lossy()))?;
value_variant_to_xml(w, elem.1)?; value_variant_to_xml(w, elem.1)?;
w.write(XmlEvent::end_element())?; w.write(XmlEvent::end_element())?;
} }
@ -101,7 +102,9 @@ fn value_variant_to_xml<W: Write>(w: &mut EventWriter<W>, value: &Value) -> Resu
w.write(attrspat)?; w.write(attrspat)?;
for arg in formals.arguments.iter() { for arg in formals.arguments.iter() {
w.write(XmlEvent::start_element("attr").attr("name", arg.0.as_str()))?; w.write(
XmlEvent::start_element("attr").attr("name", &arg.0.to_str_lossy()),
)?;
w.write(XmlEvent::end_element())?; w.write(XmlEvent::end_element())?;
} }

View file

@ -2,13 +2,15 @@ use std::cmp::Ordering;
use std::iter::{once, Chain, Once}; use std::iter::{once, Chain, Once};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use bstr::{BStr, ByteSlice, B};
/// Version strings can be broken up into Parts. /// Version strings can be broken up into Parts.
/// One Part represents either a string of digits or characters. /// One Part represents either a string of digits or characters.
/// '.' and '_' represent deviders between parts and are not included in any part. /// '.' and '_' represent deviders between parts and are not included in any part.
#[derive(PartialEq, Eq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
pub enum VersionPart<'a> { pub enum VersionPart<'a> {
Word(&'a str), Word(&'a BStr),
Number(&'a str), Number(&'a BStr),
} }
impl PartialOrd for VersionPart<'_> { impl PartialOrd for VersionPart<'_> {
@ -23,15 +25,17 @@ impl Ord for VersionPart<'_> {
(VersionPart::Number(s1), VersionPart::Number(s2)) => { (VersionPart::Number(s1), VersionPart::Number(s2)) => {
// Note: C++ Nix uses `int`, but probably doesn't make a difference // Note: C++ Nix uses `int`, but probably doesn't make a difference
// We trust that the splitting was done correctly and parsing will work // We trust that the splitting was done correctly and parsing will work
let n1: u64 = s1.parse().unwrap(); let n1: u64 = s1.to_str_lossy().parse().unwrap();
let n2: u64 = s2.parse().unwrap(); let n2: u64 = s2.to_str_lossy().parse().unwrap();
n1.cmp(&n2) n1.cmp(&n2)
} }
// `pre` looses unless the other part is also a `pre` // `pre` looses unless the other part is also a `pre`
(VersionPart::Word("pre"), VersionPart::Word("pre")) => Ordering::Equal, (VersionPart::Word(x), VersionPart::Word(y)) if *x == B("pre") && *y == B("pre") => {
(VersionPart::Word("pre"), _) => Ordering::Less, Ordering::Equal
(_, VersionPart::Word("pre")) => Ordering::Greater, }
(VersionPart::Word(x), _) if *x == B("pre") => Ordering::Less,
(_, VersionPart::Word(y)) if *y == B("pre") => Ordering::Greater,
// Number wins against Word // Number wins against Word
(VersionPart::Number(_), VersionPart::Word(_)) => Ordering::Greater, (VersionPart::Number(_), VersionPart::Word(_)) => Ordering::Greater,
@ -54,12 +58,12 @@ enum InternalPart {
/// This can then be directly used to compare two versions /// This can then be directly used to compare two versions
pub struct VersionPartsIter<'a> { pub struct VersionPartsIter<'a> {
cached_part: InternalPart, cached_part: InternalPart,
iter: std::str::CharIndices<'a>, iter: bstr::CharIndices<'a>,
version: &'a str, version: &'a BStr,
} }
impl<'a> VersionPartsIter<'a> { impl<'a> VersionPartsIter<'a> {
pub fn new(version: &'a str) -> Self { pub fn new(version: &'a BStr) -> Self {
Self { Self {
cached_part: InternalPart::Break, cached_part: InternalPart::Break,
iter: version.char_indices(), iter: version.char_indices(),
@ -77,8 +81,8 @@ impl<'a> VersionPartsIter<'a> {
/// like `2.3 < 2.3.0pre` ensues. Luckily for us, this means that we can /// like `2.3 < 2.3.0pre` ensues. Luckily for us, this means that we can
/// lexicographically compare two version strings, _if_ we append an extra /// lexicographically compare two version strings, _if_ we append an extra
/// component to both versions. /// component to both versions.
pub fn new_for_cmp(version: &'a str) -> Chain<Self, Once<VersionPart>> { pub fn new_for_cmp(version: &'a BStr) -> Chain<Self, Once<VersionPart>> {
Self::new(version).chain(once(VersionPart::Word(""))) Self::new(version).chain(once(VersionPart::Word("".into())))
} }
} }
@ -101,7 +105,7 @@ impl<'a> Iterator for VersionPartsIter<'a> {
} }
} }
let (pos, char) = char.unwrap(); let (start, end, char) = char.unwrap();
match char { match char {
// Divider encountered // Divider encountered
'.' | '-' => { '.' | '-' => {
@ -119,7 +123,9 @@ impl<'a> Iterator for VersionPartsIter<'a> {
_ if char.is_ascii_digit() => { _ if char.is_ascii_digit() => {
let cached_part = std::mem::replace( let cached_part = std::mem::replace(
&mut self.cached_part, &mut self.cached_part,
InternalPart::Number { range: pos..=pos }, InternalPart::Number {
range: start..=(end - 1),
},
); );
match cached_part { match cached_part {
InternalPart::Number { range } => { InternalPart::Number { range } => {
@ -135,7 +141,9 @@ impl<'a> Iterator for VersionPartsIter<'a> {
// char encountered // char encountered
_ => { _ => {
let mut cached_part = InternalPart::Word { range: pos..=pos }; let mut cached_part = InternalPart::Word {
range: start..=(end - 1),
};
std::mem::swap(&mut cached_part, &mut self.cached_part); std::mem::swap(&mut cached_part, &mut self.cached_part);
match cached_part { match cached_part {
InternalPart::Word { range } => { InternalPart::Word { range } => {

View file

@ -6,6 +6,7 @@
//! instance, or observers). //! instance, or observers).
use super::GlobalsMap; use super::GlobalsMap;
use bstr::ByteSlice;
use genawaiter::rc::Gen; use genawaiter::rc::Gen;
use std::rc::Weak; use std::rc::Weak;
@ -40,11 +41,11 @@ async fn import_impl(
// TODO(tazjin): make this return a string directly instead // TODO(tazjin): make this return a string directly instead
let contents: Value = generators::request_read_to_string(&co, path.clone()).await; let contents: Value = generators::request_read_to_string(&co, path.clone()).await;
let contents = contents.to_str()?.as_str().to_string(); let contents = contents.to_str()?.to_str()?.to_owned();
let parsed = rnix::ast::Root::parse(&contents); let parsed = rnix::ast::Root::parse(&contents);
let errors = parsed.errors(); let errors = parsed.errors();
let file = source.add_file(path.to_string_lossy().to_string(), contents); let file = source.add_file(path.to_string_lossy().to_string(), contents.to_owned());
if !errors.is_empty() { if !errors.is_empty() {
return Err(ErrorKind::ImportParseError { return Err(ErrorKind::ImportParseError {

View file

@ -727,7 +727,7 @@ impl Compiler<'_> {
if let (Some(attr), None) = (path_iter.next(), path_iter.next()) { if let (Some(attr), None) = (path_iter.next(), path_iter.next()) {
// Only do this optimisation for statically known attrs. // Only do this optimisation for statically known attrs.
if let Some(ident) = expr_static_attr_str(&attr) { if let Some(ident) = expr_static_attr_str(&attr) {
if let Some(selected_value) = attrs.select(ident.as_str()) { if let Some(selected_value) = attrs.select(ident.as_bytes()) {
*constant = selected_value.clone(); *constant = selected_value.clone();
// If this worked, we can unthunk the current thunk. // If this worked, we can unthunk the current thunk.

View file

@ -178,6 +178,9 @@ pub enum ErrorKind {
formals_span: Span, formals_span: Span,
}, },
/// Invalid UTF-8 was encoutered somewhere
Utf8,
/// Errors while serialising to XML. /// Errors while serialising to XML.
Xml(Rc<XmlError>), Xml(Rc<XmlError>),
@ -245,6 +248,18 @@ impl From<FromUtf8Error> for ErrorKind {
} }
} }
impl From<bstr::Utf8Error> for ErrorKind {
fn from(_: bstr::Utf8Error) -> Self {
Self::Utf8
}
}
impl From<bstr::FromUtf8Error> for ErrorKind {
fn from(_value: bstr::FromUtf8Error) -> Self {
Self::Utf8
}
}
impl From<XmlError> for ErrorKind { impl From<XmlError> for ErrorKind {
fn from(err: XmlError) -> Self { fn from(err: XmlError) -> Self {
Self::Xml(Rc::new(err)) Self::Xml(Rc::new(err))
@ -457,11 +472,11 @@ to a missing value in the attribute set(s) included via `with`."#,
} }
ErrorKind::UnexpectedArgument { arg, .. } => { ErrorKind::UnexpectedArgument { arg, .. } => {
write!( write!(f, "Unexpected argument `{arg}` supplied to function",)
f, }
"Unexpected argument `{}` supplied to function",
arg.as_str() ErrorKind::Utf8 => {
) write!(f, "Invalid UTF-8 in string")
} }
ErrorKind::Xml(error) => write!(f, "failed to serialise to XML: {error}"), ErrorKind::Xml(error) => write!(f, "failed to serialise to XML: {error}"),
@ -775,6 +790,7 @@ impl Error {
| ErrorKind::NotSerialisableToJson(_) | ErrorKind::NotSerialisableToJson(_)
| ErrorKind::FromTomlError(_) | ErrorKind::FromTomlError(_)
| ErrorKind::Xml(_) | ErrorKind::Xml(_)
| ErrorKind::Utf8
| ErrorKind::TvixError(_) | ErrorKind::TvixError(_)
| ErrorKind::TvixBug { .. } | ErrorKind::TvixBug { .. }
| ErrorKind::NotImplemented(_) | ErrorKind::NotImplemented(_)
@ -819,6 +835,7 @@ impl Error {
ErrorKind::FromTomlError(_) => "E035", ErrorKind::FromTomlError(_) => "E035",
ErrorKind::NotSerialisableToJson(_) => "E036", ErrorKind::NotSerialisableToJson(_) => "E036",
ErrorKind::UnexpectedContext => "E037", ErrorKind::UnexpectedContext => "E037",
ErrorKind::Utf8 => "E038",
// Special error code for errors from other Tvix // Special error code for errors from other Tvix
// components. We may want to introduce a code namespacing // components. We may want to introduce a code namespacing

View file

@ -5,8 +5,10 @@
//! //!
//! Due to this, construction and management of attribute sets has //! Due to this, construction and management of attribute sets has
//! some peculiarities that are encapsulated within this module. //! some peculiarities that are encapsulated within this module.
use std::borrow::Borrow;
use std::iter::FromIterator; use std::iter::FromIterator;
use bstr::BStr;
use imbl::{ordmap, OrdMap}; use imbl::{ordmap, OrdMap};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::de::{Deserializer, Error, Visitor}; use serde::de::{Deserializer, Error, Visitor};
@ -67,25 +69,25 @@ impl AttrsRep {
} }
} }
fn select(&self, key: &str) -> Option<&Value> { fn select(&self, key: &BStr) -> Option<&Value> {
match self { match self {
AttrsRep::Empty => None, AttrsRep::Empty => None,
AttrsRep::KV { name, value } => match key { AttrsRep::KV { name, value } => match &**key {
"name" => Some(name), b"name" => Some(name),
"value" => Some(value), b"value" => Some(value),
_ => None, _ => None,
}, },
AttrsRep::Im(map) => map.get(&key.into()), AttrsRep::Im(map) => map.get(key),
} }
} }
fn contains(&self, key: &str) -> bool { fn contains(&self, key: &BStr) -> bool {
match self { match self {
AttrsRep::Empty => false, AttrsRep::Empty => false,
AttrsRep::KV { .. } => key == "name" || key == "value", AttrsRep::KV { .. } => key == "name" || key == "value",
AttrsRep::Im(map) => map.contains_key(&key.into()), AttrsRep::Im(map) => map.contains_key(key),
} }
} }
} }
@ -264,19 +266,30 @@ impl NixAttrs {
} }
/// Select a value from an attribute set by key. /// Select a value from an attribute set by key.
pub fn select(&self, key: &str) -> Option<&Value> { pub fn select<K>(&self, key: &K) -> Option<&Value>
self.0.select(key) where
K: Borrow<BStr> + ?Sized,
{
self.0.select(key.borrow())
} }
/// Select a required value from an attribute set by key, return /// Select a required value from an attribute set by key, return
/// an `AttributeNotFound` error if it is missing. /// an `AttributeNotFound` error if it is missing.
pub fn select_required(&self, key: &str) -> Result<&Value, ErrorKind> { pub fn select_required<K>(&self, key: &K) -> Result<&Value, ErrorKind>
where
K: Borrow<BStr> + ?Sized,
{
self.select(key) self.select(key)
.ok_or_else(|| ErrorKind::AttributeNotFound { name: key.into() }) .ok_or_else(|| ErrorKind::AttributeNotFound {
name: key.borrow().to_string(),
})
} }
pub fn contains(&self, key: &str) -> bool { pub fn contains<'a, K: 'a>(&self, key: K) -> bool
self.0.contains(key) where
&'a BStr: From<K>,
{
self.0.contains(key.into())
} }
/// Construct an iterator over all the key-value pairs in the attribute set. /// Construct an iterator over all the key-value pairs in the attribute set.
@ -423,7 +436,7 @@ fn attempt_optimise_kv(slice: &mut [Value]) -> Option<NixAttrs> {
fn set_attr(attrs: &mut NixAttrs, key: NixString, value: Value) -> Result<(), ErrorKind> { fn set_attr(attrs: &mut NixAttrs, key: NixString, value: Value) -> Result<(), ErrorKind> {
match attrs.0.map_mut().entry(key) { match attrs.0.map_mut().entry(key) {
imbl::ordmap::Entry::Occupied(entry) => Err(ErrorKind::DuplicateAttrsKey { imbl::ordmap::Entry::Occupied(entry) => Err(ErrorKind::DuplicateAttrsKey {
key: entry.key().as_str().to_string(), key: entry.key().to_string(),
}), }),
imbl::ordmap::Entry::Vacant(entry) => { imbl::ordmap::Entry::Vacant(entry) => {

View file

@ -1,3 +1,5 @@
use bstr::B;
use super::*; use super::*;
#[test] #[test]
@ -99,6 +101,6 @@ fn test_map_attrs_iter() {
let mut iter = attrs.iter().collect::<Vec<_>>().into_iter(); let mut iter = attrs.iter().collect::<Vec<_>>().into_iter();
let (k, v) = iter.next().unwrap(); let (k, v) = iter.next().unwrap();
assert!(k == &NixString::from("key")); assert!(k == &NixString::from("key"));
assert!(v.to_str().unwrap().as_str() == "value"); assert_eq!(v.to_str().unwrap(), B("value"));
assert!(iter.next().is_none()); assert!(iter.next().is_none());
} }

View file

@ -7,6 +7,7 @@ use super::{CoercionKind, Value};
use crate::errors::{CatchableErrorKind, ErrorKind}; use crate::errors::{CatchableErrorKind, ErrorKind};
use crate::generators::{self, GenCo}; use crate::generators::{self, GenCo};
use bstr::ByteSlice;
use serde_json::value::to_value; use serde_json::value::to_value;
use serde_json::Value as Json; // name clash with *our* `Value` use serde_json::Value as Json; // name clash with *our* `Value`
use serde_json::{Map, Number}; use serde_json::{Map, Number};
@ -23,7 +24,7 @@ impl Value {
Value::Bool(b) => Json::Bool(b), Value::Bool(b) => Json::Bool(b),
Value::Integer(i) => Json::Number(Number::from(i)), Value::Integer(i) => Json::Number(Number::from(i)),
Value::Float(f) => to_value(f)?, Value::Float(f) => to_value(f)?,
Value::String(s) => Json::String(s.as_str().into()), Value::String(s) => Json::String(s.to_str()?.to_owned()),
Value::Path(p) => { Value::Path(p) => {
let imported = generators::request_path_import(co, *p).await; let imported = generators::request_path_import(co, *p).await;
@ -61,7 +62,7 @@ impl Value {
.await? .await?
{ {
Value::Catchable(cek) => return Ok(Err(cek)), Value::Catchable(cek) => return Ok(Err(cek)),
Value::String(s) => return Ok(Ok(Json::String(s.as_str().to_owned()))), Value::String(s) => return Ok(Ok(Json::String(s.to_str()?.to_owned()))),
_ => panic!("Value::coerce_to_string_() returned a non-string!"), _ => panic!("Value::coerce_to_string_() returned a non-string!"),
} }
} }
@ -76,7 +77,7 @@ impl Value {
let mut out = Map::with_capacity(attrs.len()); let mut out = Map::with_capacity(attrs.len());
for (name, value) in attrs.into_iter_sorted() { for (name, value) in attrs.into_iter_sorted() {
out.insert( out.insert(
name.as_str().to_string(), name.to_str()?.to_owned(),
match generators::request_to_json(co, value).await { match generators::request_to_json(co, value).await {
Ok(v) => v, Ok(v) => v,
Err(cek) => return Ok(Err(cek)), Err(cek) => return Ok(Err(cek)),

View file

@ -6,6 +6,7 @@ use std::num::{NonZeroI32, NonZeroUsize};
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use bstr::{BString, ByteVec};
use lexical_core::format::CXX_LITERAL; use lexical_core::format::CXX_LITERAL;
use serde::Deserialize; use serde::Deserialize;
@ -313,7 +314,7 @@ impl Value {
kind: CoercionKind, kind: CoercionKind,
span: LightSpan, span: LightSpan,
) -> Result<Value, ErrorKind> { ) -> Result<Value, ErrorKind> {
let mut result = String::new(); let mut result = BString::default();
let mut vals = vec![self]; let mut vals = vec![self];
// Track if we are coercing the first value of a list to correctly emit // Track if we are coercing the first value of a list to correctly emit
// separating white spaces. // separating white spaces.
@ -326,18 +327,15 @@ impl Value {
let value = if let Some(v) = vals.pop() { let value = if let Some(v) = vals.pop() {
v.force(co, span.clone()).await? v.force(co, span.clone()).await?
} else { } else {
return Ok(Value::String(NixString::new_context_from( return Ok(Value::String(NixString::new_context_from(context, result)));
context,
result.as_str(),
)));
}; };
let coerced = match (value, kind) { let coerced: Result<BString, _> = match (value, kind) {
// coercions that are always done // coercions that are always done
(Value::String(mut s), _) => { (Value::String(mut s), _) => {
if let Some(ctx) = s.context_mut() { if let Some(ctx) = s.context_mut() {
context = context.join(ctx); context = context.join(ctx);
} }
Ok(s.as_str().to_owned()) Ok(s.into())
} }
// TODO(sterni): Think about proper encoding handling here. This needs // TODO(sterni): Think about proper encoding handling here. This needs
@ -357,7 +355,7 @@ impl Value {
context = context.append(NixContextElement::Plain( context = context.append(NixContextElement::Plain(
imported.to_string_lossy().to_string(), imported.to_string_lossy().to_string(),
)); ));
Ok(imported.to_string_lossy().into_owned()) Ok(imported.into_os_string().into_encoded_bytes().into())
} }
( (
Value::Path(p), Value::Path(p),
@ -365,7 +363,7 @@ impl Value {
import_paths: false, import_paths: false,
.. ..
}, },
) => Ok(p.to_string_lossy().into_owned()), ) => Ok(p.into_os_string().into_encoded_bytes().into()),
// Attribute sets can be converted to strings if they either have an // Attribute sets can be converted to strings if they either have an
// `__toString` attribute which holds a function that receives the // `__toString` attribute which holds a function that receives the
@ -397,14 +395,14 @@ impl Value {
// strong coercions // strong coercions
(Value::Null, CoercionKind { strong: true, .. }) (Value::Null, CoercionKind { strong: true, .. })
| (Value::Bool(false), CoercionKind { strong: true, .. }) => Ok("".to_owned()), | (Value::Bool(false), CoercionKind { strong: true, .. }) => Ok("".into()),
(Value::Bool(true), CoercionKind { strong: true, .. }) => Ok("1".to_owned()), (Value::Bool(true), CoercionKind { strong: true, .. }) => Ok("1".into()),
(Value::Integer(i), CoercionKind { strong: true, .. }) => Ok(format!("{i}")), (Value::Integer(i), CoercionKind { strong: true, .. }) => Ok(format!("{i}").into()),
(Value::Float(f), CoercionKind { strong: true, .. }) => { (Value::Float(f), CoercionKind { strong: true, .. }) => {
// contrary to normal Display, coercing a float to a string will // contrary to normal Display, coercing a float to a string will
// result in unconditional 6 decimal places // result in unconditional 6 decimal places
Ok(format!("{:.6}", f)) Ok(format!("{:.6}", f).into())
} }
// Lists are coerced by coercing their elements and interspersing spaces // Lists are coerced by coercing their elements and interspersing spaces
@ -448,7 +446,7 @@ impl Value {
if let Some(head) = is_list_head { if let Some(head) = is_list_head {
if !head { if !head {
result.push(' '); result.push(b' ');
} else { } else {
is_list_head = Some(false); is_list_head = Some(false);
} }
@ -576,7 +574,7 @@ impl Value {
let s2 = s2.to_str(); let s2 = s2.to_str();
if let (Ok(s1), Ok(s2)) = (s1, s2) { if let (Ok(s1), Ok(s2)) = (s1, s2) {
if s1.as_str() == "derivation" && s2.as_str() == "derivation" { if s1 == "derivation" && s2 == "derivation" {
// TODO(tazjin): are the outPaths really required, // TODO(tazjin): are the outPaths really required,
// or should it fall through? // or should it fall through?
let out1 = a1 let out1 = a1

View file

@ -3,14 +3,13 @@
//! Nix language strings never need to be modified on the language //! Nix language strings never need to be modified on the language
//! level, allowing us to shave off some memory overhead and only //! level, allowing us to shave off some memory overhead and only
//! paying the cost when creating new strings. //! paying the cost when creating new strings.
use bstr::{BStr, BString, ByteSlice, Chars};
use rnix::ast; use rnix::ast;
use std::borrow::{Borrow, Cow};
use std::collections::HashSet; use std::collections::HashSet;
use std::ffi::OsStr; use std::fmt::Display;
use std::hash::Hash; use std::hash::Hash;
use std::ops::Deref; use std::ops::Deref;
use std::path::Path;
use std::str::{self, Utf8Error};
use std::{borrow::Cow, fmt::Display, str::Chars};
use serde::de::{Deserializer, Visitor}; use serde::de::{Deserializer, Visitor};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -147,16 +146,28 @@ impl NixContext {
// FIXME: when serializing, ignore the context? // FIXME: when serializing, ignore the context?
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct NixString(Box<str>, Option<NixContext>); pub struct NixString(BString, Option<NixContext>);
impl PartialEq for NixString { impl PartialEq for NixString {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str() self.as_bstr() == other.as_bstr()
} }
} }
impl Eq for NixString {} impl Eq for NixString {}
impl PartialEq<&[u8]> for NixString {
fn eq(&self, other: &&[u8]) -> bool {
**self == **other
}
}
impl PartialEq<&str> for NixString {
fn eq(&self, other: &&str) -> bool {
**self == other.as_bytes()
}
}
impl PartialOrd for NixString { impl PartialOrd for NixString {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
@ -165,39 +176,49 @@ impl PartialOrd for NixString {
impl Ord for NixString { impl Ord for NixString {
fn cmp(&self, other: &Self) -> std::cmp::Ordering { fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_str().cmp(other.as_str()) self.as_bstr().cmp(other.as_bstr())
} }
} }
impl TryFrom<&[u8]> for NixString { impl From<&BStr> for NixString {
type Error = Utf8Error; fn from(value: &BStr) -> Self {
Self(value.to_owned(), None)
}
}
fn try_from(value: &[u8]) -> Result<Self, Self::Error> { impl From<&[u8]> for NixString {
Ok(Self(Box::from(str::from_utf8(value)?), None)) fn from(value: &[u8]) -> Self {
Self(value.into(), None)
}
}
impl From<Vec<u8>> for NixString {
fn from(value: Vec<u8>) -> Self {
Self(value.into(), None)
} }
} }
impl From<&str> for NixString { impl From<&str> for NixString {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
NixString(Box::from(s), None) Self(s.as_bytes().into(), None)
} }
} }
impl From<String> for NixString { impl From<String> for NixString {
fn from(s: String) -> Self { fn from(s: String) -> Self {
NixString(s.into_boxed_str(), None) Self(s.into(), None)
} }
} }
impl From<(String, Option<NixContext>)> for NixString { impl From<(String, Option<NixContext>)> for NixString {
fn from(s: (String, Option<NixContext>)) -> Self { fn from((s, ctx): (String, Option<NixContext>)) -> Self {
NixString(s.0.into_boxed_str(), s.1) NixString(s.into(), ctx)
} }
} }
impl From<Box<str>> for NixString { impl From<Box<str>> for NixString {
fn from(s: Box<str>) -> Self { fn from(s: Box<str>) -> Self {
Self(s, None) Self(s.into_boxed_bytes().into_vec().into(), None)
} }
} }
@ -207,9 +228,33 @@ impl From<ast::Ident> for NixString {
} }
} }
impl<'a> From<&'a NixString> for &'a BStr {
fn from(s: &'a NixString) -> Self {
BStr::new(&*s.0)
}
}
impl From<NixString> for BString {
fn from(s: NixString) -> Self {
s.0
}
}
impl AsRef<[u8]> for NixString {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl Borrow<BStr> for NixString {
fn borrow(&self) -> &BStr {
self.as_bstr()
}
}
impl Hash for NixString { impl Hash for NixString {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.as_str().hash(state) self.as_bstr().hash(state)
} }
} }
@ -246,6 +291,14 @@ impl<'de> Deserialize<'de> for NixString {
} }
} }
impl Deref for NixString {
type Target = BString;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(feature = "arbitrary")] #[cfg(feature = "arbitrary")]
mod arbitrary { mod arbitrary {
use super::*; use super::*;
@ -264,13 +317,13 @@ mod arbitrary {
} }
impl NixString { impl NixString {
pub fn new_inherit_context_from(other: &NixString, new_contents: &str) -> Self { pub fn new_inherit_context_from(other: &NixString, new_contents: BString) -> Self {
Self(Box::from(new_contents), other.1.clone()) Self(new_contents, other.1.clone())
} }
pub fn new_context_from(context: NixContext, contents: &str) -> Self { pub fn new_context_from(context: NixContext, contents: BString) -> Self {
Self( Self(
Box::from(contents), contents,
if context.is_empty() { if context.is_empty() {
None None
} else { } else {
@ -279,10 +332,22 @@ impl NixString {
) )
} }
pub fn as_str(&self) -> &str { pub fn as_mut_bstring(&mut self) -> &mut BString {
&mut self.0
}
pub fn as_bstr(&self) -> &BStr {
BStr::new(self.as_bytes())
}
pub fn as_bytes(&self) -> &[u8] {
&self.0 &self.0
} }
pub fn into_bstring(self) -> BString {
self.0
}
/// Return a displayable representation of the string as an /// Return a displayable representation of the string as an
/// identifier. /// identifier.
/// ///
@ -290,8 +355,10 @@ impl NixString {
/// set keys, as those are only escaped in the presence of special /// set keys, as those are only escaped in the presence of special
/// characters. /// characters.
pub fn ident_str(&self) -> Cow<str> { pub fn ident_str(&self) -> Cow<str> {
let escaped = nix_escape_string(self.as_str()); let escaped = match self.to_str_lossy() {
Cow::Borrowed(s) => nix_escape_string(s),
Cow::Owned(s) => nix_escape_string(&s).into_owned().into(),
};
match escaped { match escaped {
// A borrowed string is unchanged and can be returned as // A borrowed string is unchanged and can be returned as
// is. // is.
@ -310,8 +377,8 @@ impl NixString {
} }
pub fn concat(&self, other: &Self) -> Self { pub fn concat(&self, other: &Self) -> Self {
let mut s = self.as_str().to_owned(); let mut s = self.to_vec();
s.push_str(other.as_str()); s.extend(other.0.as_slice());
let context = [&self.1, &other.1] let context = [&self.1, &other.1]
.into_iter() .into_iter()
@ -319,7 +386,7 @@ impl NixString {
.fold(NixContext::new(), |acc_ctx, new_ctx| { .fold(NixContext::new(), |acc_ctx, new_ctx| {
acc_ctx.join(&mut new_ctx.clone()) acc_ctx.join(&mut new_ctx.clone())
}); });
Self::new_context_from(context, &s.into_boxed_str()) Self::new_context_from(context, s.into())
} }
pub(crate) fn context_mut(&mut self) -> Option<&mut NixContext> { pub(crate) fn context_mut(&mut self) -> Option<&mut NixContext> {
@ -436,37 +503,11 @@ fn nix_escape_string(input: &str) -> Cow<str> {
impl Display for NixString { impl Display for NixString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("\"")?; f.write_str("\"")?;
f.write_str(&nix_escape_string(self.as_str()))?; f.write_str(&nix_escape_string(&self.to_str_lossy()))?;
f.write_str("\"") f.write_str("\"")
} }
} }
impl AsRef<str> for NixString {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl AsRef<OsStr> for NixString {
fn as_ref(&self) -> &OsStr {
self.as_str().as_ref()
}
}
impl AsRef<Path> for NixString {
fn as_ref(&self) -> &Path {
self.as_str().as_ref()
}
}
impl Deref for NixString {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -12,6 +12,7 @@
pub mod generators; pub mod generators;
mod macros; mod macros;
use bstr::{BString, ByteSlice, ByteVec};
use codemap::Span; use codemap::Span;
use serde_json::json; use serde_json::json;
use std::{cmp::Ordering, collections::HashMap, ops::DerefMut, path::PathBuf, rc::Rc}; use std::{cmp::Ordering, collections::HashMap, ops::DerefMut, path::PathBuf, rc::Rc};
@ -550,14 +551,14 @@ where
let key = key.to_str().with_span(&frame, self)?; let key = key.to_str().with_span(&frame, self)?;
let attrs = attrs.to_attrs().with_span(&frame, self)?; let attrs = attrs.to_attrs().with_span(&frame, self)?;
match attrs.select(key.as_str()) { match attrs.select(&key) {
Some(value) => self.stack.push(value.clone()), Some(value) => self.stack.push(value.clone()),
None => { None => {
return frame.error( return frame.error(
self, self,
ErrorKind::AttributeNotFound { ErrorKind::AttributeNotFound {
name: key.as_str().to_string(), name: (**key).clone().into_string_lossy()
}, },
); );
} }
@ -598,7 +599,7 @@ where
OpCode::OpAttrsTrySelect => { OpCode::OpAttrsTrySelect => {
let key = self.stack_pop().to_str().with_span(&frame, self)?; let key = self.stack_pop().to_str().with_span(&frame, self)?;
let value = match self.stack_pop() { let value = match self.stack_pop() {
Value::Attrs(attrs) => match attrs.select(key.as_str()) { Value::Attrs(attrs) => match attrs.select(&key) {
Some(value) => value.clone(), Some(value) => value.clone(),
None => Value::AttrNotFound, None => Value::AttrNotFound,
}, },
@ -705,7 +706,7 @@ where
self(key, attrs) => { self(key, attrs) => {
let key = key.to_str().with_span(&frame, self)?; let key = key.to_str().with_span(&frame, self)?;
let result = match attrs { let result = match attrs {
Value::Attrs(attrs) => attrs.contains(key.as_str()), Value::Attrs(attrs) => attrs.contains(&key),
// Nix allows use of `?` on non-set types, but // Nix allows use of `?` on non-set types, but
// always returns false in those cases. // always returns false in those cases.
@ -742,7 +743,7 @@ where
self.enqueue_generator("resolve_with", op_span, |co| { self.enqueue_generator("resolve_with", op_span, |co| {
resolve_with( resolve_with(
co, co,
ident.as_str().to_owned(), ident.as_bstr().to_owned(),
with_stack_len, with_stack_len,
closed_with_stack_len, closed_with_stack_len,
) )
@ -966,7 +967,7 @@ where
/// fragments of the stack, evaluating them to strings, and pushing /// fragments of the stack, evaluating them to strings, and pushing
/// the concatenated result string back on the stack. /// the concatenated result string back on the stack.
fn run_interpolate(&mut self, frame: &CallFrame, count: usize) -> EvalResult<()> { fn run_interpolate(&mut self, frame: &CallFrame, count: usize) -> EvalResult<()> {
let mut out = String::new(); let mut out = BString::default();
// Interpolation propagates the context and union them. // Interpolation propagates the context and union them.
let mut context: NixContext = NixContext::new(); let mut context: NixContext = NixContext::new();
@ -980,7 +981,7 @@ where
return Ok(()); return Ok(());
} }
let mut nix_string = val.to_contextful_str().with_span(frame, self)?; let mut nix_string = val.to_contextful_str().with_span(frame, self)?;
out.push_str(nix_string.as_str()); out.push_str(nix_string.as_bstr());
if let Some(nix_string_ctx) = nix_string.context_mut() { if let Some(nix_string_ctx) = nix_string.context_mut() {
context = context.join(nix_string_ctx); context = context.join(nix_string_ctx);
} }
@ -988,7 +989,7 @@ where
// FIXME: consume immediately here the String. // FIXME: consume immediately here the String.
self.stack self.stack
.push(Value::String(NixString::new_context_from(context, &out))); .push(Value::String(NixString::new_context_from(context, out)));
Ok(()) Ok(())
} }
@ -1160,7 +1161,7 @@ where
/// for matching values in the with-stacks carried at runtime. /// for matching values in the with-stacks carried at runtime.
async fn resolve_with( async fn resolve_with(
co: GenCo, co: GenCo,
ident: String, ident: BString,
vm_with_len: usize, vm_with_len: usize,
upvalue_with_len: usize, upvalue_with_len: usize,
) -> Result<Value, ErrorKind> { ) -> Result<Value, ErrorKind> {
@ -1213,7 +1214,7 @@ async fn resolve_with(
} }
} }
Err(ErrorKind::UnknownDynamicVariable(ident)) Err(ErrorKind::UnknownDynamicVariable(ident.to_string()))
} }
// TODO(amjoseph): de-asyncify this // TODO(amjoseph): de-asyncify this
@ -1221,7 +1222,7 @@ async fn add_values(co: GenCo, a: Value, b: Value) -> Result<Value, ErrorKind> {
// What we try to do is solely determined by the type of the first value! // What we try to do is solely determined by the type of the first value!
let result = match (a, b) { let result = match (a, b) {
(Value::Path(p), v) => { (Value::Path(p), v) => {
let mut path = p.to_string_lossy().into_owned(); let mut path = p.into_os_string();
match generators::request_string_coerce( match generators::request_string_coerce(
&co, &co,
v, v,
@ -1243,7 +1244,7 @@ async fn add_values(co: GenCo, a: Value, b: Value) -> Result<Value, ErrorKind> {
.await .await
{ {
Ok(vs) => { Ok(vs) => {
path.push_str(vs.as_str()); path.push(vs.to_os_str()?);
crate::value::canon_path(PathBuf::from(path)).into() crate::value::canon_path(PathBuf::from(path)).into()
} }
Err(c) => Value::Catchable(c), Err(c) => Value::Catchable(c),

View file

@ -125,6 +125,7 @@ pub(crate) mod derivation_builtins {
use std::collections::BTreeMap; use std::collections::BTreeMap;
use super::*; use super::*;
use bstr::{ByteSlice, ByteVec};
use nix_compat::store_path::hash_placeholder; use nix_compat::store_path::hash_placeholder;
use tvix_eval::generators::Gen; use tvix_eval::generators::Gen;
use tvix_eval::{NixContext, NixContextElement, NixString}; use tvix_eval::{NixContext, NixContextElement, NixString};
@ -139,7 +140,7 @@ pub(crate) mod derivation_builtins {
input input
.to_str() .to_str()
.context("looking at output name in builtins.placeholder")? .context("looking at output name in builtins.placeholder")?
.as_str(), .to_str()?,
); );
Ok(placeholder.into()) Ok(placeholder.into())
@ -167,10 +168,10 @@ pub(crate) mod derivation_builtins {
} }
let name = name.to_str().context("determining derivation name")?; let name = name.to_str().context("determining derivation name")?;
if name.is_empty() { if name.is_empty() {
return Err(ErrorKind::Abort("derivation has empty name".to_string())); return Err(ErrorKind::Abort("derivation has empty name".to_string()));
} }
let name = name.to_str()?;
let mut drv = Derivation::default(); let mut drv = Derivation::default();
drv.outputs.insert("out".to_string(), Default::default()); drv.outputs.insert("out".to_string(), Default::default());
@ -199,7 +200,11 @@ pub(crate) mod derivation_builtins {
/// Inserts a key and value into the drv.environment BTreeMap, and fails if the /// Inserts a key and value into the drv.environment BTreeMap, and fails if the
/// key did already exist before. /// key did already exist before.
fn insert_env(drv: &mut Derivation, k: &str, v: BString) -> Result<(), DerivationError> { fn insert_env(
drv: &mut Derivation,
k: &str, /* TODO: non-utf8 env keys */
v: BString,
) -> Result<(), DerivationError> {
if drv.environment.insert(k.into(), v).is_some() { if drv.environment.insert(k.into(), v).is_some() {
return Err(DerivationError::DuplicateEnvVar(k.into())); return Err(DerivationError::DuplicateEnvVar(k.into()));
} }
@ -228,6 +233,7 @@ pub(crate) mod derivation_builtins {
// Some set special fields in the Derivation struct, some change // Some set special fields in the Derivation struct, some change
// behaviour of other functionality. // behaviour of other functionality.
for (arg_name, arg_value) in input.clone().into_iter_sorted() { for (arg_name, arg_value) in input.clone().into_iter_sorted() {
let arg_name = arg_name.to_str()?;
// force the current value. // force the current value.
let value = generators::request_force(&co, arg_value).await; let value = generators::request_force(&co, arg_value).await;
@ -236,7 +242,7 @@ pub(crate) mod derivation_builtins {
continue; continue;
} }
match arg_name.as_str() { match arg_name {
// Command line arguments to the builder. // Command line arguments to the builder.
// These are only set in drv.arguments. // These are only set in drv.arguments.
"args" => { "args" => {
@ -245,7 +251,7 @@ pub(crate) mod derivation_builtins {
Err(cek) => return Ok(Value::Catchable(cek)), Err(cek) => return Ok(Value::Catchable(cek)),
Ok(s) => { Ok(s) => {
input_context.mimic(&s); input_context.mimic(&s);
drv.arguments.push(s.as_str().to_string()) drv.arguments.push((**s).clone().into_string()?)
} }
} }
} }
@ -274,18 +280,18 @@ pub(crate) mod derivation_builtins {
// Populate drv.outputs // Populate drv.outputs
if drv if drv
.outputs .outputs
.insert(output_name.as_str().to_string(), Default::default()) .insert((**output_name).clone().into_string()?, Default::default())
.is_some() .is_some()
{ {
Err(DerivationError::DuplicateOutput( Err(DerivationError::DuplicateOutput(
output_name.as_str().into(), (**output_name).clone().into_string_lossy(),
))? ))?
} }
output_names.push(output_name.as_str().to_string()); output_names.push((**output_name).clone().into_string()?);
} }
// Add drv.environment[outputs] unconditionally. // Add drv.environment[outputs] unconditionally.
insert_env(&mut drv, arg_name.as_str(), output_names.join(" ").into())?; insert_env(&mut drv, arg_name, output_names.join(" ").into())?;
// drv.environment[$output_name] is added after the loop, // drv.environment[$output_name] is added after the loop,
// with whatever is in drv.outputs[$output_name]. // with whatever is in drv.outputs[$output_name].
} }
@ -297,19 +303,21 @@ pub(crate) mod derivation_builtins {
Ok(val_str) => { Ok(val_str) => {
input_context.mimic(&val_str); input_context.mimic(&val_str);
if arg_name.as_str() == "builder" { if arg_name == "builder" {
drv.builder = val_str.as_str().to_owned(); drv.builder = (**val_str).clone().into_string()?;
} else { } else {
drv.system = val_str.as_str().to_owned(); drv.system = (**val_str).clone().into_string()?;
} }
// Either populate drv.environment or structured_attrs. // Either populate drv.environment or structured_attrs.
if let Some(ref mut structured_attrs) = structured_attrs { if let Some(ref mut structured_attrs) = structured_attrs {
// No need to check for dups, we only iterate over every attribute name once // No need to check for dups, we only iterate over every attribute name once
structured_attrs structured_attrs.insert(
.insert(arg_name.as_str().into(), val_str.as_str().into()); arg_name.to_owned(),
(**val_str).clone().into_string()?.into(),
);
} else { } else {
insert_env(&mut drv, arg_name.as_str(), val_str.as_bytes().into())?; insert_env(&mut drv, arg_name, val_str.as_bytes().into())?;
} }
} }
} }
@ -339,14 +347,14 @@ pub(crate) mod derivation_builtins {
}; };
// No need to check for dups, we only iterate over every attribute name once // No need to check for dups, we only iterate over every attribute name once
structured_attrs.insert(arg_name.as_str().to_string(), val_json); structured_attrs.insert(arg_name.to_owned(), val_json);
} else { } else {
match strong_importing_coerce_to_string(&co, value).await { match strong_importing_coerce_to_string(&co, value).await {
Err(cek) => return Ok(Value::Catchable(cek)), Err(cek) => return Ok(Value::Catchable(cek)),
Ok(val_str) => { Ok(val_str) => {
input_context.mimic(&val_str); input_context.mimic(&val_str);
insert_env(&mut drv, arg_name.as_str(), val_str.as_bytes().into())?; insert_env(&mut drv, arg_name, val_str.as_bytes().into())?;
} }
} }
} }
@ -365,7 +373,7 @@ pub(crate) mod derivation_builtins {
if let Some(attr) = attrs.select(key) { if let Some(attr) = attrs.select(key) {
match strong_importing_coerce_to_string(co, attr.clone()).await { match strong_importing_coerce_to_string(co, attr.clone()).await {
Err(cek) => return Ok(Err(cek)), Err(cek) => return Ok(Err(cek)),
Ok(str) => return Ok(Ok(Some(str.as_str().to_string()))), Ok(str) => return Ok(Ok(Some((**str).clone().into_string()?))),
} }
} }
@ -438,11 +446,11 @@ pub(crate) mod derivation_builtins {
}); });
// Mutate the Derivation struct and set output paths // Mutate the Derivation struct and set output paths
drv.calculate_output_paths(&name, &derivation_or_fod_hash_tmp) drv.calculate_output_paths(name, &derivation_or_fod_hash_tmp)
.map_err(DerivationError::InvalidDerivation)?; .map_err(DerivationError::InvalidDerivation)?;
let drv_path = drv let drv_path = drv
.calculate_derivation_path(&name) .calculate_derivation_path(name)
.map_err(DerivationError::InvalidDerivation)?; .map_err(DerivationError::InvalidDerivation)?;
// recompute the hash derivation modulo and add to known_paths // recompute the hash derivation modulo and add to known_paths
@ -508,21 +516,23 @@ pub(crate) mod derivation_builtins {
return Err(ErrorKind::UnexpectedContext); return Err(ErrorKind::UnexpectedContext);
} }
let path = nix_compat::store_path::build_text_path( let path =
name.as_str(), nix_compat::store_path::build_text_path(name.to_str()?, &content, content.iter_plain())
content.as_str(), .map_err(|_e| {
content.iter_plain(), nix_compat::derivation::DerivationError::InvalidOutputName(
) (**name).clone().into_string_lossy(),
.map_err(|_e| { )
nix_compat::derivation::DerivationError::InvalidOutputName(name.as_str().to_string()) })
}) .map_err(DerivationError::InvalidDerivation)?
.map_err(DerivationError::InvalidDerivation)? .to_absolute_path();
.to_absolute_path();
let context: NixContext = NixContextElement::Plain(path.clone()).into(); let context: NixContext = NixContextElement::Plain(path.clone()).into();
// TODO: actually persist the file in the store at that path ... // TODO: actually persist the file in the store at that path ...
Ok(Value::String(NixString::new_context_from(context, &path))) Ok(Value::String(NixString::new_context_from(
context,
path.into(),
)))
} }
} }

View file

@ -74,10 +74,7 @@ mod tests {
match value { match value {
tvix_eval::Value::String(s) => { tvix_eval::Value::String(s) => {
assert_eq!( assert_eq!(s, "/nix/store/xpcvxsx5sw4rbq666blz6sxqlmsqphmr-foo",);
"/nix/store/xpcvxsx5sw4rbq666blz6sxqlmsqphmr-foo",
s.as_str()
);
} }
_ => panic!("unexpected value type: {:?}", value), _ => panic!("unexpected value type: {:?}", value),
} }
@ -162,7 +159,7 @@ mod tests {
match value { match value {
tvix_eval::Value::String(s) => { tvix_eval::Value::String(s) => {
assert_eq!(expected_path, s.as_str()); assert_eq!(s, expected_path);
} }
_ => panic!("unexpected value type: {:?}", value), _ => panic!("unexpected value type: {:?}", value),
} }
@ -285,7 +282,7 @@ mod tests {
match value { match value {
tvix_eval::Value::String(s) => { tvix_eval::Value::String(s) => {
assert_eq!(expected_drvpath, s.as_str()); assert_eq!(s, expected_drvpath);
} }
_ => panic!("unexpected value type: {:?}", value), _ => panic!("unexpected value type: {:?}", value),
@ -314,7 +311,7 @@ mod tests {
match value { match value {
tvix_eval::Value::String(s) => { tvix_eval::Value::String(s) => {
assert_eq!(expected_path, s.as_str()); assert_eq!(s, expected_path);
} }
_ => panic!("unexpected value type: {:?}", value), _ => panic!("unexpected value type: {:?}", value),
} }

View file

@ -297,6 +297,7 @@ impl EvalIO for TvixStoreIO {
mod tests { mod tests {
use std::{path::Path, rc::Rc, sync::Arc}; use std::{path::Path, rc::Rc, sync::Arc};
use bstr::ByteVec;
use tempfile::TempDir; use tempfile::TempDir;
use tvix_build::buildservice::DummyBuildService; use tvix_build::buildservice::DummyBuildService;
use tvix_castore::{ use tvix_castore::{
@ -355,7 +356,7 @@ mod tests {
let value = result.value.expect("must be some"); let value = result.value.expect("must be some");
match value { match value {
tvix_eval::Value::String(s) => return Some(s.as_str().to_owned()), tvix_eval::Value::String(s) => Some((**s).clone().into_string_lossy()),
_ => panic!("unexpected value type: {:?}", value), _ => panic!("unexpected value type: {:?}", value),
} }
} }
@ -421,7 +422,7 @@ mod tests {
match value { match value {
tvix_eval::Value::String(s) => { tvix_eval::Value::String(s) => {
assert_eq!("/deep/thought", s.as_str()); assert_eq!(s, "/deep/thought");
} }
_ => panic!("unexpected value type: {:?}", value), _ => panic!("unexpected value type: {:?}", value),
} }

View file

@ -6,3 +6,4 @@ edition = "2021"
[dependencies] [dependencies]
tvix-eval = { path = "../eval" } tvix-eval = { path = "../eval" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
bstr = { version = "1.8.0", features = ["serde"] }

View file

@ -1,5 +1,6 @@
//! Deserialisation from Nix to Rust values. //! Deserialisation from Nix to Rust values.
use bstr::ByteSlice;
use serde::de::value::{MapDeserializer, SeqDeserializer}; use serde::de::value::{MapDeserializer, SeqDeserializer};
use serde::de::{self, EnumAccess, VariantAccess}; use serde::de::{self, EnumAccess, VariantAccess};
pub use tvix_eval::Evaluation; pub use tvix_eval::Evaluation;
@ -209,7 +210,7 @@ impl<'de> de::Deserializer<'de> for NixDeserializer {
V: de::Visitor<'de>, V: de::Visitor<'de>,
{ {
if let Value::String(s) = &self.value { if let Value::String(s) = &self.value {
let chars = s.as_str().chars().collect::<Vec<_>>(); let chars = s.chars().collect::<Vec<_>>();
if chars.len() == 1 { if chars.len() == 1 {
return visitor.visit_char(chars[0]); return visitor.visit_char(chars[0]);
} }
@ -223,7 +224,9 @@ impl<'de> de::Deserializer<'de> for NixDeserializer {
V: de::Visitor<'de>, V: de::Visitor<'de>,
{ {
if let Value::String(s) = &self.value { if let Value::String(s) = &self.value {
return visitor.visit_str(s.as_str()); if let Ok(s) = s.to_str() {
return visitor.visit_str(s);
}
} }
Err(unexpected("string", &self.value)) Err(unexpected("string", &self.value))
@ -234,7 +237,9 @@ impl<'de> de::Deserializer<'de> for NixDeserializer {
V: de::Visitor<'de>, V: de::Visitor<'de>,
{ {
if let Value::String(s) = &self.value { if let Value::String(s) = &self.value {
return visitor.visit_str(s.as_str()); if let Ok(s) = s.to_str() {
return visitor.visit_str(s);
}
} }
Err(unexpected("string", &self.value)) Err(unexpected("string", &self.value))
@ -379,7 +384,13 @@ impl<'de> de::Deserializer<'de> for NixDeserializer {
{ {
match self.value { match self.value {
// a string represents a unit variant // a string represents a unit variant
Value::String(s) => visitor.visit_enum(de::value::StrDeserializer::new(s.as_str())), Value::String(ref s) => {
if let Ok(s) = s.to_str() {
visitor.visit_enum(de::value::StrDeserializer::new(s))
} else {
Err(unexpected(name, &self.value))
}
}
// an attribute set however represents an externally // an attribute set however represents an externally
// tagged enum with content // tagged enum with content
@ -420,9 +431,12 @@ impl<'de> EnumAccess<'de> for Enum {
} }
let (key, value) = self.0.into_iter().next().expect("length asserted above"); let (key, value) = self.0.into_iter().next().expect("length asserted above");
let val = seed.deserialize(de::value::StrDeserializer::<Error>::new(key.as_str()))?; if let Ok(k) = key.to_str() {
let val = seed.deserialize(de::value::StrDeserializer::<Error>::new(k))?;
Ok((val, NixDeserializer::new(value))) Ok((val, NixDeserializer::new(value)))
} else {
Err(unexpected("string", &key.clone().into()))
}
} }
} }

View file

@ -213,6 +213,7 @@ fn deserialize_with_config() {
#[builtins] #[builtins]
mod test_builtins { mod test_builtins {
use bstr::ByteSlice;
use tvix_eval::generators::{Gen, GenCo}; use tvix_eval::generators::{Gen, GenCo};
use tvix_eval::{ErrorKind, NixString, Value}; use tvix_eval::{ErrorKind, NixString, Value};
@ -220,7 +221,7 @@ mod test_builtins {
pub async fn builtin_prepend_hello(co: GenCo, x: Value) -> Result<Value, ErrorKind> { pub async fn builtin_prepend_hello(co: GenCo, x: Value) -> Result<Value, ErrorKind> {
match x { match x {
Value::String(s) => { Value::String(s) => {
let new_string = NixString::from(format!("hello {}", s.as_str())); let new_string = NixString::from(format!("hello {}", s.to_str().unwrap()));
Ok(Value::String(new_string)) Ok(Value::String(new_string))
} }
_ => Err(ErrorKind::TypeError { _ => Err(ErrorKind::TypeError {