This commit is contained in:
2026-03-12 17:47:46 +08:00
parent 7a7229d70e
commit 0c9a391618
511 changed files with 234 additions and 12772 deletions
+862
View File
@@ -0,0 +1,862 @@
use std::ops::Deref;
use std::path::Path;
use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use rnix::TextRange;
use crate::ir::{ArgId, Attr, BinOpKind, Ir, Param, RawIrRef, SymId, ThunkId, UnOpKind};
#[derive(Clone, Hash, Eq, PartialEq)]
pub(crate) enum Constant {
Int(i64),
Float(u64),
}
pub struct Bytecode {
pub code: Box<[u8]>,
pub current_dir: String,
}
pub(crate) trait BytecodeContext {
fn intern_string(&mut self, s: &str) -> u32;
fn intern_constant(&mut self, c: Constant) -> u32;
fn register_span(&self, range: TextRange) -> u32;
fn get_sym(&self, id: SymId) -> &str;
fn get_current_dir(&self) -> &Path;
}
#[repr(u8)]
#[derive(Clone, Copy, TryFromPrimitive)]
#[allow(clippy::enum_variant_names)]
pub enum Op {
PushConst,
PushString,
PushNull,
PushTrue,
PushFalse,
LoadLocal,
LoadOuter,
StoreLocal,
AllocLocals,
MakeThunk,
MakeClosure,
MakePatternClosure,
Call,
CallNoSpan,
MakeAttrs,
MakeAttrsDyn,
MakeEmptyAttrs,
Select,
SelectDefault,
HasAttr,
MakeList,
OpAdd,
OpSub,
OpMul,
OpDiv,
OpEq,
OpNeq,
OpLt,
OpGt,
OpLeq,
OpGeq,
OpConcat,
OpUpdate,
OpNeg,
OpNot,
ForceBool,
JumpIfFalse,
JumpIfTrue,
Jump,
ConcatStrings,
ResolvePath,
Assert,
PushWith,
PopWith,
WithLookup,
LoadBuiltins,
LoadBuiltin,
MkPos,
LoadReplBinding,
LoadScopedBinding,
Return,
}
struct ScopeInfo {
depth: u16,
arg_id: Option<ArgId>,
thunk_map: HashMap<ThunkId, u32>,
}
struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
ctx: &'a mut Ctx,
code: Vec<u8>,
scope_stack: Vec<ScopeInfo>,
}
pub(crate) fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> Bytecode {
let current_dir = ctx.get_current_dir().to_string_lossy().to_string();
let mut emitter = BytecodeEmitter::new(ctx);
emitter.emit_toplevel(ir);
Bytecode {
code: emitter.code.into_boxed_slice(),
current_dir,
}
}
impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
fn new(ctx: &'a mut Ctx) -> Self {
Self {
ctx,
code: Vec::with_capacity(4096),
scope_stack: Vec::with_capacity(32),
}
}
#[inline]
fn emit_op(&mut self, op: Op) {
self.code.push(op as u8);
}
#[inline]
fn emit_u8(&mut self, val: u8) {
self.code.push(val);
}
#[inline]
fn emit_u16(&mut self, val: u16) {
self.code.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_u32(&mut self, val: u32) {
self.code.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_i32_placeholder(&mut self) -> usize {
let offset = self.code.len();
self.code.extend_from_slice(&[0u8; 4]);
offset
}
#[inline]
fn patch_i32(&mut self, offset: usize, val: i32) {
self.code[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_jump_placeholder(&mut self) -> usize {
self.emit_op(Op::Jump);
self.emit_i32_placeholder()
}
#[inline]
fn patch_jump_target(&mut self, placeholder_offset: usize) {
let current_pos = self.code.len();
let relative_offset = (current_pos as i32) - (placeholder_offset as i32) - 4;
self.patch_i32(placeholder_offset, relative_offset);
}
fn current_depth(&self) -> u16 {
self.scope_stack.last().map_or(0, |s| s.depth)
}
fn resolve_thunk(&self, id: ThunkId) -> (u16, u32) {
for scope in self.scope_stack.iter().rev() {
if let Some(&local_idx) = scope.thunk_map.get(&id) {
let layer = self.current_depth() - scope.depth;
return (layer, local_idx);
}
}
panic!("ThunkId {:?} not found in any scope", id);
}
fn resolve_arg(&self, id: ArgId) -> (u16, u32) {
for scope in self.scope_stack.iter().rev() {
if scope.arg_id == Some(id) {
let layer = self.current_depth() - scope.depth;
return (layer, 0);
}
}
panic!("ArgId {:?} not found in any scope", id);
}
fn emit_load(&mut self, layer: u16, local: u32) {
if layer == 0 {
self.emit_op(Op::LoadLocal);
self.emit_u32(local);
} else {
self.emit_op(Op::LoadOuter);
self.emit_u8(layer as u8);
self.emit_u32(local);
}
}
fn count_with_thunks(&self, ir: RawIrRef<'_>) -> usize {
match ir.deref() {
Ir::With { thunks, body, .. } => thunks.len() + self.count_with_thunks(*body),
Ir::TopLevel { thunks, body } => thunks.len() + self.count_with_thunks(*body),
Ir::If { cond, consq, alter } => {
self.count_with_thunks(*cond)
+ self.count_with_thunks(*consq)
+ self.count_with_thunks(*alter)
}
Ir::BinOp { lhs, rhs, .. } => {
self.count_with_thunks(*lhs) + self.count_with_thunks(*rhs)
}
Ir::UnOp { rhs, .. } => self.count_with_thunks(*rhs),
Ir::Call { func, arg, .. } => {
self.count_with_thunks(*func) + self.count_with_thunks(*arg)
}
Ir::Assert {
assertion, expr, ..
} => self.count_with_thunks(*assertion) + self.count_with_thunks(*expr),
Ir::Select { expr, default, .. } => {
self.count_with_thunks(*expr) + default.map_or(0, |d| self.count_with_thunks(d))
}
Ir::HasAttr { lhs, .. } => self.count_with_thunks(*lhs),
Ir::ConcatStrings { parts, .. } => {
parts.iter().map(|p| self.count_with_thunks(*p)).sum()
}
Ir::Path(p) => self.count_with_thunks(*p),
Ir::List { items } => items.iter().map(|item| self.count_with_thunks(*item)).sum(),
Ir::AttrSet { stcs, dyns } => {
stcs.iter()
.map(|(_, &(val, _))| self.count_with_thunks(val))
.sum::<usize>()
+ dyns
.iter()
.map(|&(k, v, _)| self.count_with_thunks(k) + self.count_with_thunks(v))
.sum::<usize>()
}
_ => 0,
}
}
fn collect_all_thunks<'ir>(
&self,
own_thunks: &[(ThunkId, RawIrRef<'ir>)],
body: RawIrRef<'ir>,
) -> Vec<(ThunkId, RawIrRef<'ir>)> {
let mut all = Vec::from(own_thunks);
self.collect_with_thunks_recursive(body, &mut all);
let mut i = 0;
while i < all.len() {
let thunk_body = all[i].1;
self.collect_with_thunks_recursive(thunk_body, &mut all);
i += 1;
}
all
}
fn collect_with_thunks_recursive<'ir>(
&self,
ir: RawIrRef<'ir>,
out: &mut Vec<(ThunkId, RawIrRef<'ir>)>,
) {
match ir.deref() {
Ir::With { thunks, body, .. } => {
for &(id, inner) in thunks.iter() {
out.push((id, inner));
}
self.collect_with_thunks_recursive(*body, out);
}
Ir::TopLevel { thunks, body } => {
for &(id, inner) in thunks.iter() {
out.push((id, inner));
}
self.collect_with_thunks_recursive(*body, out);
}
Ir::If { cond, consq, alter } => {
self.collect_with_thunks_recursive(*cond, out);
self.collect_with_thunks_recursive(*consq, out);
self.collect_with_thunks_recursive(*alter, out);
}
Ir::BinOp { lhs, rhs, .. } => {
self.collect_with_thunks_recursive(*lhs, out);
self.collect_with_thunks_recursive(*rhs, out);
}
Ir::UnOp { rhs, .. } => self.collect_with_thunks_recursive(*rhs, out),
Ir::Call { func, arg, .. } => {
self.collect_with_thunks_recursive(*func, out);
self.collect_with_thunks_recursive(*arg, out);
}
Ir::Assert {
assertion, expr, ..
} => {
self.collect_with_thunks_recursive(*assertion, out);
self.collect_with_thunks_recursive(*expr, out);
}
Ir::Select { expr, default, .. } => {
self.collect_with_thunks_recursive(*expr, out);
if let Some(d) = default {
self.collect_with_thunks_recursive(*d, out);
}
}
Ir::HasAttr { lhs, .. } => self.collect_with_thunks_recursive(*lhs, out),
Ir::ConcatStrings { parts, .. } => {
for p in parts.iter() {
self.collect_with_thunks_recursive(*p, out);
}
}
Ir::Path(p) => self.collect_with_thunks_recursive(*p, out),
Ir::List { items } => {
for item in items.iter() {
self.collect_with_thunks_recursive(*item, out);
}
}
Ir::AttrSet { stcs, dyns } => {
for (_, &(val, _)) in stcs.iter() {
self.collect_with_thunks_recursive(val, out);
}
for &(key, val, _) in dyns.iter() {
self.collect_with_thunks_recursive(key, out);
self.collect_with_thunks_recursive(val, out);
}
}
_ => {}
}
}
fn push_scope(&mut self, has_arg: bool, arg_id: Option<ArgId>, thunk_ids: &[ThunkId]) {
let depth = self.scope_stack.len() as u16;
let thunk_base = if has_arg { 1u32 } else { 0u32 };
let thunk_map = thunk_ids
.iter()
.enumerate()
.map(|(i, &id)| (id, thunk_base + i as u32))
.collect();
self.scope_stack.push(ScopeInfo {
depth,
arg_id,
thunk_map,
});
}
fn pop_scope(&mut self) {
self.scope_stack.pop();
}
fn emit_toplevel(&mut self, ir: RawIrRef<'_>) {
match ir.deref() {
Ir::TopLevel { body, thunks } => {
let with_thunk_count = self.count_with_thunks(*body);
let total_slots = thunks.len() + with_thunk_count;
let all_thunks = self.collect_all_thunks(thunks, *body);
let thunk_ids: Vec<ThunkId> = all_thunks.iter().map(|&(id, _)| id).collect();
self.push_scope(false, None, &thunk_ids);
if total_slots > 0 {
self.emit_op(Op::AllocLocals);
self.emit_u32(total_slots as u32);
}
self.emit_scope_thunks(thunks);
self.emit_expr(*body);
self.emit_op(Op::Return);
self.pop_scope();
}
_ => {
self.push_scope(false, None, &[]);
self.emit_expr(ir);
self.emit_op(Op::Return);
self.pop_scope();
}
}
}
fn emit_scope_thunks(&mut self, thunks: &[(ThunkId, RawIrRef<'_>)]) {
for &(id, inner) in thunks {
let label = format!("e{}", id.0);
let label_idx = self.ctx.intern_string(&label);
let skip_patch = self.emit_jump_placeholder();
let entry_point = self.code.len() as u32;
self.emit_expr(inner);
self.emit_op(Op::Return);
self.patch_jump_target(skip_patch);
self.emit_op(Op::MakeThunk);
self.emit_u32(entry_point);
self.emit_u32(label_idx);
let (_, local_idx) = self.resolve_thunk(id);
self.emit_op(Op::StoreLocal);
self.emit_u32(local_idx);
}
}
fn emit_expr(&mut self, ir: RawIrRef<'_>) {
match ir.deref() {
&Ir::Int(x) => {
let idx = self.ctx.intern_constant(Constant::Int(x));
self.emit_op(Op::PushConst);
self.emit_u32(idx);
}
&Ir::Float(x) => {
let idx = self.ctx.intern_constant(Constant::Float(x.to_bits()));
self.emit_op(Op::PushConst);
self.emit_u32(idx);
}
&Ir::Bool(true) => self.emit_op(Op::PushTrue),
&Ir::Bool(false) => self.emit_op(Op::PushFalse),
Ir::Null => self.emit_op(Op::PushNull),
Ir::Str(s) => {
let idx = self.ctx.intern_string(s.deref());
self.emit_op(Op::PushString);
self.emit_u32(idx);
}
&Ir::Path(p) => {
self.emit_expr(p);
self.emit_op(Op::ResolvePath);
}
&Ir::If { cond, consq, alter } => {
self.emit_expr(cond);
self.emit_op(Op::ForceBool);
self.emit_op(Op::JumpIfFalse);
let else_placeholder = self.emit_i32_placeholder();
let after_jif = self.code.len();
self.emit_expr(consq);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.code.len();
let else_offset = (after_jump as i32) - (after_jif as i32);
self.patch_i32(else_placeholder, else_offset);
self.emit_expr(alter);
let end_offset = (self.code.len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
&Ir::BinOp { lhs, rhs, kind } => {
self.emit_binop(lhs, rhs, kind);
}
&Ir::UnOp { rhs, kind } => match kind {
UnOpKind::Neg => {
self.emit_expr(rhs);
self.emit_op(Op::OpNeg);
}
UnOpKind::Not => {
self.emit_expr(rhs);
self.emit_op(Op::OpNot);
}
},
&Ir::Func {
body,
ref param,
arg,
ref thunks,
} => {
self.emit_func(arg, thunks, param, body);
}
Ir::AttrSet { stcs, dyns } => {
self.emit_attrset(stcs, dyns);
}
Ir::List { items } => {
for &item in items.iter() {
self.emit_expr(item);
}
self.emit_op(Op::MakeList);
self.emit_u32(items.len() as u32);
}
&Ir::Call { func, arg, span } => {
self.emit_expr(func);
self.emit_expr(arg);
let span_id = self.ctx.register_span(span);
self.emit_op(Op::Call);
self.emit_u32(span_id);
}
&Ir::Arg(id) => {
let (layer, local) = self.resolve_arg(id);
self.emit_load(layer, local);
}
&Ir::TopLevel { body, ref thunks } => {
self.emit_toplevel_inner(body, thunks);
}
&Ir::Select {
expr,
ref attrpath,
default,
span,
} => {
self.emit_select(expr, attrpath, default, span);
}
&Ir::Thunk(id) => {
let (layer, local) = self.resolve_thunk(id);
self.emit_load(layer, local);
}
Ir::Builtins => {
self.emit_op(Op::LoadBuiltins);
}
&Ir::Builtin(name) => {
let sym = self.ctx.get_sym(name).to_string();
let idx = self.ctx.intern_string(&sym);
self.emit_op(Op::LoadBuiltin);
self.emit_u32(idx);
}
&Ir::ConcatStrings {
ref parts,
force_string,
} => {
for &part in parts.iter() {
self.emit_expr(part);
}
self.emit_op(Op::ConcatStrings);
self.emit_u16(parts.len() as u16);
self.emit_u8(if force_string { 1 } else { 0 });
}
&Ir::HasAttr { lhs, ref rhs } => {
self.emit_has_attr(lhs, rhs);
}
Ir::Assert {
assertion,
expr,
assertion_raw,
span,
} => {
let raw_idx = self.ctx.intern_string(assertion_raw);
let span_id = self.ctx.register_span(*span);
self.emit_expr(*assertion);
self.emit_expr(*expr);
self.emit_op(Op::Assert);
self.emit_u32(raw_idx);
self.emit_u32(span_id);
}
&Ir::CurPos(span) => {
let span_id = self.ctx.register_span(span);
self.emit_op(Op::MkPos);
self.emit_u32(span_id);
}
&Ir::ReplBinding(name) => {
let sym = self.ctx.get_sym(name).to_string();
let idx = self.ctx.intern_string(&sym);
self.emit_op(Op::LoadReplBinding);
self.emit_u32(idx);
}
&Ir::ScopedImportBinding(name) => {
let sym = self.ctx.get_sym(name).to_string();
let idx = self.ctx.intern_string(&sym);
self.emit_op(Op::LoadScopedBinding);
self.emit_u32(idx);
}
&Ir::With {
namespace,
body,
ref thunks,
} => {
self.emit_with(namespace, body, thunks);
}
&Ir::WithLookup(name) => {
let sym = self.ctx.get_sym(name).to_string();
let idx = self.ctx.intern_string(&sym);
self.emit_op(Op::WithLookup);
self.emit_u32(idx);
}
}
}
fn emit_binop(&mut self, lhs: RawIrRef<'_>, rhs: RawIrRef<'_>, kind: BinOpKind) {
use BinOpKind::*;
match kind {
And => {
self.emit_expr(lhs);
self.emit_op(Op::ForceBool);
self.emit_op(Op::JumpIfFalse);
let skip_placeholder = self.emit_i32_placeholder();
let after_jif = self.code.len();
self.emit_expr(rhs);
self.emit_op(Op::ForceBool);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.code.len();
let false_offset = (after_jump as i32) - (after_jif as i32);
self.patch_i32(skip_placeholder, false_offset);
self.emit_op(Op::PushFalse);
let end_offset = (self.code.len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
Or => {
self.emit_expr(lhs);
self.emit_op(Op::ForceBool);
self.emit_op(Op::JumpIfTrue);
let skip_placeholder = self.emit_i32_placeholder();
let after_jit = self.code.len();
self.emit_expr(rhs);
self.emit_op(Op::ForceBool);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.code.len();
let true_offset = (after_jump as i32) - (after_jit as i32);
self.patch_i32(skip_placeholder, true_offset);
self.emit_op(Op::PushTrue);
let end_offset = (self.code.len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
Impl => {
self.emit_expr(lhs);
self.emit_op(Op::ForceBool);
self.emit_op(Op::JumpIfFalse);
let skip_placeholder = self.emit_i32_placeholder();
let after_jif = self.code.len();
self.emit_expr(rhs);
self.emit_op(Op::ForceBool);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.code.len();
let true_offset = (after_jump as i32) - (after_jif as i32);
self.patch_i32(skip_placeholder, true_offset);
self.emit_op(Op::PushTrue);
let end_offset = (self.code.len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
PipeL => {
self.emit_expr(rhs);
self.emit_expr(lhs);
self.emit_op(Op::CallNoSpan);
}
PipeR => {
self.emit_expr(lhs);
self.emit_expr(rhs);
self.emit_op(Op::CallNoSpan);
}
_ => {
self.emit_expr(lhs);
self.emit_expr(rhs);
self.emit_op(match kind {
Add => Op::OpAdd,
Sub => Op::OpSub,
Mul => Op::OpMul,
Div => Op::OpDiv,
Eq => Op::OpEq,
Neq => Op::OpNeq,
Lt => Op::OpLt,
Gt => Op::OpGt,
Leq => Op::OpLeq,
Geq => Op::OpGeq,
Con => Op::OpConcat,
Upd => Op::OpUpdate,
_ => unreachable!(),
});
}
}
}
fn emit_func(
&mut self,
arg: ArgId,
thunks: &[(ThunkId, RawIrRef<'_>)],
param: &Option<Param<'_>>,
body: RawIrRef<'_>,
) {
let with_thunk_count = self.count_with_thunks(body);
let total_slots = thunks.len() + with_thunk_count;
let all_thunks = self.collect_all_thunks(thunks, body);
let thunk_ids: Vec<ThunkId> = all_thunks.iter().map(|&(id, _)| id).collect();
let skip_patch = self.emit_jump_placeholder();
let entry_point = self.code.len() as u32;
self.push_scope(true, Some(arg), &thunk_ids);
self.emit_scope_thunks(thunks);
self.emit_expr(body);
self.emit_op(Op::Return);
self.pop_scope();
self.patch_jump_target(skip_patch);
if let Some(Param {
required,
optional,
ellipsis,
}) = param
{
self.emit_op(Op::MakePatternClosure);
self.emit_u32(entry_point);
self.emit_u32(total_slots as u32);
self.emit_u16(required.len() as u16);
self.emit_u16(optional.len() as u16);
self.emit_u8(if *ellipsis { 1 } else { 0 });
for &(sym, _) in required.iter() {
let name = self.ctx.get_sym(sym).to_string();
let idx = self.ctx.intern_string(&name);
self.emit_u32(idx);
}
for &(sym, _) in optional.iter() {
let name = self.ctx.get_sym(sym).to_string();
let idx = self.ctx.intern_string(&name);
self.emit_u32(idx);
}
for &(sym, span) in required.iter().chain(optional.iter()) {
let name = self.ctx.get_sym(sym).to_string();
let name_idx = self.ctx.intern_string(&name);
let span_id = self.ctx.register_span(span);
self.emit_u32(name_idx);
self.emit_u32(span_id);
}
} else {
self.emit_op(Op::MakeClosure);
self.emit_u32(entry_point);
self.emit_u32(total_slots as u32);
}
}
fn emit_attrset(
&mut self,
stcs: &crate::ir::HashMap<'_, SymId, (RawIrRef<'_>, TextRange)>,
dyns: &[(RawIrRef<'_>, RawIrRef<'_>, TextRange)],
) {
if stcs.is_empty() && dyns.is_empty() {
self.emit_op(Op::MakeEmptyAttrs);
return;
}
if !dyns.is_empty() {
for (&sym, &(val, _)) in stcs.iter() {
let key = self.ctx.get_sym(sym).to_string();
let idx = self.ctx.intern_string(&key);
self.emit_op(Op::PushString);
self.emit_u32(idx);
self.emit_expr(val);
}
for (_, &(_, span)) in stcs.iter() {
let span_id = self.ctx.register_span(span);
let idx = self.ctx.intern_constant(Constant::Int(span_id as i64));
self.emit_op(Op::PushConst);
self.emit_u32(idx);
}
for &(key, val, span) in dyns.iter() {
self.emit_expr(key);
self.emit_expr(val);
let span_id = self.ctx.register_span(span);
let idx = self.ctx.intern_constant(Constant::Int(span_id as i64));
self.emit_op(Op::PushConst);
self.emit_u32(idx);
}
self.emit_op(Op::MakeAttrsDyn);
self.emit_u32(stcs.len() as u32);
self.emit_u32(dyns.len() as u32);
} else {
for (&sym, &(val, _)) in stcs.iter() {
let key = self.ctx.get_sym(sym).to_string();
let idx = self.ctx.intern_string(&key);
self.emit_op(Op::PushString);
self.emit_u32(idx);
self.emit_expr(val);
}
for (_, &(_, span)) in stcs.iter() {
let span_id = self.ctx.register_span(span);
let idx = self.ctx.intern_constant(Constant::Int(span_id as i64));
self.emit_op(Op::PushConst);
self.emit_u32(idx);
}
self.emit_op(Op::MakeAttrs);
self.emit_u32(stcs.len() as u32);
}
}
fn emit_select(
&mut self,
expr: RawIrRef<'_>,
attrpath: &[Attr<RawIrRef<'_>>],
default: Option<RawIrRef<'_>>,
span: TextRange,
) {
self.emit_expr(expr);
for attr in attrpath.iter() {
match attr {
Attr::Str(sym, _) => {
let key = self.ctx.get_sym(*sym).to_string();
let idx = self.ctx.intern_string(&key);
self.emit_op(Op::PushString);
self.emit_u32(idx);
}
Attr::Dynamic(expr, _) => {
self.emit_expr(*expr);
}
}
}
if let Some(default) = default {
self.emit_expr(default);
let span_id = self.ctx.register_span(span);
self.emit_op(Op::SelectDefault);
self.emit_u16(attrpath.len() as u16);
self.emit_u32(span_id);
} else {
let span_id = self.ctx.register_span(span);
self.emit_op(Op::Select);
self.emit_u16(attrpath.len() as u16);
self.emit_u32(span_id);
}
}
fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr<RawIrRef<'_>>]) {
self.emit_expr(lhs);
for attr in rhs.iter() {
match attr {
Attr::Str(sym, _) => {
let key = self.ctx.get_sym(*sym).to_string();
let idx = self.ctx.intern_string(&key);
self.emit_op(Op::PushString);
self.emit_u32(idx);
}
Attr::Dynamic(expr, _) => {
self.emit_expr(*expr);
}
}
}
self.emit_op(Op::HasAttr);
self.emit_u16(rhs.len() as u16);
}
fn emit_with(
&mut self,
namespace: RawIrRef<'_>,
body: RawIrRef<'_>,
thunks: &[(ThunkId, RawIrRef<'_>)],
) {
self.emit_expr(namespace);
self.emit_op(Op::PushWith);
self.emit_scope_thunks(thunks);
self.emit_expr(body);
self.emit_op(Op::PopWith);
}
fn emit_toplevel_inner(&mut self, body: RawIrRef<'_>, thunks: &[(ThunkId, RawIrRef<'_>)]) {
self.emit_scope_thunks(thunks);
self.emit_expr(body);
}
}
+592
View File
@@ -0,0 +1,592 @@
use std::cell::UnsafeCell;
use std::hash::BuildHasher;
use std::path::Path;
use bumpalo::Bump;
use ghost_cell::{GhostCell, GhostToken};
use hashbrown::{DefaultHashBuilder, HashMap, HashSet, HashTable};
use rnix::TextRange;
use string_interner::DefaultStringInterner;
use crate::bytecode::{self, Bytecode, BytecodeContext, Constant};
use crate::disassembler::{Disassembler, DisassemblerContext};
use crate::downgrade::*;
use crate::error::{Error, Result, Source};
use crate::ir::{ArgId, Ir, IrKey, IrRef, RawIrRef, SymId, ThunkId, ir_content_eq};
use crate::store::{DaemonStore, Store, StoreConfig};
use crate::value::{Symbol, Value};
fn parse_error_span(error: &rnix::ParseError) -> Option<rnix::TextRange> {
use rnix::ParseError::*;
match error {
Unexpected(range)
| UnexpectedExtra(range)
| UnexpectedWanted(_, range, _)
| UnexpectedDoubleBind(range)
| DuplicatedArgs(range, _) => Some(*range),
_ => None,
}
}
fn handle_parse_error<'a>(
errors: impl IntoIterator<Item = &'a rnix::ParseError>,
source: Source,
) -> Option<Box<Error>> {
for err in errors {
if let Some(span) = parse_error_span(err) {
return Some(
Error::parse_error(err.to_string())
.with_source(source)
.with_span(span),
);
}
}
None
}
impl Context {
pub fn eval(&mut self, _source: Source) -> Result<Value> {
todo!()
}
pub fn eval_shallow(&mut self, _source: Source) -> Result<Value> {
todo!()
}
pub fn eval_deep(&mut self, _source: Source) -> Result<Value> {
todo!()
}
pub fn eval_repl<'a>(&'a mut self, _source: Source, _scope: &'a HashSet<SymId>) -> Result<Value> {
todo!()
}
pub fn disassemble(&self, bytecode: &Bytecode) -> String {
Disassembler::new(bytecode, self).disassemble()
}
pub fn disassemble_colored(&self, bytecode: &Bytecode) -> String {
Disassembler::new(bytecode, self).disassemble_colored()
}
pub fn add_binding<'a>(
&'a mut self,
name: &str,
expr: &str,
scope: &'a mut HashSet<SymId>,
) -> Result<Value> {
todo!()
}
}
pub struct Context {
symbols: DefaultStringInterner,
global: HashMap<SymId, Ir<'static, RawIrRef<'static>>>,
sources: Vec<Source>,
store: DaemonStore,
spans: UnsafeCell<Vec<(usize, TextRange)>>,
thunk_count: usize,
global_strings: Vec<String>,
global_string_map: HashMap<String, u32>,
global_constants: Vec<Constant>,
global_constant_map: HashMap<Constant, u32>,
synced_strings: usize,
synced_constants: usize,
}
/// Owns the bump allocator and a read-only reference into it.
///
/// # Safety
/// The `ir` field points into `_bump`'s storage. We use `'static` as a sentinel
/// lifetime because the struct owns the backing memory. The `as_ref` method
/// re-binds the lifetime to `&self`, preventing use-after-free.
struct OwnedIr {
_bump: Bump,
ir: RawIrRef<'static>,
}
impl OwnedIr {
fn as_ref(&self) -> RawIrRef<'_> {
self.ir
}
}
impl Context {
pub fn new() -> Result<Self> {
let mut symbols = DefaultStringInterner::new();
let mut global = HashMap::new();
let builtins_sym = symbols.get_or_intern("builtins");
global.insert(builtins_sym, Ir::Builtins);
let free_globals = [
"abort",
"baseNameOf",
"break",
"dirOf",
"derivation",
"derivationStrict",
"fetchGit",
"fetchMercurial",
"fetchTarball",
"fetchTree",
"fromTOML",
"import",
"isNull",
"map",
"placeholder",
"removeAttrs",
"scopedImport",
"throw",
"toString",
];
let consts = [
("true", Ir::Bool(true)),
("false", Ir::Bool(false)),
("null", Ir::Null),
];
for name in free_globals {
let name = symbols.get_or_intern(name);
let value = Ir::Builtin(name);
global.insert(name, value);
}
for (name, value) in consts {
let name = symbols.get_or_intern(name);
global.insert(name, value);
}
let config = StoreConfig::from_env();
let store = DaemonStore::connect(&config.daemon_socket)?;
Ok(Self {
symbols,
global,
sources: Vec::new(),
store,
spans: UnsafeCell::new(Vec::new()),
thunk_count: 0,
global_strings: Vec::new(),
global_string_map: HashMap::new(),
global_constants: Vec::new(),
global_constant_map: HashMap::new(),
synced_strings: 0,
synced_constants: 0,
})
}
fn downgrade_ctx<'ctx, 'id, 'ir>(
&'ctx mut self,
bump: &'ir Bump,
token: GhostToken<'id>,
extra_scope: Option<Scope<'ctx>>,
) -> DowngradeCtx<'ctx, 'id, 'ir> {
let source = self.get_current_source();
DowngradeCtx::new(
bump,
token,
&mut self.symbols,
&self.global,
extra_scope,
&mut self.thunk_count,
source,
)
}
fn get_current_dir(&self) -> &Path {
self.sources
.last()
.as_ref()
.expect("current_source is not set")
.get_dir()
}
fn get_current_source(&self) -> Source {
self.sources
.last()
.expect("current_source is not set")
.clone()
}
fn downgrade<'ctx>(
&'ctx mut self,
source: Source,
extra_scope: Option<Scope<'ctx>>,
) -> Result<OwnedIr> {
tracing::debug!("Parsing Nix expression");
self.sources.push(source.clone());
let root = rnix::Root::parse(&source.src);
handle_parse_error(root.errors(), source).map_or(Ok(()), Err)?;
tracing::debug!("Downgrading Nix expression");
let expr = root
.tree()
.expr()
.ok_or_else(|| Error::parse_error("unexpected EOF".into()))?;
let bump = Bump::new();
GhostToken::new(|token| {
let ir = self
.downgrade_ctx(&bump, token, extra_scope)
.downgrade_toplevel(expr)?;
let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) };
Ok(OwnedIr { _bump: bump, ir })
})
}
pub fn compile_bytecode(&mut self, source: Source) -> Result<Bytecode> {
let root = self.downgrade(source, None)?;
tracing::debug!("Generating bytecode");
let bytecode = bytecode::compile_bytecode(root.as_ref(), self);
tracing::debug!("Compiled bytecode: {:#04X?}", bytecode.code);
Ok(bytecode)
}
pub fn get_store_dir(&self) -> &str {
self.store.get_store_dir()
}
}
impl BytecodeContext for Context {
fn intern_string(&mut self, s: &str) -> u32 {
if let Some(&idx) = self.global_string_map.get(s) {
return idx;
}
let idx = self.global_strings.len() as u32;
self.global_strings.push(s.to_string());
self.global_string_map.insert(s.to_string(), idx);
idx
}
fn intern_constant(&mut self, c: Constant) -> u32 {
if let Some(&idx) = self.global_constant_map.get(&c) {
return idx;
}
let idx = self.global_constants.len() as u32;
self.global_constants.push(c.clone());
self.global_constant_map.insert(c, idx);
idx
}
fn register_span(&self, range: TextRange) -> u32 {
// FIXME: SAFETY
let spans = unsafe { &mut *self.spans.get() };
let id = spans.len();
let source_id = self
.sources
.len()
.checked_sub(1)
.expect("current_source not set");
spans.push((source_id, range));
id as u32
}
fn get_sym(&self, id: SymId) -> &str {
self.symbols.resolve(id).expect("SymId out of bounds")
}
fn get_current_dir(&self) -> &Path {
Context::get_current_dir(self)
}
}
impl DisassemblerContext for Context {
fn lookup_string(&self, id: u32) -> &str {
self.global_strings
.get(id as usize)
.expect("string not found")
}
fn lookup_constant(&self, id: u32) -> &Constant {
self.global_constants
.get(id as usize)
.expect("constant not found")
}
}
enum Scope<'ctx> {
Global(&'ctx HashMap<SymId, Ir<'static, RawIrRef<'static>>>),
Repl(&'ctx HashSet<SymId>),
ScopedImport(HashSet<SymId>),
Let(HashMap<SymId, ThunkId>),
Param(SymId, ArgId),
}
struct ScopeGuard<'a, 'ctx, 'id, 'ir> {
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir>,
}
impl Drop for ScopeGuard<'_, '_, '_, '_> {
fn drop(&mut self) {
self.ctx.scopes.pop();
}
}
impl<'id, 'ir, 'ctx> ScopeGuard<'_, 'ctx, 'id, 'ir> {
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir> {
self.ctx
}
}
struct ThunkScope<'id, 'ir> {
bindings: bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
cache: HashTable<(IrRef<'id, 'ir>, ThunkId)>,
hasher: DefaultHashBuilder,
}
impl<'id, 'ir> ThunkScope<'id, 'ir> {
fn new_in(bump: &'ir Bump) -> Self {
Self {
bindings: bumpalo::collections::Vec::new_in(bump),
cache: HashTable::new(),
hasher: DefaultHashBuilder::default(),
}
}
fn lookup_cache(&self, key: IrRef<'id, 'ir>, token: &GhostToken<'id>) -> Option<ThunkId> {
let hash = self.hasher.hash_one(IrKey(key, token));
self.cache
.find(hash, |&(ir, _)| ir_content_eq(key, ir, token))
.map(|&(_, id)| id)
}
fn add_binding(&mut self, id: ThunkId, ir: IrRef<'id, 'ir>, token: &GhostToken<'id>) {
self.bindings.push((id, ir));
let hash = self.hasher.hash_one(IrKey(ir, token));
self.cache.insert_unique(hash, (ir, id), |&(ir, _)| {
self.hasher.hash_one(IrKey(ir, token))
});
}
fn extend_bindings(&mut self, iter: impl IntoIterator<Item = (ThunkId, IrRef<'id, 'ir>)>) {
self.bindings.extend(iter);
}
}
struct DowngradeCtx<'ctx, 'id, 'ir> {
bump: &'ir Bump,
token: GhostToken<'id>,
symbols: &'ctx mut DefaultStringInterner,
source: Source,
scopes: Vec<Scope<'ctx>>,
with_scope_count: usize,
arg_count: usize,
thunk_count: &'ctx mut usize,
thunk_scopes: Vec<ThunkScope<'id, 'ir>>,
}
fn should_thunk<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>) -> bool {
!matches!(
ir.borrow(token),
Ir::Builtin(_)
| Ir::Builtins
| Ir::Int(_)
| Ir::Float(_)
| Ir::Bool(_)
| Ir::Null
| Ir::Str(_)
| Ir::Thunk(_)
)
}
impl<'ctx, 'id, 'ir> DowngradeCtx<'ctx, 'id, 'ir> {
fn new(
bump: &'ir Bump,
token: GhostToken<'id>,
symbols: &'ctx mut DefaultStringInterner,
global: &'ctx HashMap<SymId, Ir<'static, RawIrRef<'static>>>,
extra_scope: Option<Scope<'ctx>>,
thunk_count: &'ctx mut usize,
source: Source,
) -> Self {
Self {
bump,
token,
symbols,
source,
scopes: std::iter::once(Scope::Global(global))
.chain(extra_scope)
.collect(),
thunk_count,
arg_count: 0,
with_scope_count: 0,
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
}
impl<'ctx: 'ir, 'id, 'ir> DowngradeContext<'id, 'ir> for DowngradeCtx<'ctx, 'id, 'ir> {
fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir> {
IrRef::new(self.bump.alloc(GhostCell::new(expr)))
}
fn new_arg(&mut self) -> ArgId {
self.arg_count += 1;
ArgId(self.arg_count - 1)
}
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir> {
if !should_thunk(ir, &self.token) {
return ir;
}
let cached = self
.thunk_scopes
.last()
.expect("no active cache scope")
.lookup_cache(ir, &self.token);
if let Some(id) = cached {
return IrRef::alloc(self.bump, Ir::Thunk(id));
}
let id = ThunkId(*self.thunk_count);
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
self.thunk_scopes
.last_mut()
.expect("no active cache scope")
.add_binding(id, ir, &self.token);
IrRef::alloc(self.bump, Ir::Thunk(id))
}
fn new_sym(&mut self, sym: String) -> SymId {
self.symbols.get_or_intern(sym)
}
fn get_sym(&self, id: SymId) -> Symbol<'_> {
self.symbols.resolve(id).expect("no symbol found").into()
}
fn lookup(&self, sym: SymId, span: TextRange) -> Result<IrRef<'id, 'ir>> {
for scope in self.scopes.iter().rev() {
match scope {
&Scope::Global(global_scope) => {
if let Some(expr) = global_scope.get(&sym) {
let ir = match expr {
Ir::Builtins => Ir::Builtins,
Ir::Builtin(s) => Ir::Builtin(*s),
Ir::Bool(b) => Ir::Bool(*b),
Ir::Null => Ir::Null,
_ => unreachable!("globals should only contain leaf IR nodes"),
};
return Ok(self.new_expr(ir));
}
}
&Scope::Repl(repl_bindings) => {
if repl_bindings.contains(&sym) {
return Ok(self.new_expr(Ir::ReplBinding(sym)));
}
}
Scope::ScopedImport(scoped_bindings) => {
if scoped_bindings.contains(&sym) {
return Ok(self.new_expr(Ir::ScopedImportBinding(sym)));
}
}
Scope::Let(let_scope) => {
if let Some(&expr) = let_scope.get(&sym) {
return Ok(self.new_expr(Ir::Thunk(expr)));
}
}
&Scope::Param(param_sym, id) => {
if param_sym == sym {
return Ok(self.new_expr(Ir::Arg(id)));
}
}
}
}
if self.with_scope_count > 0 {
Ok(self.new_expr(Ir::WithLookup(sym)))
} else {
Err(Error::downgrade_error(
format!("'{}' not found", self.get_sym(sym)),
self.get_current_source(),
span,
))
}
}
fn get_current_source(&self) -> Source {
self.source.clone()
}
fn with_let_scope<F, R>(&mut self, keys: &[SymId], f: F) -> Result<R>
where
F: FnOnce(&mut Self) -> Result<(bumpalo::collections::Vec<'ir, IrRef<'id, 'ir>>, R)>,
{
let base = *self.thunk_count;
*self.thunk_count = self
.thunk_count
.checked_add(keys.len())
.expect("thunk id overflow");
let iter = keys.iter().enumerate().map(|(offset, &key)| {
(
key,
ThunkId(unsafe { base.checked_add(offset).unwrap_unchecked() }),
)
});
self.scopes.push(Scope::Let(iter.collect()));
let (vals, ret) = {
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())?
};
assert_eq!(keys.len(), vals.len());
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
scope.extend_bindings((base..base + keys.len()).map(ThunkId).zip(vals));
Ok(ret)
}
fn with_param_scope<F, R>(&mut self, param: SymId, arg: ArgId, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.scopes.push(Scope::Param(param, arg));
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())
}
fn with_with_scope<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.with_scope_count += 1;
let ret = f(self);
self.with_scope_count -= 1;
ret
}
fn with_thunk_scope<F, R>(
&mut self,
f: F,
) -> (
R,
bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
)
where
F: FnOnce(&mut Self) -> R,
{
self.thunk_scopes.push(ThunkScope::new_in(self.bump));
let ret = f(self);
(
ret,
self.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings,
)
}
fn bump(&self) -> &'ir bumpalo::Bump {
self.bump
}
}
impl<'id, 'ir, 'ctx: 'ir> DowngradeCtx<'ctx, 'id, 'ir> {
fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result<RawIrRef<'ir>> {
let body = root.downgrade(&mut self)?;
let thunks = self
.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings;
let ir = IrRef::alloc(self.bump, Ir::TopLevel { body, thunks });
Ok(ir.freeze(self.token))
}
}
+142
View File
@@ -0,0 +1,142 @@
use std::collections::{BTreeMap, BTreeSet};
pub struct OutputInfo {
pub path: String,
pub hash_algo: String,
pub hash: String,
}
pub struct DerivationData {
pub name: String,
pub outputs: BTreeMap<String, OutputInfo>,
pub input_drvs: BTreeMap<String, BTreeSet<String>>,
pub input_srcs: BTreeSet<String>,
pub platform: String,
pub builder: String,
pub args: Vec<String>,
pub env: BTreeMap<String, String>,
}
fn escape_string(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 2);
result.push('"');
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
_ => result.push(c),
}
}
result.push('"');
result
}
fn quote_string(s: &str) -> String {
format!("\"{}\"", s)
}
impl DerivationData {
pub fn generate_aterm(&self) -> String {
let mut output_entries = Vec::new();
for (name, info) in &self.outputs {
output_entries.push(format!(
"({},{},{},{})",
quote_string(name),
quote_string(&info.path),
quote_string(&info.hash_algo),
quote_string(&info.hash),
));
}
let outputs = output_entries.join(",");
let mut input_drv_entries = Vec::new();
for (drv_path, output_names) in &self.input_drvs {
let sorted_outs: Vec<String> = output_names.iter().map(|s| quote_string(s)).collect();
let out_list = format!("[{}]", sorted_outs.join(","));
input_drv_entries.push(format!("({},{})", quote_string(drv_path), out_list));
}
let input_drvs = input_drv_entries.join(",");
let input_srcs: Vec<String> = self.input_srcs.iter().map(|s| quote_string(s)).collect();
let input_srcs = input_srcs.join(",");
let args: Vec<String> = self.args.iter().map(|s| escape_string(s)).collect();
let args = args.join(",");
let mut env_entries: Vec<String> = Vec::new();
for (k, v) in &self.env {
env_entries.push(format!("({},{})", escape_string(k), escape_string(v)));
}
format!(
"Derive([{}],[{}],[{}],{},{},[{}],[{}])",
outputs,
input_drvs,
input_srcs,
quote_string(&self.platform),
escape_string(&self.builder),
args,
env_entries.join(","),
)
}
pub fn generate_aterm_modulo(&self, input_drv_hashes: &BTreeMap<String, String>) -> String {
let mut output_entries = Vec::new();
for (name, info) in &self.outputs {
output_entries.push(format!(
"({},{},{},{})",
quote_string(name),
quote_string(&info.path),
quote_string(&info.hash_algo),
quote_string(&info.hash),
));
}
let outputs = output_entries.join(",");
let mut input_drv_entries = Vec::new();
for (drv_hash, outputs_csv) in input_drv_hashes {
let mut sorted_outs: Vec<&str> = outputs_csv.split(',').collect();
sorted_outs.sort();
let out_list: Vec<String> = sorted_outs.iter().map(|s| quote_string(s)).collect();
let out_list = format!("[{}]", out_list.join(","));
input_drv_entries.push(format!("({},{})", quote_string(drv_hash), out_list));
}
let input_drvs = input_drv_entries.join(",");
let input_srcs: Vec<String> = self.input_srcs.iter().map(|s| quote_string(s)).collect();
let input_srcs = input_srcs.join(",");
let args: Vec<String> = self.args.iter().map(|s| escape_string(s)).collect();
let args = args.join(",");
let mut env_entries: Vec<String> = Vec::new();
for (k, v) in &self.env {
env_entries.push(format!("({},{})", escape_string(k), escape_string(v)));
}
format!(
"Derive([{}],[{}],[{}],{},{},[{}],[{}])",
outputs,
input_drvs,
input_srcs,
quote_string(&self.platform),
escape_string(&self.builder),
args,
env_entries.join(","),
)
}
pub fn collect_references(&self) -> Vec<String> {
let mut refs = BTreeSet::new();
for src in &self.input_srcs {
refs.insert(src.clone());
}
for drv_path in self.input_drvs.keys() {
refs.insert(drv_path.clone());
}
refs.into_iter().collect()
}
}
+354
View File
@@ -0,0 +1,354 @@
use std::fmt::Write;
use colored::Colorize;
use num_enum::TryFromPrimitive;
use crate::bytecode::{Bytecode, Constant, Op};
pub(crate) trait DisassemblerContext {
fn lookup_string(&self, id: u32) -> &str;
fn lookup_constant(&self, id: u32) -> &Constant;
}
pub(crate) struct Disassembler<'a, Ctx> {
code: &'a [u8],
ctx: &'a Ctx,
pos: usize,
}
impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
pub fn new(bytecode: &'a Bytecode, ctx: &'a Ctx) -> Self {
Self {
code: &bytecode.code,
ctx,
pos: 0,
}
}
fn read_u8(&mut self) -> u8 {
let b = self.code[self.pos];
self.pos += 1;
b
}
fn read_u16(&mut self) -> u16 {
let bytes = self.code[self.pos..self.pos + 2]
.try_into()
.expect("no enough bytes");
self.pos += 2;
u16::from_le_bytes(bytes)
}
fn read_u32(&mut self) -> u32 {
let bytes = self.code[self.pos..self.pos + 4]
.try_into()
.expect("no enough bytes");
self.pos += 4;
u32::from_le_bytes(bytes)
}
fn read_i32(&mut self) -> i32 {
let bytes = self.code[self.pos..self.pos + 4]
.try_into()
.expect("no enough bytes");
self.pos += 4;
i32::from_le_bytes(bytes)
}
pub fn disassemble(&mut self) -> String {
self.disassemble_impl(false)
}
pub fn disassemble_colored(&mut self) -> String {
self.disassemble_impl(true)
}
fn disassemble_impl(&mut self, color: bool) -> String {
let mut out = String::new();
if color {
let _ = writeln!(out, "{}", "=== Bytecode Disassembly ===".bold().white());
let _ = writeln!(
out,
"{} {}",
"Length:".white(),
format!("{} bytes", self.code.len()).cyan()
);
} else {
let _ = writeln!(out, "=== Bytecode Disassembly ===");
let _ = writeln!(out, "Length: {} bytes", self.code.len());
}
while self.pos < self.code.len() {
let start_pos = self.pos;
let op_byte = self.read_u8();
let (mnemonic, args) = self.decode_instruction(op_byte, start_pos);
let bytes_slice = &self.code[start_pos + 1..self.pos];
for (i, chunk) in bytes_slice.chunks(4).enumerate() {
let bytes_str = {
let mut temp = String::new();
if i == 0 {
let _ = write!(&mut temp, "{:02x}", self.code[start_pos]);
} else {
let _ = write!(&mut temp, " ");
}
for b in chunk.iter() {
let _ = write!(&mut temp, " {:02x}", b);
}
temp
};
if i == 0 {
if color {
let sep = if args.is_empty() { "" } else { " " };
let _ = writeln!(
out,
"{} {:<14} | {}{}{}",
format!("{:04x}", start_pos).dimmed(),
bytes_str.green(),
mnemonic.yellow().bold(),
sep,
args.cyan()
);
} else {
let op_str = if args.is_empty() {
mnemonic.to_string()
} else {
format!("{} {}", mnemonic, args)
};
let _ = writeln!(out, "{:04x} {:<14} | {}", start_pos, bytes_str, op_str);
}
} else {
let extra_width = start_pos.ilog2() >> 4;
if color {
let _ = write!(out, " ");
for _ in 0..extra_width {
let _ = write!(out, " ");
}
let _ = writeln!(out, " {:<14} |", bytes_str.green());
} else {
let _ = write!(out, " ");
for _ in 0..extra_width {
let _ = write!(out, " ");
}
let _ = writeln!(out, " {:<14} |", bytes_str);
}
}
}
}
out
}
fn decode_instruction(&mut self, op_byte: u8, current_pc: usize) -> (&'static str, String) {
let op = Op::try_from_primitive(op_byte).expect("invalid op code");
match op {
Op::PushConst => {
let idx = self.read_u32();
let val = self.ctx.lookup_constant(idx);
let val_str = match val {
Constant::Int(i) => format!("Int({})", i),
Constant::Float(f) => format!("Float(bits: {})", f),
};
("PushConst", format!("@{} ({})", idx, val_str))
}
Op::PushString => {
let idx = self.read_u32();
let s = self.ctx.lookup_string(idx);
let len = s.len();
let mut s_fmt = format!("{:?}", s);
if s_fmt.len() > 60 {
s_fmt.truncate(57);
#[allow(clippy::unwrap_used)]
write!(s_fmt, "...\" (total {len} bytes)").unwrap();
}
("PushString", format!("@{} {}", idx, s_fmt))
}
Op::PushNull => ("PushNull", String::new()),
Op::PushTrue => ("PushTrue", String::new()),
Op::PushFalse => ("PushFalse", String::new()),
Op::LoadLocal => {
let idx = self.read_u32();
("LoadLocal", format!("[{}]", idx))
}
Op::LoadOuter => {
let depth = self.read_u8();
let idx = self.read_u32();
("LoadOuter", format!("depth={} [{}]", depth, idx))
}
Op::StoreLocal => {
let idx = self.read_u32();
("StoreLocal", format!("[{}]", idx))
}
Op::AllocLocals => {
let count = self.read_u32();
("AllocLocals", format!("count={}", count))
}
Op::MakeThunk => {
let offset = self.read_u32();
let label_idx = self.read_u32();
let label = self.ctx.lookup_string(label_idx);
("MakeThunk", format!("-> {:04x} label={}", offset, label))
}
Op::MakeClosure => {
let offset = self.read_u32();
let slots = self.read_u32();
("MakeClosure", format!("-> {:04x} slots={}", offset, slots))
}
Op::MakePatternClosure => {
let offset = self.read_u32();
let slots = self.read_u32();
let req_count = self.read_u16();
let opt_count = self.read_u16();
let ellipsis = self.read_u8() != 0;
let mut arg_str = format!(
"-> {:04x} slots={} req={} opt={} ...={})",
offset, slots, req_count, opt_count, ellipsis
);
arg_str.push_str(" Args=[");
for _ in 0..req_count {
let idx = self.read_u32();
arg_str.push_str(&format!("Req({}) ", self.ctx.lookup_string(idx)));
}
for _ in 0..opt_count {
let idx = self.read_u32();
arg_str.push_str(&format!("Opt({}) ", self.ctx.lookup_string(idx)));
}
let total_args = req_count + opt_count;
for _ in 0..total_args {
let _name_idx = self.read_u32();
let _span_id = self.read_u32();
}
arg_str.push(']');
("MakePatternClosure", arg_str)
}
Op::Call => {
let span_id = self.read_u32();
("Call", format!("span={}", span_id))
}
Op::CallNoSpan => ("CallNoSpan", String::new()),
Op::MakeAttrs => {
let count = self.read_u32();
("MakeAttrs", format!("size={}", count))
}
Op::MakeAttrsDyn => {
let static_count = self.read_u32();
let dyn_count = self.read_u32();
(
"MakeAttrsDyn",
format!("static={} dyn={}", static_count, dyn_count),
)
}
Op::MakeEmptyAttrs => ("MakeEmptyAttrs", String::new()),
Op::Select => {
let path_len = self.read_u16();
let span_id = self.read_u32();
("Select", format!("path_len={} span={}", path_len, span_id))
}
Op::SelectDefault => {
let path_len = self.read_u16();
let span_id = self.read_u32();
(
"SelectDefault",
format!("path_len={} span={}", path_len, span_id),
)
}
Op::HasAttr => {
let path_len = self.read_u16();
("HasAttr", format!("path_len={}", path_len))
}
Op::MakeList => {
let count = self.read_u32();
("MakeList", format!("size={}", count))
}
Op::OpAdd => ("OpAdd", String::new()),
Op::OpSub => ("OpSub", String::new()),
Op::OpMul => ("OpMul", String::new()),
Op::OpDiv => ("OpDiv", String::new()),
Op::OpEq => ("OpEq", String::new()),
Op::OpNeq => ("OpNeq", String::new()),
Op::OpLt => ("OpLt", String::new()),
Op::OpGt => ("OpGt", String::new()),
Op::OpLeq => ("OpLeq", String::new()),
Op::OpGeq => ("OpGeq", String::new()),
Op::OpConcat => ("OpConcat", String::new()),
Op::OpUpdate => ("OpUpdate", String::new()),
Op::OpNeg => ("OpNeg", String::new()),
Op::OpNot => ("OpNot", String::new()),
Op::ForceBool => ("ForceBool", String::new()),
Op::JumpIfFalse => {
let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
(
"JumpIfFalse",
format!("-> {:04x} offset={}", target, offset),
)
}
Op::JumpIfTrue => {
let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
("JumpIfTrue", format!("-> {:04x} offset={}", target, offset))
}
Op::Jump => {
let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
("Jump", format!("-> {:04x} offset={}", target, offset))
}
Op::ConcatStrings => {
let count = self.read_u16();
let force = self.read_u8();
("ConcatStrings", format!("count={} force={}", count, force))
}
Op::ResolvePath => ("ResolvePath", String::new()),
Op::Assert => {
let raw_idx = self.read_u32();
let span_id = self.read_u32();
("Assert", format!("text_id={} span={}", raw_idx, span_id))
}
Op::PushWith => ("PushWith", String::new()),
Op::PopWith => ("PopWith", String::new()),
Op::WithLookup => {
let idx = self.read_u32();
let name = self.ctx.lookup_string(idx);
("WithLookup", format!("{:?}", name))
}
Op::LoadBuiltins => ("LoadBuiltins", String::new()),
Op::LoadBuiltin => {
let idx = self.read_u32();
let name = self.ctx.lookup_string(idx);
("LoadBuiltin", format!("{:?}", name))
}
Op::MkPos => {
let span_id = self.read_u32();
("MkPos", format!("id={}", span_id))
}
Op::LoadReplBinding => {
let idx = self.read_u32();
let name = self.ctx.lookup_string(idx);
("LoadReplBinding", format!("{:?}", name))
}
Op::LoadScopedBinding => {
let idx = self.read_u32();
let name = self.ctx.lookup_string(idx);
("LoadScopedBinding", format!("{:?}", name))
}
Op::Return => ("Return", String::new()),
}
}
}
+1249
View File
File diff suppressed because it is too large Load Diff
+226
View File
@@ -0,0 +1,226 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;
pub type Result<T> = core::result::Result<T, Box<Error>>;
#[derive(Clone, Debug)]
pub enum SourceType {
/// dir
Eval(Arc<PathBuf>),
/// dir
Repl(Arc<PathBuf>),
/// file
File(Arc<PathBuf>),
/// virtual (name, no path)
Virtual(Arc<str>),
}
#[derive(Clone, Debug)]
pub struct Source {
pub ty: SourceType,
pub src: Arc<str>,
}
impl TryFrom<&str> for Source {
type Error = Box<Error>;
fn try_from(value: &str) -> Result<Self> {
Source::new_eval(value.into())
}
}
impl From<Source> for NamedSource<Arc<str>> {
fn from(value: Source) -> Self {
let name = value.get_name();
NamedSource::new(name, value.src.clone())
}
}
impl Source {
pub fn new_file(path: PathBuf) -> std::io::Result<Self> {
Ok(Source {
src: std::fs::read_to_string(&path)?.into(),
ty: crate::error::SourceType::File(Arc::new(path)),
})
}
pub fn new_eval(src: String) -> Result<Self> {
Ok(Self {
ty: std::env::current_dir()
.map_err(|err| Error::internal(format!("Failed to get current working dir: {err}")))
.map(Arc::new)
.map(SourceType::Eval)?,
src: src.into(),
})
}
pub fn new_repl(src: String) -> Result<Self> {
Ok(Self {
ty: std::env::current_dir()
.map_err(|err| Error::internal(format!("Failed to get current working dir: {err}")))
.map(Arc::new)
.map(SourceType::Repl)?,
src: src.into(),
})
}
pub fn new_virtual(name: Arc<str>, src: String) -> Self {
Self {
ty: SourceType::Virtual(name),
src: src.into(),
}
}
pub fn get_dir(&self) -> &Path {
use SourceType::*;
match &self.ty {
Eval(dir) | Repl(dir) => dir.as_ref(),
File(file) => file
.as_path()
.parent()
.expect("source file must have a parent dir"),
Virtual(_) => Path::new("/"),
}
}
pub fn get_name(&self) -> String {
match &self.ty {
SourceType::Eval(_) => "«eval»".into(),
SourceType::Repl(_) => "«repl»".into(),
SourceType::File(path) => path.as_os_str().to_string_lossy().to_string(),
SourceType::Virtual(name) => name.to_string(),
}
}
}
#[derive(Error, Debug, Diagnostic)]
pub enum Error {
#[error("Parse error: {message}")]
#[diagnostic(code(nix::parse))]
ParseError {
#[source_code]
src: Option<NamedSource<Arc<str>>>,
#[label("error occurred here")]
span: Option<SourceSpan>,
message: String,
},
#[error("Downgrade error: {message}")]
#[diagnostic(code(nix::downgrade))]
DowngradeError {
#[source_code]
src: Option<NamedSource<Arc<str>>>,
#[label("{message}")]
span: Option<SourceSpan>,
message: String,
},
#[error("Evaluation error: {message}")]
#[diagnostic(code(nix::eval))]
EvalError {
#[source_code]
src: Option<NamedSource<Arc<str>>>,
#[label("error occurred here")]
span: Option<SourceSpan>,
message: String,
#[help]
js_backtrace: Option<String>,
#[related]
stack_trace: Vec<StackFrame>,
},
#[error("Internal error: {message}")]
#[diagnostic(code(nix::internal))]
InternalError { message: String },
#[error("{message}")]
#[diagnostic(code(nix::catchable))]
Catchable { message: String },
#[error("Unknown error")]
#[diagnostic(code(nix::unknown))]
Unknown,
}
impl Error {
pub fn parse_error(msg: String) -> Box<Self> {
Error::ParseError {
src: None,
span: None,
message: msg,
}
.into()
}
pub fn downgrade_error(msg: String, src: Source, span: rnix::TextRange) -> Box<Self> {
Error::DowngradeError {
src: Some(src.into()),
span: Some(text_range_to_source_span(span)),
message: msg,
}
.into()
}
pub fn eval_error(msg: String, backtrace: Option<String>) -> Box<Self> {
Error::EvalError {
src: None,
span: None,
message: msg,
js_backtrace: backtrace,
stack_trace: Vec::new(),
}
.into()
}
pub fn internal(msg: String) -> Box<Self> {
Error::InternalError { message: msg }.into()
}
pub fn catchable(msg: String) -> Box<Self> {
Error::Catchable { message: msg }.into()
}
pub fn with_span(mut self: Box<Self>, span: rnix::TextRange) -> Box<Self> {
use Error::*;
let source_span = Some(text_range_to_source_span(span));
let (ParseError { span, .. } | DowngradeError { span, .. } | EvalError { span, .. }) =
self.as_mut()
else {
return self;
};
*span = source_span;
self
}
pub fn with_source(mut self: Box<Self>, source: Source) -> Box<Self> {
use Error::*;
let new_src = Some(source.into());
let (ParseError { src, .. } | DowngradeError { src, .. } | EvalError { src, .. }) =
self.as_mut()
else {
return self;
};
*src = new_src;
self
}
}
pub fn text_range_to_source_span(range: rnix::TextRange) -> SourceSpan {
let start = usize::from(range.start());
let len = usize::from(range.end()) - start;
SourceSpan::new(start.into(), len)
}
/// Stack frame types from Nix evaluation
#[derive(Debug, Clone, Error, Diagnostic)]
#[error("{message}")]
pub struct StackFrame {
#[label]
pub span: SourceSpan,
#[help]
pub message: String,
#[source_code]
pub src: NamedSource<Arc<str>>,
}
+305
View File
@@ -0,0 +1,305 @@
use deno_core::OpState;
use deno_core::ToV8;
use deno_core::op2;
use nix_compat::nixhash::HashAlgo;
use nix_compat::nixhash::NixHash;
use tracing::{debug, info, warn};
use crate::store::Store as _;
mod archive;
pub(crate) mod cache;
mod download;
mod git;
mod metadata_cache;
pub use cache::FetcherCache;
pub use download::Downloader;
pub use metadata_cache::MetadataCache;
use crate::nar;
#[derive(ToV8)]
pub struct FetchUrlResult {
pub store_path: String,
pub hash: String,
}
#[derive(ToV8)]
pub struct FetchTarballResult {
pub store_path: String,
pub nar_hash: String,
}
#[derive(ToV8)]
pub struct FetchGitResult {
pub out_path: String,
pub rev: String,
pub short_rev: String,
pub rev_count: u64,
pub last_modified: u64,
pub last_modified_date: String,
pub submodules: bool,
pub nar_hash: Option<String>,
}
#[op2]
pub fn op_fetch_url<Ctx: RuntimeContext>(
state: &mut OpState,
#[string] url: String,
#[string] expected_hash: Option<String>,
#[string] name: Option<String>,
executable: bool,
) -> Result<FetchUrlResult, NixRuntimeError> {
let _span = tracing::info_span!("op_fetch_url", url = %url).entered();
info!("fetchurl started");
let file_name =
name.unwrap_or_else(|| url.rsplit('/').next().unwrap_or("download").to_string());
let metadata_cache =
MetadataCache::new(3600).map_err(|e| NixRuntimeError::from(e.to_string()))?;
let input = serde_json::json!({
"type": "file",
"url": url,
"name": file_name,
"executable": executable,
});
if let Some(cached_entry) = metadata_cache
.lookup(&input)
.map_err(|e| NixRuntimeError::from(e.to_string()))?
{
let cached_hash = cached_entry
.info
.get("hash")
.and_then(|v| v.as_str())
.unwrap_or("");
if let Some(ref expected) = expected_hash {
let normalized_expected = normalize_hash(expected);
if cached_hash != normalized_expected {
warn!("Cached hash mismatch, re-fetching");
} else {
info!("Cache hit");
return Ok(FetchUrlResult {
store_path: cached_entry.store_path.clone(),
hash: cached_hash.to_string(),
});
}
} else {
info!("Cache hit (no hash check)");
return Ok(FetchUrlResult {
store_path: cached_entry.store_path.clone(),
hash: cached_hash.to_string(),
});
}
}
info!("Cache miss, downloading");
let downloader = Downloader::new();
let data = downloader
.download(&url)
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
info!(bytes = data.len(), "Download complete");
let hash = crate::nix_utils::sha256_hex(&data);
if let Some(ref expected) = expected_hash {
let normalized_expected = normalize_hash(expected);
if hash != normalized_expected {
return Err(NixRuntimeError::from(format!(
"hash mismatch for '{}': expected {}, got {}",
url, normalized_expected, hash
)));
}
}
let ctx: &Ctx = state.get_ctx();
let store = ctx.get_store();
let store_path = store
.add_to_store(&file_name, &data, false, vec![])
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
info!(store_path = %store_path, "Added to store");
#[cfg(unix)]
if executable {
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = std::fs::metadata(&store_path) {
let mut perms = metadata.permissions();
perms.set_mode(0o755);
let _ = std::fs::set_permissions(&store_path, perms);
}
}
let info = serde_json::json!({
"hash": hash,
"url": url,
});
metadata_cache
.add(&input, &info, &store_path, true)
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
Ok(FetchUrlResult { store_path, hash })
}
#[op2]
pub fn op_fetch_tarball<Ctx: RuntimeContext>(
state: &mut OpState,
#[string] url: String,
#[string] name: Option<String>,
#[string] sha256: Option<String>,
) -> Result<FetchTarballResult, NixRuntimeError> {
let _span = tracing::info_span!("op_fetch_tarball", url = %url).entered();
info!("fetchTarball started");
let dir_name = name.unwrap_or_else(|| "source".to_string());
let metadata_cache =
MetadataCache::new(3600).map_err(|e| NixRuntimeError::from(e.to_string()))?;
let input = serde_json::json!({
"type": "tarball",
"url": url,
"name": dir_name,
});
let expected_sha256 = sha256
.map(
|ref sha256| match NixHash::from_str(sha256, Some(HashAlgo::Sha256)) {
Ok(NixHash::Sha256(digest)) => Ok(digest),
_ => Err(format!("fetchTarball: invalid sha256 '{sha256}'")),
},
)
.transpose()?;
let expected_hex = expected_sha256.map(hex::encode);
if let Some(cached_entry) = metadata_cache
.lookup(&input)
.map_err(|e| NixRuntimeError::from(e.to_string()))?
{
let cached_nar_hash = cached_entry
.info
.get("nar_hash")
.and_then(|v| v.as_str())
.unwrap_or("");
if let Some(ref hex) = expected_hex {
if cached_nar_hash == hex {
info!("Cache hit");
return Ok(FetchTarballResult {
store_path: cached_entry.store_path.clone(),
nar_hash: cached_nar_hash.to_string(),
});
}
} else if !cached_entry.is_expired(3600) {
info!("Cache hit (no hash check)");
return Ok(FetchTarballResult {
store_path: cached_entry.store_path.clone(),
nar_hash: cached_nar_hash.to_string(),
});
}
}
info!("Cache miss, downloading tarball");
let downloader = Downloader::new();
let data = downloader
.download(&url)
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
info!(bytes = data.len(), "Download complete");
info!("Extracting tarball");
let (extracted_path, _temp_dir) = archive::extract_tarball_to_temp(&data)
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
info!("Computing NAR hash");
let nar_hash =
nar::compute_nar_hash(&extracted_path).map_err(|e| NixRuntimeError::from(e.to_string()))?;
let nar_hash_hex = hex::encode(nar_hash);
debug!(
nar_hash = %nar_hash_hex,
"Hash computation complete"
);
if let Some(ref expected) = expected_hex
&& nar_hash_hex != *expected
{
return Err(NixRuntimeError::from(format!(
"Tarball hash mismatch for '{}': expected {}, got {}",
url, expected, nar_hash_hex
)));
}
info!("Adding to store");
let ctx: &Ctx = state.get_ctx();
let store = ctx.get_store();
let store_path = store
.add_to_store_from_path(&dir_name, &extracted_path, vec![])
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
info!(store_path = %store_path, "Added to store");
let info = serde_json::json!({
"nar_hash": nar_hash_hex,
"url": url,
});
let immutable = expected_sha256.is_some();
metadata_cache
.add(&input, &info, &store_path, immutable)
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
Ok(FetchTarballResult {
store_path,
nar_hash: nar_hash_hex,
})
}
#[op2]
pub fn op_fetch_git<Ctx: RuntimeContext>(
state: &mut OpState,
#[string] url: String,
#[string] git_ref: Option<String>,
#[string] rev: Option<String>,
shallow: bool,
submodules: bool,
all_refs: bool,
#[string] name: Option<String>,
) -> Result<FetchGitResult, NixRuntimeError> {
let _span = tracing::info_span!("op_fetch_git", url = %url).entered();
info!("fetchGit started");
let cache = FetcherCache::new().map_err(|e| NixRuntimeError::from(e.to_string()))?;
let dir_name = name.unwrap_or_else(|| "source".to_string());
let ctx: &Ctx = state.get_ctx();
let store = ctx.get_store();
git::fetch_git(
&cache,
store,
&url,
git_ref.as_deref(),
rev.as_deref(),
shallow,
submodules,
all_refs,
&dir_name,
)
.map_err(|e| NixRuntimeError::from(e.to_string()))
}
fn normalize_hash(hash: &str) -> String {
use base64::prelude::*;
if hash.starts_with("sha256-")
&& let Some(b64) = hash.strip_prefix("sha256-")
&& let Ok(bytes) = BASE64_STANDARD.decode(b64)
{
return hex::encode(bytes);
}
hash.to_string()
}
+173
View File
@@ -0,0 +1,173 @@
use std::fs;
use std::io::Cursor;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use flate2::read::GzDecoder;
#[derive(Debug, Clone, Copy)]
pub enum ArchiveFormat {
TarGz,
TarXz,
TarBz2,
Tar,
}
impl ArchiveFormat {
pub fn detect(url: &str, data: &[u8]) -> Self {
if url.ends_with(".tar.gz") || url.ends_with(".tgz") {
return ArchiveFormat::TarGz;
}
if url.ends_with(".tar.xz") || url.ends_with(".txz") {
return ArchiveFormat::TarXz;
}
if url.ends_with(".tar.bz2") || url.ends_with(".tbz2") {
return ArchiveFormat::TarBz2;
}
if url.ends_with(".tar") {
return ArchiveFormat::Tar;
}
if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
return ArchiveFormat::TarGz;
}
if data.len() >= 6 && &data[0..6] == b"\xfd7zXZ\x00" {
return ArchiveFormat::TarXz;
}
if data.len() >= 3 && &data[0..3] == b"BZh" {
return ArchiveFormat::TarBz2;
}
ArchiveFormat::TarGz
}
}
pub fn extract_tarball(data: &[u8], dest: &Path) -> Result<PathBuf, ArchiveError> {
let format = ArchiveFormat::detect("", data);
let temp_dir = dest.join("_extract_temp");
fs::create_dir_all(&temp_dir)?;
match format {
ArchiveFormat::TarGz => extract_tar_gz(data, &temp_dir)?,
ArchiveFormat::TarXz => extract_tar_xz(data, &temp_dir)?,
ArchiveFormat::TarBz2 => extract_tar_bz2(data, &temp_dir)?,
ArchiveFormat::Tar => extract_tar(data, &temp_dir)?,
}
strip_single_toplevel(&temp_dir, dest)
}
fn extract_tar_gz(data: &[u8], dest: &Path) -> Result<(), ArchiveError> {
let decoder = GzDecoder::new(Cursor::new(data));
let mut archive = tar::Archive::new(decoder);
archive.unpack(dest)?;
Ok(())
}
fn extract_tar_xz(data: &[u8], dest: &Path) -> Result<(), ArchiveError> {
let decoder = xz2::read::XzDecoder::new(Cursor::new(data));
let mut archive = tar::Archive::new(decoder);
archive.unpack(dest)?;
Ok(())
}
fn extract_tar_bz2(data: &[u8], dest: &Path) -> Result<(), ArchiveError> {
let decoder = bzip2::read::BzDecoder::new(Cursor::new(data));
let mut archive = tar::Archive::new(decoder);
archive.unpack(dest)?;
Ok(())
}
fn extract_tar(data: &[u8], dest: &Path) -> Result<(), ArchiveError> {
let mut archive = tar::Archive::new(Cursor::new(data));
archive.unpack(dest)?;
Ok(())
}
fn strip_single_toplevel(temp_dir: &Path, dest: &Path) -> Result<PathBuf, ArchiveError> {
let entries: Vec<_> = fs::read_dir(temp_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.file_name().as_os_str().as_bytes()[0] != b'.')
.collect();
let source_dir = if entries.len() == 1 && entries[0].file_type()?.is_dir() {
entries[0].path()
} else {
temp_dir.to_path_buf()
};
let final_dest = dest.join("content");
if final_dest.exists() {
fs::remove_dir_all(&final_dest)?;
}
if source_dir == *temp_dir {
fs::rename(temp_dir, &final_dest)?;
} else {
copy_dir_recursive(&source_dir, &final_dest)?;
fs::remove_dir_all(temp_dir)?;
}
Ok(final_dest)
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), std::io::Error> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let path = entry.path();
let dest_path = dst.join(entry.file_name());
let metadata = fs::symlink_metadata(&path)?;
if metadata.is_symlink() {
let target = fs::read_link(&path)?;
#[cfg(unix)]
{
std::os::unix::fs::symlink(&target, &dest_path)?;
}
#[cfg(windows)]
{
if target.is_dir() {
std::os::windows::fs::symlink_dir(&target, &dest_path)?;
} else {
std::os::windows::fs::symlink_file(&target, &dest_path)?;
}
}
} else if metadata.is_dir() {
copy_dir_recursive(&path, &dest_path)?;
} else {
fs::copy(&path, &dest_path)?;
}
}
Ok(())
}
pub fn extract_tarball_to_temp(data: &[u8]) -> Result<(PathBuf, tempfile::TempDir), ArchiveError> {
let temp_dir = tempfile::tempdir()?;
let extracted_path = extract_tarball(data, temp_dir.path())?;
Ok((extracted_path, temp_dir))
}
#[derive(Debug)]
pub enum ArchiveError {
IoError(std::io::Error),
}
impl std::fmt::Display for ArchiveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArchiveError::IoError(e) => write!(f, "I/O error: {}", e),
}
}
}
impl std::error::Error for ArchiveError {}
impl From<std::io::Error> for ArchiveError {
fn from(e: std::io::Error) -> Self {
ArchiveError::IoError(e)
}
}
+29
View File
@@ -0,0 +1,29 @@
use std::fs;
use std::path::PathBuf;
#[derive(Debug)]
pub struct FetcherCache {
base_dir: PathBuf,
}
impl FetcherCache {
pub fn new() -> Result<Self, std::io::Error> {
let base_dir = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("fix")
.join("fetchers");
fs::create_dir_all(&base_dir)?;
Ok(Self { base_dir })
}
fn git_cache_dir(&self) -> PathBuf {
self.base_dir.join("git")
}
pub fn get_git_bare(&self, url: &str) -> PathBuf {
let key = crate::nix_utils::sha256_hex(url.as_bytes());
self.git_cache_dir().join(key)
}
}
+64
View File
@@ -0,0 +1,64 @@
use std::time::Duration;
use reqwest::blocking::Client;
pub struct Downloader {
client: Client,
}
impl Downloader {
pub fn new() -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(300))
.user_agent("nix-js/0.1")
.build()
.expect("Failed to create HTTP client");
Self { client }
}
pub fn download(&self, url: &str) -> Result<Vec<u8>, DownloadError> {
let response = self
.client
.get(url)
.send()
.map_err(|e| DownloadError::NetworkError(e.to_string()))?;
if !response.status().is_success() {
return Err(DownloadError::HttpError {
url: url.to_string(),
status: response.status().as_u16(),
});
}
response
.bytes()
.map(|b| b.to_vec())
.map_err(|e| DownloadError::NetworkError(e.to_string()))
}
}
impl Default for Downloader {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub enum DownloadError {
NetworkError(String),
HttpError { url: String, status: u16 },
}
impl std::fmt::Display for DownloadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DownloadError::NetworkError(msg) => write!(f, "Network error: {}", msg),
DownloadError::HttpError { url, status } => {
write!(f, "HTTP error {} for URL: {}", status, url)
}
}
}
}
impl std::error::Error for DownloadError {}
+315
View File
@@ -0,0 +1,315 @@
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use super::FetchGitResult;
use super::cache::FetcherCache;
use crate::store::Store;
#[allow(clippy::too_many_arguments)]
pub fn fetch_git(
cache: &FetcherCache,
store: &dyn Store,
url: &str,
git_ref: Option<&str>,
rev: Option<&str>,
_shallow: bool,
submodules: bool,
all_refs: bool,
name: &str,
) -> Result<FetchGitResult, GitError> {
let bare_repo = cache.get_git_bare(url);
if !bare_repo.exists() {
clone_bare(url, &bare_repo)?;
} else {
fetch_repo(&bare_repo, all_refs)?;
}
let target_rev = resolve_rev(&bare_repo, git_ref, rev)?;
let temp_dir = tempfile::tempdir()?;
let checkout_dir = checkout_rev_to_temp(&bare_repo, &target_rev, submodules, temp_dir.path())?;
let nar_hash = hex::encode(
crate::nar::compute_nar_hash(&checkout_dir)
.map_err(|e| GitError::NarHashError(e.to_string()))?,
);
let store_path = store
.add_to_store_from_path(name, &checkout_dir, vec![])
.map_err(|e| GitError::StoreError(e.to_string()))?;
let rev_count = get_rev_count(&bare_repo, &target_rev)?;
let last_modified = get_last_modified(&bare_repo, &target_rev)?;
let last_modified_date = format_timestamp(last_modified);
let short_rev = if target_rev.len() >= 7 {
target_rev[..7].to_string()
} else {
target_rev.clone()
};
Ok(FetchGitResult {
out_path: store_path,
rev: target_rev,
short_rev,
rev_count,
last_modified,
last_modified_date,
submodules,
nar_hash: Some(nar_hash),
})
}
fn clone_bare(url: &str, dest: &PathBuf) -> Result<(), GitError> {
fs::create_dir_all(dest.parent().unwrap_or(dest))?;
let output = Command::new("git")
.args(["clone", "--bare", url])
.arg(dest)
.output()?;
if !output.status.success() {
return Err(GitError::CommandFailed {
operation: "clone".to_string(),
message: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
Ok(())
}
fn fetch_repo(repo: &PathBuf, all_refs: bool) -> Result<(), GitError> {
let mut args = vec!["fetch", "--prune"];
if all_refs {
args.push("--all");
}
let output = Command::new("git").args(args).current_dir(repo).output()?;
if !output.status.success() {
return Err(GitError::CommandFailed {
operation: "fetch".to_string(),
message: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
Ok(())
}
fn resolve_rev(
repo: &PathBuf,
git_ref: Option<&str>,
rev: Option<&str>,
) -> Result<String, GitError> {
if let Some(rev) = rev {
return Ok(rev.to_string());
}
let ref_to_resolve = git_ref.unwrap_or("HEAD");
let output = Command::new("git")
.args(["rev-parse", ref_to_resolve])
.current_dir(repo)
.output()?;
if !output.status.success() {
let output = Command::new("git")
.args(["rev-parse", &format!("refs/heads/{}", ref_to_resolve)])
.current_dir(repo)
.output()?;
if !output.status.success() {
let output = Command::new("git")
.args(["rev-parse", &format!("refs/tags/{}", ref_to_resolve)])
.current_dir(repo)
.output()?;
if !output.status.success() {
return Err(GitError::CommandFailed {
operation: "rev-parse".to_string(),
message: format!("Could not resolve ref: {}", ref_to_resolve),
});
}
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn checkout_rev_to_temp(
bare_repo: &PathBuf,
rev: &str,
submodules: bool,
temp_path: &std::path::Path,
) -> Result<PathBuf, GitError> {
let checkout_dir = temp_path.join("checkout");
fs::create_dir_all(&checkout_dir)?;
let output = Command::new("git")
.args(["--work-tree", checkout_dir.to_str().unwrap_or(".")])
.arg("checkout")
.arg(rev)
.arg("--")
.arg(".")
.current_dir(bare_repo)
.output()?;
if !output.status.success() {
fs::remove_dir_all(&checkout_dir)?;
return Err(GitError::CommandFailed {
operation: "checkout".to_string(),
message: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
if submodules {
let output = Command::new("git")
.args(["submodule", "update", "--init", "--recursive"])
.current_dir(&checkout_dir)
.output()?;
if !output.status.success() {
tracing::warn!(
"failed to initialize submodules: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
let git_dir = checkout_dir.join(".git");
if git_dir.exists() {
fs::remove_dir_all(&git_dir)?;
}
Ok(checkout_dir)
}
fn get_rev_count(repo: &PathBuf, rev: &str) -> Result<u64, GitError> {
let output = Command::new("git")
.args(["rev-list", "--count", rev])
.current_dir(repo)
.output()?;
if !output.status.success() {
return Ok(0);
}
let count_str = String::from_utf8_lossy(&output.stdout);
count_str.trim().parse().unwrap_or(0).pipe(Ok)
}
fn get_last_modified(repo: &PathBuf, rev: &str) -> Result<u64, GitError> {
let output = Command::new("git")
.args(["log", "-1", "--format=%ct", rev])
.current_dir(repo)
.output()?;
if !output.status.success() {
return Ok(0);
}
let ts_str = String::from_utf8_lossy(&output.stdout);
ts_str.trim().parse().unwrap_or(0).pipe(Ok)
}
fn format_timestamp(ts: u64) -> String {
use std::time::{Duration, UNIX_EPOCH};
let datetime = UNIX_EPOCH + Duration::from_secs(ts);
let secs = datetime
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let days_since_epoch = secs / 86400;
let remaining_secs = secs % 86400;
let hours = remaining_secs / 3600;
let minutes = (remaining_secs % 3600) / 60;
let seconds = remaining_secs % 60;
let (year, month, day) = days_to_ymd(days_since_epoch);
format!(
"{:04}{:02}{:02}{:02}{:02}{:02}",
year, month, day, hours, minutes, seconds
)
}
fn days_to_ymd(days: u64) -> (u64, u64, u64) {
let mut y = 1970;
let mut remaining = days as i64;
loop {
let days_in_year = if is_leap_year(y) { 366 } else { 365 };
if remaining < days_in_year {
break;
}
remaining -= days_in_year;
y += 1;
}
let days_in_months: [i64; 12] = if is_leap_year(y) {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut m = 1;
for days_in_month in days_in_months.iter() {
if remaining < *days_in_month {
break;
}
remaining -= *days_in_month;
m += 1;
}
(y, m, (remaining + 1) as u64)
}
fn is_leap_year(y: u64) -> bool {
(y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
}
trait Pipe: Sized {
fn pipe<F, R>(self, f: F) -> R
where
F: FnOnce(Self) -> R,
{
f(self)
}
}
impl<T> Pipe for T {}
#[derive(Debug)]
pub enum GitError {
IoError(std::io::Error),
CommandFailed { operation: String, message: String },
NarHashError(String),
StoreError(String),
}
impl std::fmt::Display for GitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GitError::IoError(e) => write!(f, "I/O error: {}", e),
GitError::CommandFailed { operation, message } => {
write!(f, "Git {} failed: {}", operation, message)
}
GitError::NarHashError(e) => write!(f, "NAR hash error: {}", e),
GitError::StoreError(e) => write!(f, "Store error: {}", e),
}
}
}
impl std::error::Error for GitError {}
impl From<std::io::Error> for GitError {
fn from(e: std::io::Error) -> Self {
GitError::IoError(e)
}
}
+218
View File
@@ -0,0 +1,218 @@
#![allow(dead_code)]
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use rusqlite::{Connection, OptionalExtension, params};
use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub enum CacheError {
Database(rusqlite::Error),
Json(serde_json::Error),
}
impl std::fmt::Display for CacheError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CacheError::Database(e) => write!(f, "Database error: {}", e),
CacheError::Json(e) => write!(f, "JSON error: {}", e),
}
}
}
impl std::error::Error for CacheError {}
impl From<rusqlite::Error> for CacheError {
fn from(e: rusqlite::Error) -> Self {
CacheError::Database(e)
}
}
impl From<serde_json::Error> for CacheError {
fn from(e: serde_json::Error) -> Self {
CacheError::Json(e)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheEntry {
pub input: serde_json::Value,
pub info: serde_json::Value,
pub store_path: String,
pub immutable: bool,
pub timestamp: u64,
}
impl CacheEntry {
pub fn is_expired(&self, ttl_seconds: u64) -> bool {
if self.immutable {
return false;
}
if ttl_seconds == 0 {
return false;
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Clock may have gone backwards")
.as_secs();
now > self.timestamp + ttl_seconds
}
}
pub struct MetadataCache {
conn: Connection,
ttl_seconds: u64,
}
impl MetadataCache {
pub fn new(ttl_seconds: u64) -> Result<Self, CacheError> {
let cache_dir = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("nix-js");
std::fs::create_dir_all(&cache_dir).map_err(|e| {
CacheError::Database(rusqlite::Error::ToSqlConversionFailure(Box::new(e)))
})?;
let db_path = cache_dir.join("fetcher-cache.sqlite");
let conn = Connection::open(db_path)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS cache (
input TEXT NOT NULL PRIMARY KEY,
info TEXT NOT NULL,
store_path TEXT NOT NULL,
immutable INTEGER NOT NULL,
timestamp INTEGER NOT NULL
)",
[],
)?;
Ok(Self { conn, ttl_seconds })
}
pub fn lookup(&self, input: &serde_json::Value) -> Result<Option<CacheEntry>, CacheError> {
let input_str = serde_json::to_string(input)?;
let entry: Option<(String, String, String, i64, i64)> = self
.conn
.query_row(
"SELECT input, info, store_path, immutable, timestamp FROM cache WHERE input = ?1",
params![input_str],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
},
)
.optional()?;
match entry {
Some((input_json, info_json, store_path, immutable, timestamp)) => {
let entry = CacheEntry {
input: serde_json::from_str(&input_json)?,
info: serde_json::from_str(&info_json)?,
store_path,
immutable: immutable != 0,
timestamp: timestamp as u64,
};
if entry.is_expired(self.ttl_seconds) {
Ok(None)
} else {
Ok(Some(entry))
}
}
None => Ok(None),
}
}
pub fn lookup_expired(
&self,
input: &serde_json::Value,
) -> Result<Option<CacheEntry>, CacheError> {
let input_str = serde_json::to_string(input)?;
let entry: Option<(String, String, String, i64, i64)> = self
.conn
.query_row(
"SELECT input, info, store_path, immutable, timestamp FROM cache WHERE input = ?1",
params![input_str],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
},
)
.optional()?;
match entry {
Some((input_json, info_json, store_path, immutable, timestamp)) => {
Ok(Some(CacheEntry {
input: serde_json::from_str(&input_json)?,
info: serde_json::from_str(&info_json)?,
store_path,
immutable: immutable != 0,
timestamp: timestamp as u64,
}))
}
None => Ok(None),
}
}
pub fn add(
&self,
input: &serde_json::Value,
info: &serde_json::Value,
store_path: &str,
immutable: bool,
) -> Result<(), CacheError> {
let input_str = serde_json::to_string(input)?;
let info_str = serde_json::to_string(info)?;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Clock may have gone backwards")
.as_secs();
self.conn.execute(
"INSERT OR REPLACE INTO cache (input, info, store_path, immutable, timestamp)
VALUES (?1, ?2, ?3, ?4, ?5)",
params![
input_str,
info_str,
store_path,
if immutable { 1 } else { 0 },
timestamp as i64
],
)?;
Ok(())
}
pub fn update_timestamp(&self, input: &serde_json::Value) -> Result<(), CacheError> {
let input_str = serde_json::to_string(input)?;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Clock may have gone backwards")
.as_secs();
self.conn.execute(
"UPDATE cache SET timestamp = ?1 WHERE input = ?2",
params![timestamp as i64, input_str],
)?;
Ok(())
}
}
+683
View File
@@ -0,0 +1,683 @@
use std::{
hash::{Hash, Hasher},
ops::Deref,
};
use bumpalo::{Bump, boxed::Box, collections::Vec};
use ghost_cell::{GhostCell, GhostToken};
use rnix::{TextRange, ast};
use string_interner::symbol::SymbolU32;
pub type HashMap<'ir, K, V> = hashbrown::HashMap<K, V, hashbrown::DefaultHashBuilder, &'ir Bump>;
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct IrRef<'id, 'ir>(&'ir GhostCell<'id, Ir<'ir, Self>>);
impl<'id, 'ir> IrRef<'id, 'ir> {
pub fn new(ir: &'ir GhostCell<'id, Ir<'ir, Self>>) -> Self {
Self(ir)
}
pub fn alloc(bump: &'ir Bump, ir: Ir<'ir, Self>) -> Self {
Self(bump.alloc(GhostCell::new(ir)))
}
/// Freeze a mutable IR reference into a read-only one, consuming the
/// `GhostToken` to prevent any further mutation.
///
/// # Safety
/// The transmute is sound because:
/// - `GhostCell<'id, T>` is `#[repr(transparent)]` over `T`
/// - `IrRef<'id, 'ir>` is `#[repr(transparent)]` over
/// `&'ir GhostCell<'id, Ir<'ir, Self>>`
/// - `RawIrRef<'ir>` is `#[repr(transparent)]` over `&'ir Ir<'ir, Self>`
/// - `Ir<'ir, Ref>` is `#[repr(C)]` and both ref types are pointer-sized
///
/// Consuming the `GhostToken` guarantees no `borrow_mut` calls can occur
/// afterwards, so the shared `&Ir` references from `RawIrRef::Deref` can
/// never alias with mutable references.
pub fn freeze(self, _token: GhostToken<'id>) -> RawIrRef<'ir> {
unsafe { std::mem::transmute(self) }
}
}
impl<'id, 'ir> Deref for IrRef<'id, 'ir> {
type Target = GhostCell<'id, Ir<'ir, IrRef<'id, 'ir>>>;
fn deref(&self) -> &Self::Target {
self.0
}
}
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct RawIrRef<'ir>(&'ir Ir<'ir, Self>);
impl<'ir> Deref for RawIrRef<'ir> {
type Target = Ir<'ir, RawIrRef<'ir>>;
fn deref(&self) -> &Self::Target {
self.0
}
}
#[repr(C)]
pub enum Ir<'ir, Ref> {
Int(i64),
Float(f64),
Bool(bool),
Null,
Str(Box<'ir, String>),
AttrSet {
stcs: HashMap<'ir, SymId, (Ref, TextRange)>,
dyns: Vec<'ir, (Ref, Ref, TextRange)>,
},
List {
items: Vec<'ir, Ref>,
},
Path(Ref),
ConcatStrings {
parts: Vec<'ir, Ref>,
force_string: bool,
},
// OPs
UnOp {
rhs: Ref,
kind: UnOpKind,
},
BinOp {
lhs: Ref,
rhs: Ref,
kind: BinOpKind,
},
HasAttr {
lhs: Ref,
rhs: Vec<'ir, Attr<Ref>>,
},
Select {
expr: Ref,
attrpath: Vec<'ir, Attr<Ref>>,
default: Option<Ref>,
span: TextRange,
},
// Conditionals
If {
cond: Ref,
consq: Ref,
alter: Ref,
},
Assert {
assertion: Ref,
expr: Ref,
assertion_raw: String,
span: TextRange,
},
With {
namespace: Ref,
body: Ref,
thunks: Vec<'ir, (ThunkId, Ref)>,
},
WithLookup(SymId),
// Function related
Func {
body: Ref,
param: Option<Param<'ir>>,
arg: ArgId,
thunks: Vec<'ir, (ThunkId, Ref)>,
},
Arg(ArgId),
Call {
func: Ref,
arg: Ref,
span: TextRange,
},
// Builtins
Builtins,
Builtin(SymId),
// Misc
TopLevel {
body: Ref,
thunks: Vec<'ir, (ThunkId, Ref)>,
},
Thunk(ThunkId),
CurPos(TextRange),
ReplBinding(SymId),
ScopedImportBinding(SymId),
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ThunkId(pub usize);
pub type SymId = SymbolU32;
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ArgId(pub usize);
/// Represents a key in an attribute path.
#[allow(unused)]
#[derive(Debug)]
pub enum Attr<Ref> {
/// A dynamic attribute key, which is an expression that must evaluate to a string.
/// Example: `attrs.${key}`
Dynamic(Ref, TextRange),
/// A static attribute key.
/// Example: `attrs.key`
Str(SymId, TextRange),
}
/// The kinds of binary operations supported in Nix.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum BinOpKind {
// Arithmetic
Add,
Sub,
Div,
Mul,
// Comparison
Eq,
Neq,
Lt,
Gt,
Leq,
Geq,
// Logical
And,
Or,
Impl,
// Set/String/Path operations
Con, // List concatenation (`++`)
Upd, // AttrSet update (`//`)
// Not standard, but part of rnix AST
PipeL,
PipeR,
}
impl From<ast::BinOpKind> for BinOpKind {
fn from(op: ast::BinOpKind) -> Self {
use BinOpKind::*;
use ast::BinOpKind as kind;
match op {
kind::Concat => Con,
kind::Update => Upd,
kind::Add => Add,
kind::Sub => Sub,
kind::Mul => Mul,
kind::Div => Div,
kind::And => And,
kind::Equal => Eq,
kind::Implication => Impl,
kind::Less => Lt,
kind::LessOrEq => Leq,
kind::More => Gt,
kind::MoreOrEq => Geq,
kind::NotEqual => Neq,
kind::Or => Or,
kind::PipeLeft => PipeL,
kind::PipeRight => PipeR,
}
}
}
/// The kinds of unary operations.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum UnOpKind {
Neg, // Negation (`-`)
Not, // Logical not (`!`)
}
impl From<ast::UnaryOpKind> for UnOpKind {
fn from(value: ast::UnaryOpKind) -> Self {
match value {
ast::UnaryOpKind::Invert => UnOpKind::Not,
ast::UnaryOpKind::Negate => UnOpKind::Neg,
}
}
}
/// Describes the parameters of a function.
#[derive(Debug)]
pub struct Param<'ir> {
pub required: Vec<'ir, (SymId, TextRange)>,
pub optional: Vec<'ir, (SymId, TextRange)>,
pub ellipsis: bool,
}
#[derive(Clone, Copy)]
pub(crate) struct IrKey<'id, 'ir, 'a>(pub IrRef<'id, 'ir>, pub &'a GhostToken<'id>);
impl std::hash::Hash for IrKey<'_, '_, '_> {
fn hash<H: Hasher>(&self, state: &mut H) {
ir_content_hash(self.0, self.1, state);
}
}
impl PartialEq for IrKey<'_, '_, '_> {
fn eq(&self, other: &Self) -> bool {
ir_content_eq(self.0, other.0, self.1)
}
}
impl Eq for IrKey<'_, '_, '_> {}
fn attr_content_hash<'id>(
attr: &Attr<IrRef<'id, '_>>,
token: &GhostToken<'id>,
state: &mut impl Hasher,
) {
core::mem::discriminant(attr).hash(state);
match attr {
Attr::Dynamic(expr, _) => ir_content_hash(*expr, token, state),
Attr::Str(sym, _) => sym.hash(state),
}
}
fn attr_content_eq<'id, 'ir>(
a: &Attr<IrRef<'id, 'ir>>,
b: &Attr<IrRef<'id, 'ir>>,
token: &GhostToken<'id>,
) -> bool {
match (a, b) {
(Attr::Dynamic(ae, _), Attr::Dynamic(be, _)) => ir_content_eq(*ae, *be, token),
(Attr::Str(a, _), Attr::Str(b, _)) => a == b,
_ => false,
}
}
fn param_content_hash(param: &Param<'_>, state: &mut impl Hasher) {
param.required.len().hash(state);
for (sym, _) in param.required.iter() {
sym.hash(state);
}
param.optional.len().hash(state);
for (sym, _) in param.optional.iter() {
sym.hash(state);
}
param.ellipsis.hash(state);
}
fn param_content_eq(a: &Param<'_>, b: &Param<'_>) -> bool {
a.ellipsis == b.ellipsis
&& a.required.len() == b.required.len()
&& a.optional.len() == b.optional.len()
&& a.required
.iter()
.zip(b.required.iter())
.all(|((a, _), (b, _))| a == b)
&& a.optional
.iter()
.zip(b.optional.iter())
.all(|((a, _), (b, _))| a == b)
}
fn thunks_content_hash<'id>(
thunks: &[(ThunkId, IrRef<'id, '_>)],
token: &GhostToken<'id>,
state: &mut impl Hasher,
) {
thunks.len().hash(state);
for &(id, ir) in thunks {
id.hash(state);
ir_content_hash(ir, token, state);
}
}
fn thunks_content_eq<'id, 'ir>(
a: &[(ThunkId, IrRef<'id, 'ir>)],
b: &[(ThunkId, IrRef<'id, 'ir>)],
token: &GhostToken<'id>,
) -> bool {
a.len() == b.len()
&& a.iter()
.zip(b.iter())
.all(|(&(ai, ae), &(bi, be))| ai == bi && ir_content_eq(ae, be, token))
}
fn ir_content_hash<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>, state: &mut impl Hasher) {
let ir = ir.borrow(token);
core::mem::discriminant(ir).hash(state);
match ir {
Ir::Int(x) => x.hash(state),
Ir::Float(x) => x.to_bits().hash(state),
Ir::Bool(x) => x.hash(state),
Ir::Null => {}
Ir::Str(x) => x.hash(state),
Ir::AttrSet { stcs, dyns } => {
stcs.len().hash(state);
let mut combined: u64 = 0;
for (&key, &(val, _)) in stcs.iter() {
let mut h = std::hash::DefaultHasher::new();
key.hash(&mut h);
ir_content_hash(val, token, &mut h);
combined = combined.wrapping_add(h.finish());
}
combined.hash(state);
dyns.len().hash(state);
for &(k, v, _) in dyns.iter() {
ir_content_hash(k, token, state);
ir_content_hash(v, token, state);
}
}
Ir::List { items } => {
items.len().hash(state);
for &item in items.iter() {
ir_content_hash(item, token, state);
}
}
Ir::HasAttr { lhs, rhs } => {
ir_content_hash(*lhs, token, state);
rhs.len().hash(state);
for attr in rhs.iter() {
attr_content_hash(attr, token, state);
}
}
&Ir::BinOp { lhs, rhs, kind } => {
ir_content_hash(lhs, token, state);
ir_content_hash(rhs, token, state);
kind.hash(state);
}
&Ir::UnOp { rhs, kind } => {
ir_content_hash(rhs, token, state);
kind.hash(state);
}
Ir::Select {
expr,
attrpath,
default,
..
} => {
ir_content_hash(*expr, token, state);
attrpath.len().hash(state);
for attr in attrpath.iter() {
attr_content_hash(attr, token, state);
}
default.is_some().hash(state);
if let Some(d) = default {
ir_content_hash(*d, token, state);
}
}
&Ir::If { cond, consq, alter } => {
ir_content_hash(cond, token, state);
ir_content_hash(consq, token, state);
ir_content_hash(alter, token, state);
}
&Ir::Call { func, arg, .. } => {
ir_content_hash(func, token, state);
ir_content_hash(arg, token, state);
}
Ir::Assert {
assertion,
expr,
assertion_raw,
..
} => {
ir_content_hash(*assertion, token, state);
ir_content_hash(*expr, token, state);
assertion_raw.hash(state);
}
Ir::ConcatStrings {
force_string,
parts,
} => {
force_string.hash(state);
parts.len().hash(state);
for &part in parts.iter() {
ir_content_hash(part, token, state);
}
}
&Ir::Path(expr) => ir_content_hash(expr, token, state),
Ir::Func {
body,
arg,
param,
thunks,
} => {
ir_content_hash(*body, token, state);
arg.hash(state);
param.is_some().hash(state);
if let Some(p) = param {
param_content_hash(p, state);
}
thunks_content_hash(thunks, token, state);
}
Ir::TopLevel { body, thunks } => {
ir_content_hash(*body, token, state);
thunks_content_hash(thunks, token, state);
}
Ir::Arg(x) => x.hash(state),
Ir::Thunk(x) => x.hash(state),
Ir::Builtins => {}
Ir::Builtin(x) => x.hash(state),
Ir::CurPos(x) => x.hash(state),
Ir::ReplBinding(x) => x.hash(state),
Ir::ScopedImportBinding(x) => x.hash(state),
&Ir::With {
namespace,
body,
ref thunks,
} => {
ir_content_hash(namespace, token, state);
ir_content_hash(body, token, state);
thunks_content_hash(thunks, token, state);
}
Ir::WithLookup(x) => x.hash(state),
}
}
pub(crate) fn ir_content_eq<'id, 'ir>(
a: IrRef<'id, 'ir>,
b: IrRef<'id, 'ir>,
token: &GhostToken<'id>,
) -> bool {
std::ptr::eq(a.0, b.0)
|| match (a.borrow(token), b.borrow(token)) {
(Ir::Int(a), Ir::Int(b)) => a == b,
(Ir::Float(a), Ir::Float(b)) => a.to_bits() == b.to_bits(),
(Ir::Bool(a), Ir::Bool(b)) => a == b,
(Ir::Null, Ir::Null) => true,
(Ir::Str(a), Ir::Str(b)) => **a == **b,
(
Ir::AttrSet {
stcs: a_stcs,
dyns: a_dyns,
},
Ir::AttrSet {
stcs: b_stcs,
dyns: b_dyns,
},
) => {
a_stcs.len() == b_stcs.len()
&& a_dyns.len() == b_dyns.len()
&& a_stcs.iter().all(|(&k, &(av, _))| {
b_stcs
.get(&k)
.is_some_and(|&(bv, _)| ir_content_eq(av, bv, token))
})
&& a_dyns
.iter()
.zip(b_dyns.iter())
.all(|(&(ak, av, _), &(bk, bv, _))| {
ir_content_eq(ak, bk, token) && ir_content_eq(av, bv, token)
})
}
(Ir::List { items: a }, Ir::List { items: b }) => {
a.len() == b.len()
&& a.iter()
.zip(b.iter())
.all(|(&a, &b)| ir_content_eq(a, b, token))
}
(Ir::HasAttr { lhs: al, rhs: ar }, Ir::HasAttr { lhs: bl, rhs: br }) => {
ir_content_eq(*al, *bl, token)
&& ar.len() == br.len()
&& ar
.iter()
.zip(br.iter())
.all(|(a, b)| attr_content_eq(a, b, token))
}
(
&Ir::BinOp {
lhs: al,
rhs: ar,
kind: ak,
},
&Ir::BinOp {
lhs: bl,
rhs: br,
kind: bk,
},
) => ak == bk && ir_content_eq(al, bl, token) && ir_content_eq(ar, br, token),
(&Ir::UnOp { rhs: ar, kind: ak }, &Ir::UnOp { rhs: br, kind: bk }) => {
ak == bk && ir_content_eq(ar, br, token)
}
(
Ir::Select {
expr: ae,
attrpath: aa,
default: ad,
..
},
Ir::Select {
expr: be,
attrpath: ba,
default: bd,
..
},
) => {
ir_content_eq(*ae, *be, token)
&& aa.len() == ba.len()
&& aa
.iter()
.zip(ba.iter())
.all(|(a, b)| attr_content_eq(a, b, token))
&& match (ad, bd) {
(Some(a), Some(b)) => ir_content_eq(*a, *b, token),
(None, None) => true,
_ => false,
}
}
(
&Ir::If {
cond: ac,
consq: acs,
alter: aa,
},
&Ir::If {
cond: bc,
consq: bcs,
alter: ba,
},
) => {
ir_content_eq(ac, bc, token)
&& ir_content_eq(acs, bcs, token)
&& ir_content_eq(aa, ba, token)
}
(
&Ir::Call {
func: af, arg: aa, ..
},
&Ir::Call {
func: bf, arg: ba, ..
},
) => ir_content_eq(af, bf, token) && ir_content_eq(aa, ba, token),
(
Ir::Assert {
assertion: aa,
expr: ae,
assertion_raw: ar,
..
},
Ir::Assert {
assertion: ba,
expr: be,
assertion_raw: br,
..
},
) => ar == br && ir_content_eq(*aa, *ba, token) && ir_content_eq(*ae, *be, token),
(
Ir::ConcatStrings {
force_string: af,
parts: ap,
},
Ir::ConcatStrings {
force_string: bf,
parts: bp,
},
) => {
af == bf
&& ap.len() == bp.len()
&& ap
.iter()
.zip(bp.iter())
.all(|(&a, &b)| ir_content_eq(a, b, token))
}
(&Ir::Path(a), &Ir::Path(b)) => ir_content_eq(a, b, token),
(
Ir::Func {
body: ab,
arg: aa,
param: ap,
thunks: at,
},
Ir::Func {
body: bb,
arg: ba,
param: bp,
thunks: bt,
},
) => {
ir_content_eq(*ab, *bb, token)
&& aa == ba
&& match (ap, bp) {
(Some(a), Some(b)) => param_content_eq(a, b),
(None, None) => true,
_ => false,
}
&& thunks_content_eq(at, bt, token)
}
(
Ir::TopLevel {
body: ab,
thunks: at,
},
Ir::TopLevel {
body: bb,
thunks: bt,
},
) => ir_content_eq(*ab, *bb, token) && thunks_content_eq(at, bt, token),
(Ir::Arg(a), Ir::Arg(b)) => a == b,
(Ir::Thunk(a), Ir::Thunk(b)) => a == b,
(Ir::Builtins, Ir::Builtins) => true,
(Ir::Builtin(a), Ir::Builtin(b)) => a == b,
(Ir::CurPos(a), Ir::CurPos(b)) => a == b,
(Ir::ReplBinding(a), Ir::ReplBinding(b)) => a == b,
(Ir::ScopedImportBinding(a), Ir::ScopedImportBinding(b)) => a == b,
(
Ir::With {
namespace: a_ns,
body: a_body,
thunks: a_thunks,
},
Ir::With {
namespace: b_ns,
body: b_body,
thunks: b_thunks,
},
) => {
ir_content_eq(*a_ns, *b_ns, token)
&& ir_content_eq(*a_body, *b_body, token)
&& thunks_content_eq(a_thunks, b_thunks, token)
}
(Ir::WithLookup(a), Ir::WithLookup(b)) => a == b,
_ => false,
}
}
+21
View File
@@ -0,0 +1,21 @@
#![warn(clippy::unwrap_used)]
pub mod context;
pub mod error;
pub mod logging;
pub mod value;
mod bytecode;
mod derivation;
mod disassembler;
mod downgrade;
// mod fetcher;
mod ir;
mod nar;
mod nix_utils;
mod runtime;
mod store;
mod string_context;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
+48
View File
@@ -0,0 +1,48 @@
use std::env;
use std::io::IsTerminal;
use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt};
pub fn init_logging() {
let is_terminal = std::io::stderr().is_terminal();
let show_time = env::var("NIX_JS_LOG_TIME")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false);
let filter = EnvFilter::from_default_env();
let fmt_layer = fmt::layer()
.with_target(true)
.with_thread_ids(false)
.with_thread_names(false)
.with_file(false)
.with_line_number(false)
.with_ansi(is_terminal)
.with_level(true);
let fmt_layer = if show_time {
fmt_layer.with_timer(fmt::time::uptime()).boxed()
} else {
fmt_layer.without_time().boxed()
};
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.init();
init_miette_handler();
}
fn init_miette_handler() {
let is_terminal = std::io::stderr().is_terminal();
miette::set_hook(Box::new(move |_| {
Box::new(
miette::MietteHandlerOpts::new()
.terminal_links(is_terminal)
.unicode(is_terminal)
.color(is_terminal)
.build(),
)
}))
.ok();
}
+151
View File
@@ -0,0 +1,151 @@
use std::path::PathBuf;
use std::process::exit;
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use hashbrown::HashSet;
use fix::context::Context;
use fix::error::Source;
use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
#[derive(Parser)]
#[command(name = "nix-js", about = "Nix expression evaluator")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Compile {
#[clap(flatten)]
source: ExprSource,
#[arg(long)]
silent: bool,
},
Eval {
#[clap(flatten)]
source: ExprSource,
},
Repl,
}
#[derive(Args)]
#[group(required = true, multiple = false)]
struct ExprSource {
#[clap(short, long)]
expr: Option<String>,
#[clap(short, long)]
file: Option<PathBuf>,
}
fn run_compile(context: &mut Context, src: ExprSource, silent: bool) -> Result<()> {
let src = if let Some(expr) = src.expr {
Source::new_eval(expr)?
} else if let Some(file) = src.file {
Source::new_file(file)?
} else {
unreachable!()
};
match context.compile_bytecode(src) {
Ok(compiled) => {
if !silent {
println!("{}", context.disassemble_colored(&compiled));
}
}
Err(err) => {
eprintln!("{:?}", miette::Report::new(*err));
exit(1);
}
};
Ok(())
}
fn run_eval(context: &mut Context, src: ExprSource) -> Result<()> {
let src = if let Some(expr) = src.expr {
Source::new_eval(expr)?
} else if let Some(file) = src.file {
Source::new_file(file)?
} else {
unreachable!()
};
match context.eval_deep(src) {
Ok(value) => {
println!("{}", value.display_compat());
}
Err(err) => {
eprintln!("{:?}", miette::Report::new(*err));
exit(1);
}
};
Ok(())
}
fn run_repl(context: &mut Context) -> Result<()> {
let mut rl = DefaultEditor::new()?;
let mut scope = HashSet::new();
const RE: ere::Regex<3> = ere::compile_regex!("^[ \t]*([a-zA-Z_][a-zA-Z0-9_'-]*)[ \t]*(.*)$");
loop {
let readline = rl.readline("nix-js-repl> ");
match readline {
Ok(line) => {
if line.trim().is_empty() {
continue;
}
let _ = rl.add_history_entry(line.as_str());
if let Some([Some(_), Some(ident), Some(rest)]) = RE.exec(&line) {
if let Some(expr) = rest.strip_prefix('=') {
let expr = expr.trim_start();
if expr.is_empty() {
eprintln!("Error: missing expression after '='");
continue;
}
match context.add_binding(ident, expr, &mut scope) {
Ok(value) => println!("{} = {}", ident, value),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}
} else {
let src = Source::new_repl(line)?;
match context.eval_repl(src, &scope) {
Ok(value) => println!("{value}"),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}
}
} else {
let src = Source::new_repl(line)?;
match context.eval_shallow(src) {
Ok(value) => println!("{value}"),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}
}
}
Err(ReadlineError::Interrupted) => {
println!();
}
Err(ReadlineError::Eof) => {
println!("CTRL-D");
break;
}
Err(err) => {
eprintln!("Error: {err:?}");
break;
}
}
}
Ok(())
}
fn main() -> Result<()> {
fix::logging::init_logging();
let cli = Cli::parse();
let mut context = Context::new()?;
match cli.command {
Command::Compile { source, silent } => run_compile(&mut context, source, silent),
Command::Eval { source } => run_eval(&mut context, source),
Command::Repl => run_repl(&mut context),
}
}
+66
View File
@@ -0,0 +1,66 @@
use std::io::Read;
use std::path::Path;
use nix_nar::Encoder;
use sha2::{Digest, Sha256};
use crate::error::{Error, Result};
pub fn compute_nar_hash(path: &Path) -> Result<[u8; 32]> {
let mut hasher = Sha256::new();
std::io::copy(
&mut Encoder::new(path).map_err(|err| Error::internal(err.to_string()))?,
&mut hasher,
)
.map_err(|err| Error::internal(err.to_string()))?;
Ok(hasher.finalize().into())
}
pub fn pack_nar(path: &Path) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
Encoder::new(path)
.map_err(|err| Error::internal(err.to_string()))?
.read_to_end(&mut buffer)
.map_err(|err| Error::internal(err.to_string()))?;
Ok(buffer)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::fs;
use tempfile::TempDir;
use super::*;
#[test_log::test]
fn test_simple_file() {
let temp = TempDir::new().unwrap();
let file_path = temp.path().join("test.txt");
fs::write(&file_path, "hello").unwrap();
let hash = hex::encode(compute_nar_hash(&file_path).unwrap());
assert_eq!(
hash,
"0a430879c266f8b57f4092a0f935cf3facd48bbccde5760d4748ca405171e969"
);
assert!(!hash.is_empty());
assert_eq!(hash.len(), 64);
}
#[test_log::test]
fn test_directory() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("a.txt"), "aaa").unwrap();
fs::write(temp.path().join("b.txt"), "bbb").unwrap();
let hash = hex::encode(compute_nar_hash(temp.path()).unwrap());
assert_eq!(
hash,
"0036c14209749bc9b9631e2077b108b701c322ab53853cd26f2746268a86fc0f"
);
assert!(!hash.is_empty());
assert_eq!(hash.len(), 64);
}
}
+21
View File
@@ -0,0 +1,21 @@
use nix_compat::store_path::compress_hash;
use sha2::{Digest as _, Sha256};
pub fn sha256_hex(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
hex::encode(hasher.finalize())
}
pub fn make_store_path(store_dir: &str, ty: &str, hash_hex: &str, name: &str) -> String {
let s = format!("{}:sha256:{}:{}:{}", ty, hash_hex, store_dir, name);
let mut hasher = Sha256::new();
hasher.update(s.as_bytes());
let hash: [u8; 32] = hasher.finalize().into();
let compressed = compress_hash::<20>(&hash);
let encoded = nix_compat::nixbase32::encode(&compressed);
format!("{}/{}-{}", store_dir, encoded, name)
}
View File
+31
View File
@@ -0,0 +1,31 @@
drvAttrs@{
outputs ? [ "out" ],
...
}:
let
strict = derivationStrict drvAttrs;
commonAttrs =
drvAttrs
// (builtins.listToAttrs outputsList)
// {
all = map (x: x.value) outputsList;
inherit drvAttrs;
};
outputToAttrListElement = outputName: {
name = outputName;
value = commonAttrs // {
outPath = builtins.getAttr outputName strict;
drvPath = strict.drvPath;
type = "derivation";
inherit outputName;
};
};
outputsList = map outputToAttrListElement outputs;
in
(builtins.head outputsList).value
+76
View File
@@ -0,0 +1,76 @@
{
system ? "", # obsolete
url,
hash ? "", # an SRI hash
# Legacy hash specification
md5 ? "",
sha1 ? "",
sha256 ? "",
sha512 ? "",
outputHash ?
if hash != "" then
hash
else if sha512 != "" then
sha512
else if sha1 != "" then
sha1
else if md5 != "" then
md5
else
sha256,
outputHashAlgo ?
if hash != "" then
""
else if sha512 != "" then
"sha512"
else if sha1 != "" then
"sha1"
else if md5 != "" then
"md5"
else
"sha256",
executable ? false,
unpack ? false,
name ? baseNameOf (toString url),
# still translates to __impure to trigger derivationStrict error checks.
impure ? false,
}:
derivation (
{
builder = "builtin:fetchurl";
# New-style output content requirements.
outputHashMode = if unpack || executable then "recursive" else "flat";
inherit
name
url
executable
unpack
;
system = "builtin";
# No need to double the amount of network traffic
preferLocalBuild = true;
impureEnvVars = [
# We borrow these environment variables from the caller to allow
# easy proxy configuration. This is impure, but a fixed-output
# derivation like fetchurl is allowed to do so since its result is
# by definition pure.
"http_proxy"
"https_proxy"
"ftp_proxy"
"all_proxy"
"no_proxy"
];
# To make "nix-prefetch-url" work.
urls = [ url ];
}
// (if impure then { __impure = true; } else { inherit outputHashAlgo outputHash; })
)
+40
View File
@@ -0,0 +1,40 @@
use crate::error::Result;
mod config;
mod daemon;
mod error;
mod validation;
pub use config::StoreConfig;
pub use daemon::DaemonStore;
pub use validation::validate_store_path;
pub trait Store: Send + Sync {
fn get_store_dir(&self) -> &str;
fn is_valid_path(&self, path: &str) -> Result<bool>;
fn ensure_path(&self, path: &str) -> Result<()>;
fn add_to_store(
&self,
name: &str,
content: &[u8],
recursive: bool,
references: Vec<String>,
) -> Result<String>;
fn add_to_store_from_path(
&self,
name: &str,
source_path: &std::path::Path,
references: Vec<String>,
) -> Result<String>;
fn add_text_to_store(
&self,
name: &str,
content: &str,
references: Vec<String>,
) -> Result<String>;
}
+22
View File
@@ -0,0 +1,22 @@
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct StoreConfig {
pub daemon_socket: PathBuf,
}
impl StoreConfig {
pub fn from_env() -> Self {
let daemon_socket = std::env::var("NIX_DAEMON_SOCKET")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/nix/var/nix/daemon-socket/socket"));
Self { daemon_socket }
}
}
impl Default for StoreConfig {
fn default() -> Self {
Self::from_env()
}
}
+773
View File
@@ -0,0 +1,773 @@
use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
use std::path::Path;
use nix_compat::nix_daemon::types::{AddToStoreNarRequest, UnkeyedValidPathInfo};
use nix_compat::nix_daemon::worker_protocol::{ClientSettings, Operation};
use nix_compat::store_path::StorePath;
use nix_compat::wire::ProtocolVersion;
use nix_compat::wire::de::{NixRead, NixReader};
use nix_compat::wire::ser::{NixSerialize, NixWrite, NixWriter, NixWriterBuilder};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf, split};
use tokio::net::UnixStream;
use tokio::sync::Mutex;
use super::Store;
use crate::error::{Error, Result};
pub struct DaemonStore {
runtime: tokio::runtime::Runtime,
connection: NixDaemonConnection,
}
impl DaemonStore {
pub fn connect(socket_path: &Path) -> Result<Self> {
let runtime = tokio::runtime::Runtime::new()
.map_err(|e| Error::internal(format!("Failed to create tokio runtime: {}", e)))?;
let connection = runtime.block_on(async {
NixDaemonConnection::connect(socket_path)
.await
.map_err(|e| {
Error::internal(format!(
"Failed to connect to nix-daemon at {}: {}",
socket_path.display(),
e
))
})
})?;
Ok(Self {
runtime,
connection,
})
}
fn block_on<F>(&self, future: F) -> F::Output
where
F: std::future::Future,
{
self.runtime.block_on(future)
}
}
impl Store for DaemonStore {
fn get_store_dir(&self) -> &str {
"/nix/store"
}
fn is_valid_path(&self, path: &str) -> Result<bool> {
self.block_on(async {
self.connection
.is_valid_path(path)
.await
.map_err(|e| Error::internal(format!("Daemon error in is_valid_path: {}", e)))
})
}
fn ensure_path(&self, path: &str) -> Result<()> {
self.block_on(async {
self.connection.ensure_path(path).await.map_err(|e| {
Error::eval_error(
format!(
"builtins.storePath: path '{}' is not valid in nix store: {}",
path, e
),
None,
)
})
})
}
fn add_to_store(
&self,
name: &str,
content: &[u8],
recursive: bool,
references: Vec<String>,
) -> Result<String> {
use std::fs;
use nix_compat::nix_daemon::types::AddToStoreNarRequest;
use nix_compat::nixhash::{CAHash, NixHash};
use nix_compat::store_path::{StorePath, build_ca_path};
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new()
.map_err(|e| Error::internal(format!("Failed to create temp file: {}", e)))?;
fs::write(temp_file.path(), content)
.map_err(|e| Error::internal(format!("Failed to write temp file: {}", e)))?;
let nar_data = crate::nar::pack_nar(temp_file.path())?;
let nar_hash_hex = {
let mut hasher = Sha256::new();
hasher.update(&nar_data);
hex::encode(hasher.finalize())
};
let nar_hash_bytes = hex::decode(&nar_hash_hex)
.map_err(|e| Error::internal(format!("Invalid nar hash: {}", e)))?;
let mut nar_hash_arr = [0u8; 32];
nar_hash_arr.copy_from_slice(&nar_hash_bytes);
let ca_hash = if recursive {
CAHash::Nar(NixHash::Sha256(nar_hash_arr))
} else {
let mut content_hasher = Sha256::new();
content_hasher.update(content);
let content_hash = content_hasher.finalize();
let mut content_hash_arr = [0u8; 32];
content_hash_arr.copy_from_slice(&content_hash);
CAHash::Flat(NixHash::Sha256(content_hash_arr))
};
let ref_store_paths: std::result::Result<Vec<StorePath<String>>, _> = references
.iter()
.map(|r| StorePath::<String>::from_absolute_path(r.as_bytes()))
.collect();
let ref_store_paths = ref_store_paths
.map_err(|e| Error::internal(format!("Invalid reference path: {}", e)))?;
let store_path: StorePath<String> =
build_ca_path(name, &ca_hash, references.clone(), false)
.map_err(|e| Error::internal(format!("Failed to build store path: {}", e)))?;
let store_path_str = store_path.to_absolute_path();
if self.is_valid_path(&store_path_str)? {
return Ok(store_path_str);
}
let request = AddToStoreNarRequest {
path: store_path,
deriver: None,
nar_hash: unsafe {
std::mem::transmute::<[u8; 32], nix_compat::nix_daemon::types::NarHash>(
nar_hash_arr,
)
},
references: ref_store_paths,
registration_time: 0,
nar_size: nar_data.len() as u64,
ultimate: false,
signatures: vec![],
ca: Some(ca_hash),
repair: false,
dont_check_sigs: false,
};
self.block_on(async {
self.connection
.add_to_store_nar(request, &nar_data)
.await
.map_err(|e| Error::internal(format!("Failed to add to store: {}", e)))
})?;
Ok(store_path_str)
}
fn add_to_store_from_path(
&self,
name: &str,
source_path: &std::path::Path,
references: Vec<String>,
) -> Result<String> {
use nix_compat::nix_daemon::types::AddToStoreNarRequest;
use nix_compat::nixhash::{CAHash, NixHash};
use nix_compat::store_path::{StorePath, build_ca_path};
use sha2::{Digest, Sha256};
let nar_data = crate::nar::pack_nar(source_path)?;
let nar_hash: [u8; 32] = {
let mut hasher = Sha256::new();
hasher.update(&nar_data);
hasher.finalize().into()
};
let ca_hash = CAHash::Nar(NixHash::Sha256(nar_hash));
let ref_store_paths: std::result::Result<Vec<StorePath<String>>, _> = references
.iter()
.map(|r| StorePath::<String>::from_absolute_path(r.as_bytes()))
.collect();
let ref_store_paths = ref_store_paths
.map_err(|e| Error::internal(format!("Invalid reference path: {}", e)))?;
let store_path: StorePath<String> =
build_ca_path(name, &ca_hash, references.clone(), false)
.map_err(|e| Error::internal(format!("Failed to build store path: {}", e)))?;
let store_path_str = store_path.to_absolute_path();
if self.is_valid_path(&store_path_str)? {
return Ok(store_path_str);
}
let request = AddToStoreNarRequest {
path: store_path,
deriver: None,
nar_hash: unsafe {
std::mem::transmute::<[u8; 32], nix_compat::nix_daemon::types::NarHash>(nar_hash)
},
references: ref_store_paths,
registration_time: 0,
nar_size: nar_data.len() as u64,
ultimate: false,
signatures: vec![],
ca: Some(ca_hash),
repair: false,
dont_check_sigs: false,
};
self.block_on(async {
self.connection
.add_to_store_nar(request, &nar_data)
.await
.map_err(|e| Error::internal(format!("Failed to add to store: {}", e)))
})?;
Ok(store_path_str)
}
fn add_text_to_store(
&self,
name: &str,
content: &str,
references: Vec<String>,
) -> Result<String> {
use std::fs;
use nix_compat::nix_daemon::types::AddToStoreNarRequest;
use nix_compat::nixhash::CAHash;
use nix_compat::store_path::{StorePath, build_text_path};
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new()
.map_err(|e| Error::internal(format!("Failed to create temp file: {}", e)))?;
fs::write(temp_file.path(), content.as_bytes())
.map_err(|e| Error::internal(format!("Failed to write temp file: {}", e)))?;
let nar_data = crate::nar::pack_nar(temp_file.path())?;
let nar_hash: [u8; 32] = {
let mut hasher = Sha256::new();
hasher.update(&nar_data);
hasher.finalize().into()
};
let content_hash = {
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
hasher.finalize().into()
};
let ref_store_paths: std::result::Result<Vec<StorePath<String>>, _> = references
.iter()
.map(|r| StorePath::<String>::from_absolute_path(r.as_bytes()))
.collect();
let ref_store_paths = ref_store_paths
.map_err(|e| Error::internal(format!("Invalid reference path: {}", e)))?;
let store_path: StorePath<String> = build_text_path(name, content, references.clone())
.map_err(|e| Error::internal(format!("Failed to build text store path: {}", e)))?;
let store_path_str = store_path.to_absolute_path();
if self.is_valid_path(&store_path_str)? {
return Ok(store_path_str);
}
let request = AddToStoreNarRequest {
path: store_path,
deriver: None,
nar_hash: unsafe {
std::mem::transmute::<[u8; 32], nix_compat::nix_daemon::types::NarHash>(nar_hash)
},
references: ref_store_paths,
registration_time: 0,
nar_size: nar_data.len() as u64,
ultimate: false,
signatures: vec![],
ca: Some(CAHash::Text(content_hash)),
repair: false,
dont_check_sigs: false,
};
self.block_on(async {
self.connection
.add_to_store_nar(request, &nar_data)
.await
.map_err(|e| Error::internal(format!("Failed to add text to store: {}", e)))
})?;
Ok(store_path_str)
}
}
const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::from_parts(1, 37);
// Protocol magic numbers (from nix-compat worker_protocol.rs)
const WORKER_MAGIC_1: u64 = 0x6e697863; // "nixc"
const WORKER_MAGIC_2: u64 = 0x6478696f; // "dxio"
const STDERR_LAST: u64 = 0x616c7473; // "alts"
const STDERR_ERROR: u64 = 0x63787470; // "cxtp"
/// Performs the client handshake with a nix-daemon server
///
/// This is the client-side counterpart to `server_handshake_client`.
/// It exchanges magic numbers, negotiates protocol version, and sends client settings.
async fn client_handshake<RW>(
conn: &mut RW,
client_settings: &ClientSettings,
) -> IoResult<ProtocolVersion>
where
RW: AsyncReadExt + AsyncWriteExt + Unpin,
{
// 1. Send magic number 1
conn.write_u64_le(WORKER_MAGIC_1).await?;
// 2. Receive magic number 2
let magic2 = conn.read_u64_le().await?;
if magic2 != WORKER_MAGIC_2 {
return Err(IoError::new(
IoErrorKind::InvalidData,
format!("Invalid magic number from server: {}", magic2),
));
}
// 3. Receive server protocol version
let server_version_raw = conn.read_u64_le().await?;
let server_version: ProtocolVersion = server_version_raw.try_into().map_err(|e| {
IoError::new(
IoErrorKind::InvalidData,
format!("Invalid protocol version: {}", e),
)
})?;
// 4. Send our protocol version
conn.write_u64_le(PROTOCOL_VERSION.into()).await?;
// Pick the minimum version
let protocol_version = std::cmp::min(PROTOCOL_VERSION, server_version);
// 5. Send obsolete fields based on protocol version
if protocol_version.minor() >= 14 {
// CPU affinity (obsolete, send 0)
conn.write_u64_le(0).await?;
}
if protocol_version.minor() >= 11 {
// Reserve space (obsolete, send 0)
conn.write_u64_le(0).await?;
}
if protocol_version.minor() >= 33 {
// Read Nix version string
let version_len = conn.read_u64_le().await? as usize;
let mut version_bytes = vec![0u8; version_len];
conn.read_exact(&mut version_bytes).await?;
// Padding
let padding = (8 - (version_len % 8)) % 8;
if padding > 0 {
let mut pad = vec![0u8; padding];
conn.read_exact(&mut pad).await?;
}
}
if protocol_version.minor() >= 35 {
// Read trust level
let _trust = conn.read_u64_le().await?;
}
// 6. Read STDERR_LAST
let stderr_last = conn.read_u64_le().await?;
if stderr_last != STDERR_LAST {
return Err(IoError::new(
IoErrorKind::InvalidData,
format!("Expected STDERR_LAST, got: {}", stderr_last),
));
}
// 7. Send SetOptions operation with client settings
conn.write_u64_le(Operation::SetOptions.into()).await?;
conn.flush().await?;
// Serialize client settings
let mut settings_buf = Vec::new();
{
let mut writer = NixWriterBuilder::default()
.set_version(protocol_version)
.build(&mut settings_buf);
writer.write_value(client_settings).await?;
writer.flush().await?;
}
conn.write_all(&settings_buf).await?;
conn.flush().await?;
// 8. Read response to SetOptions
let response = conn.read_u64_le().await?;
if response != STDERR_LAST {
return Err(IoError::new(
IoErrorKind::InvalidData,
format!("Expected STDERR_LAST after SetOptions, got: {}", response),
));
}
Ok(protocol_version)
}
/// Low-level Nix Daemon client
///
/// This struct manages communication with a nix-daemon using the wire protocol.
/// It is NOT thread-safe and should be wrapped in a Mutex for concurrent access.
pub struct NixDaemonClient {
protocol_version: ProtocolVersion,
reader: NixReader<ReadHalf<UnixStream>>,
writer: NixWriter<WriteHalf<UnixStream>>,
_marker: std::marker::PhantomData<std::cell::Cell<()>>,
}
impl NixDaemonClient {
/// Connect to a nix-daemon at the given Unix socket path
pub async fn connect(socket_path: &Path) -> IoResult<Self> {
let stream = UnixStream::connect(socket_path).await?;
Self::from_stream(stream).await
}
/// Create a client from an existing Unix stream
pub async fn from_stream(mut stream: UnixStream) -> IoResult<Self> {
let client_settings = ClientSettings::default();
// Perform handshake
let protocol_version = client_handshake(&mut stream, &client_settings).await?;
// Split stream into reader and writer
let (read_half, write_half) = split(stream);
let reader = NixReader::builder()
.set_version(protocol_version)
.build(read_half);
let writer = NixWriterBuilder::default()
.set_version(protocol_version)
.build(write_half);
Ok(Self {
protocol_version,
reader,
writer,
_marker: Default::default(),
})
}
/// Execute an operation with a single parameter
async fn execute_with<P, T>(&mut self, operation: Operation, param: &P) -> IoResult<T>
where
P: NixSerialize + Send,
T: nix_compat::wire::de::NixDeserialize,
{
// Send operation
self.writer.write_value(&operation).await?;
// Send parameter
self.writer.write_value(param).await?;
self.writer.flush().await?;
self.read_response().await
}
/// Read a response from the daemon
///
/// The daemon sends either:
/// - STDERR_LAST followed by the result
/// - STDERR_ERROR followed by a structured error
async fn read_response<T>(&mut self) -> IoResult<T>
where
T: nix_compat::wire::de::NixDeserialize,
{
loop {
let msg = self.reader.read_number().await?;
if msg == STDERR_LAST {
let result: T = self.reader.read_value().await?;
return Ok(result);
} else if msg == STDERR_ERROR {
let error_msg = self.read_daemon_error().await?;
return Err(IoError::other(error_msg));
} else {
let _data: String = self.reader.read_value().await?;
continue;
}
}
}
async fn read_daemon_error(&mut self) -> IoResult<NixDaemonError> {
let type_marker: String = self.reader.read_value().await?;
assert_eq!(type_marker, "Error");
let level = NixDaemonErrorLevel::try_from_primitive(
self.reader
.read_number()
.await?
.try_into()
.map_err(|_| IoError::other("invalid nix-daemon error level"))?,
)
.map_err(|_| IoError::other("invalid nix-daemon error level"))?;
// removed
let _name: String = self.reader.read_value().await?;
let msg: String = self.reader.read_value().await?;
let have_pos: u64 = self.reader.read_number().await?;
assert_eq!(have_pos, 0);
let nr_traces: u64 = self.reader.read_number().await?;
let mut traces = Vec::new();
for _ in 0..nr_traces {
let _trace_pos: u64 = self.reader.read_number().await?;
let trace_hint: String = self.reader.read_value().await?;
traces.push(trace_hint);
}
Ok(NixDaemonError { level, msg, traces })
}
/// Check if a path is valid in the store
pub async fn is_valid_path(&mut self, path: &str) -> IoResult<bool> {
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
self.execute_with(Operation::IsValidPath, &store_path).await
}
/// Query information about a store path
#[allow(dead_code)]
pub async fn query_path_info(&mut self, path: &str) -> IoResult<Option<UnkeyedValidPathInfo>> {
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
self.writer.write_value(&Operation::QueryPathInfo).await?;
self.writer.write_value(&store_path).await?;
self.writer.flush().await?;
loop {
let msg = self.reader.read_number().await?;
if msg == STDERR_LAST {
let has_value: bool = self.reader.read_value().await?;
if has_value {
use nix_compat::narinfo::Signature;
use nix_compat::nixhash::CAHash;
let deriver = self.reader.read_value().await?;
let nar_hash: String = self.reader.read_value().await?;
let references = self.reader.read_value().await?;
let registration_time = self.reader.read_value().await?;
let nar_size = self.reader.read_value().await?;
let ultimate = self.reader.read_value().await?;
let signatures: Vec<Signature<String>> = self.reader.read_value().await?;
let ca: Option<CAHash> = self.reader.read_value().await?;
let value = UnkeyedValidPathInfo {
deriver,
nar_hash,
references,
registration_time,
nar_size,
ultimate,
signatures,
ca,
};
return Ok(Some(value));
} else {
return Ok(None);
}
} else if msg == STDERR_ERROR {
let error_msg = self.read_daemon_error().await?;
return Err(IoError::other(error_msg));
} else {
let _data: String = self.reader.read_value().await?;
continue;
}
}
}
/// Ensure a path is available in the store
pub async fn ensure_path(&mut self, path: &str) -> IoResult<()> {
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
self.writer.write_value(&Operation::EnsurePath).await?;
self.writer.write_value(&store_path).await?;
self.writer.flush().await?;
loop {
let msg = self.reader.read_number().await?;
if msg == STDERR_LAST {
return Ok(());
} else if msg == STDERR_ERROR {
let error_msg = self.read_daemon_error().await?;
return Err(IoError::other(error_msg));
} else {
let _data: String = self.reader.read_value().await?;
continue;
}
}
}
/// Query which paths are valid
#[allow(dead_code)]
pub async fn query_valid_paths(&mut self, paths: Vec<String>) -> IoResult<Vec<String>> {
let store_paths: IoResult<Vec<StorePath<String>>> = paths
.iter()
.map(|p| {
StorePath::<String>::from_absolute_path(p.as_bytes())
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))
})
.collect();
let store_paths = store_paths?;
// Send operation
self.writer.write_value(&Operation::QueryValidPaths).await?;
// Manually serialize the request since QueryValidPaths doesn't impl NixSerialize
// QueryValidPaths = { paths: Vec<StorePath>, substitute: bool }
self.writer.write_value(&store_paths).await?;
// For protocol >= 1.27, send substitute flag
if self.protocol_version.minor() >= 27 {
self.writer.write_value(&false).await?;
}
self.writer.flush().await?;
let result: Vec<StorePath<String>> = self.read_response().await?;
Ok(result.into_iter().map(|p| p.to_absolute_path()).collect())
}
/// Add a NAR to the store
pub async fn add_to_store_nar(
&mut self,
request: AddToStoreNarRequest,
nar_data: &[u8],
) -> IoResult<()> {
tracing::debug!(
"add_to_store_nar: path={}, nar_size={}",
request.path.to_absolute_path(),
request.nar_size,
);
self.writer.write_value(&Operation::AddToStoreNar).await?;
self.writer.write_value(&request.path).await?;
self.writer.write_value(&request.deriver).await?;
let nar_hash_hex = hex::encode(request.nar_hash.as_ref());
self.writer.write_value(&nar_hash_hex).await?;
self.writer.write_value(&request.references).await?;
self.writer.write_value(&request.registration_time).await?;
self.writer.write_value(&request.nar_size).await?;
self.writer.write_value(&request.ultimate).await?;
self.writer.write_value(&request.signatures).await?;
self.writer.write_value(&request.ca).await?;
self.writer.write_value(&request.repair).await?;
self.writer.write_value(&request.dont_check_sigs).await?;
if self.protocol_version.minor() >= 23 {
self.writer.write_number(nar_data.len() as u64).await?;
self.writer.write_all(nar_data).await?;
self.writer.write_number(0u64).await?;
} else {
self.writer.write_slice(nar_data).await?;
}
self.writer.flush().await?;
loop {
let msg = self.reader.read_number().await?;
if msg == STDERR_LAST {
return Ok(());
} else if msg == STDERR_ERROR {
let error_msg = self.read_daemon_error().await?;
return Err(IoError::other(error_msg));
} else {
let _data: String = self.reader.read_value().await?;
continue;
}
}
}
}
/// Thread-safe wrapper around NixDaemonClient
pub struct NixDaemonConnection {
client: Mutex<NixDaemonClient>,
}
impl NixDaemonConnection {
/// Connect to a nix-daemon at the given socket path
pub async fn connect(socket_path: &Path) -> IoResult<Self> {
let client = NixDaemonClient::connect(socket_path).await?;
Ok(Self {
client: Mutex::new(client),
})
}
/// Check if a path is valid in the store
pub async fn is_valid_path(&self, path: &str) -> IoResult<bool> {
let mut client = self.client.lock().await;
client.is_valid_path(path).await
}
/// Query information about a store path
#[allow(dead_code)]
pub async fn query_path_info(&self, path: &str) -> IoResult<Option<UnkeyedValidPathInfo>> {
let mut client = self.client.lock().await;
client.query_path_info(path).await
}
/// Ensure a path is available in the store
pub async fn ensure_path(&self, path: &str) -> IoResult<()> {
let mut client = self.client.lock().await;
client.ensure_path(path).await
}
/// Query which paths are valid
#[allow(dead_code)]
pub async fn query_valid_paths(&self, paths: Vec<String>) -> IoResult<Vec<String>> {
let mut client = self.client.lock().await;
client.query_valid_paths(paths).await
}
/// Add a NAR to the store
pub async fn add_to_store_nar(
&self,
request: AddToStoreNarRequest,
nar_data: &[u8],
) -> IoResult<()> {
let mut client = self.client.lock().await;
client.add_to_store_nar(request, nar_data).await
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum NixDaemonErrorLevel {
Error = 0,
Warn,
Notice,
Info,
Talkative,
Chatty,
Debug,
Vomit,
}
#[derive(Debug, Error)]
#[error("{msg}")]
pub struct NixDaemonError {
level: NixDaemonErrorLevel,
msg: String,
traces: Vec<String>,
}
+34
View File
@@ -0,0 +1,34 @@
#![allow(dead_code)]
use std::fmt;
#[derive(Debug)]
pub enum StoreError {
DaemonConnectionFailed(String),
OperationFailed(String),
InvalidPath(String),
PathNotFound(String),
Io(std::io::Error),
}
impl fmt::Display for StoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StoreError::DaemonConnectionFailed(msg) => {
write!(f, "Failed to connect to nix-daemon: {}", msg)
}
StoreError::OperationFailed(msg) => write!(f, "Store operation failed: {}", msg),
StoreError::InvalidPath(msg) => write!(f, "Invalid store path: {}", msg),
StoreError::PathNotFound(path) => write!(f, "Path not found in store: {}", path),
StoreError::Io(e) => write!(f, "I/O error: {}", e),
}
}
}
impl std::error::Error for StoreError {}
impl From<std::io::Error> for StoreError {
fn from(e: std::io::Error) -> Self {
StoreError::Io(e)
}
}
+153
View File
@@ -0,0 +1,153 @@
use crate::error::{Error, Result};
pub fn validate_store_path(store_dir: &str, path: &str) -> Result<()> {
if !path.starts_with(store_dir) {
return Err(Error::eval_error(
format!("path '{}' is not in the Nix store", path),
None,
));
}
let relative = path
.strip_prefix(store_dir)
.and_then(|s| s.strip_prefix('/'))
.ok_or_else(|| Error::eval_error(format!("invalid store path format: {}", path), None))?;
if relative.is_empty() {
return Err(Error::eval_error(
format!("store path cannot be store directory itself: {}", path),
None,
));
}
let parts: Vec<&str> = relative.splitn(2, '-').collect();
if parts.len() != 2 {
return Err(Error::eval_error(
format!("invalid store path format (missing name): {}", path),
None,
));
}
let hash = parts[0];
let name = parts[1];
if hash.len() != 32 {
return Err(Error::eval_error(
format!(
"invalid store path hash length (expected 32, got {}): {}",
hash.len(),
hash
),
None,
));
}
for ch in hash.chars() {
if !matches!(ch, '0'..='9' | 'a'..='d' | 'f'..='n' | 'p'..='s' | 'v'..='z') {
return Err(Error::eval_error(
format!("invalid character '{}' in store path hash: {}", ch, hash),
None,
));
}
}
if name.is_empty() {
return Err(Error::eval_error(
format!("store path has empty name: {}", path),
None,
));
}
if name.starts_with('.') {
return Err(Error::eval_error(
format!("store path name cannot start with '.': {}", name),
None,
));
}
for ch in name.chars() {
if !matches!(ch, '0'..='9' | 'a'..='z' | 'A'..='Z' | '+' | '-' | '.' | '_' | '?' | '=') {
return Err(Error::eval_error(
format!("invalid character '{}' in store path name: {}", ch, name),
None,
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test_log::test]
fn test_valid_store_paths() {
let store_dir = "/nix/store";
let valid_paths = vec![
"/nix/store/0123456789abcdfghijklmnpqrsvwxyz-hello",
"/nix/store/abcdfghijklmnpqrsvwxyz0123456789-hello-1.0",
"/nix/store/00000000000000000000000000000000-test_+-.?=",
];
for path in valid_paths {
assert!(
validate_store_path(store_dir, path).is_ok(),
"Expected {} to be valid, got {:?}",
path,
validate_store_path(store_dir, path)
);
}
}
#[test_log::test]
fn test_invalid_store_paths() {
let store_dir = "/nix/store";
let invalid_paths = vec![
("/tmp/foo", "not in store"),
("/nix/store", "empty relative"),
("/nix/store/tooshort-name", "hash too short"),
(
"/nix/store/abc123defghijklmnopqrstuvwxyz123-name",
"hash too long",
),
(
"/nix/store/abcd1234abcd1234abcd1234abcd123e-name",
"e in hash",
),
(
"/nix/store/abcd1234abcd1234abcd1234abcd123o-name",
"o in hash",
),
(
"/nix/store/abcd1234abcd1234abcd1234abcd123u-name",
"u in hash",
),
(
"/nix/store/abcd1234abcd1234abcd1234abcd123t-name",
"t in hash",
),
(
"/nix/store/abcd1234abcd1234abcd1234abcd1234-.name",
"name starts with dot",
),
(
"/nix/store/abcd1234abcd1234abcd1234abcd1234-na/me",
"slash in name",
),
(
"/nix/store/abcd1234abcd1234abcd1234abcd1234",
"missing name",
),
];
for (path, reason) in invalid_paths {
assert!(
validate_store_path(store_dir, path).is_err(),
"Expected {} to be invalid ({})",
path,
reason
);
}
}
}
+209
View File
@@ -0,0 +1,209 @@
use std::collections::{BTreeMap, BTreeSet, VecDeque};
pub enum StringContextElem {
Opaque { path: String },
DrvDeep { drv_path: String },
Built { drv_path: String, output: String },
}
impl StringContextElem {
pub fn decode(encoded: &str) -> Self {
if let Some(drv_path) = encoded.strip_prefix('=') {
StringContextElem::DrvDeep {
drv_path: drv_path.to_string(),
}
} else if let Some(rest) = encoded.strip_prefix('!') {
if let Some(second_bang) = rest.find('!') {
let output = rest[..second_bang].to_string();
let drv_path = rest[second_bang + 1..].to_string();
StringContextElem::Built { drv_path, output }
} else {
StringContextElem::Opaque {
path: encoded.to_string(),
}
}
} else {
StringContextElem::Opaque {
path: encoded.to_string(),
}
}
}
}
pub type InputDrvs = BTreeMap<String, BTreeSet<String>>;
pub type Srcs = BTreeSet<String>;
pub fn extract_input_drvs_and_srcs(context: &[String]) -> Result<(InputDrvs, Srcs), String> {
let mut input_drvs: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut input_srcs: BTreeSet<String> = BTreeSet::new();
for encoded in context {
match StringContextElem::decode(encoded) {
StringContextElem::Opaque { path } => {
input_srcs.insert(path);
}
StringContextElem::DrvDeep { drv_path } => {
compute_fs_closure(&drv_path, &mut input_drvs, &mut input_srcs)?;
}
StringContextElem::Built { drv_path, output } => {
input_drvs.entry(drv_path).or_default().insert(output);
}
}
}
Ok((input_drvs, input_srcs))
}
fn compute_fs_closure(
drv_path: &str,
input_drvs: &mut BTreeMap<String, BTreeSet<String>>,
input_srcs: &mut BTreeSet<String>,
) -> Result<(), String> {
let mut queue: VecDeque<String> = VecDeque::new();
let mut visited: BTreeSet<String> = BTreeSet::new();
queue.push_back(drv_path.to_string());
while let Some(current_path) = queue.pop_front() {
if visited.contains(&current_path) {
continue;
}
visited.insert(current_path.clone());
input_srcs.insert(current_path.clone());
if !current_path.ends_with(".drv") {
continue;
}
let content = std::fs::read_to_string(&current_path)
.map_err(|e| format!("failed to read derivation {}: {}", current_path, e))?;
let inputs = parse_derivation_inputs(&content)
.ok_or_else(|| format!("failed to parse derivation {}", current_path))?;
for src in inputs.input_srcs {
input_srcs.insert(src.clone());
if !visited.contains(&src) {
queue.push_back(src);
}
}
for (dep_drv, outputs) in inputs.input_drvs {
input_srcs.insert(dep_drv.clone());
let entry = input_drvs.entry(dep_drv.clone()).or_default();
for output in outputs {
entry.insert(output);
}
if !visited.contains(&dep_drv) {
queue.push_back(dep_drv);
}
}
}
Ok(())
}
struct DerivationInputs {
input_drvs: Vec<(String, Vec<String>)>,
input_srcs: Vec<String>,
}
fn parse_derivation_inputs(aterm: &str) -> Option<DerivationInputs> {
let aterm = aterm.strip_prefix("Derive([")?;
let mut bracket_count: i32 = 1;
let mut pos = 0;
let bytes = aterm.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
_ => {}
}
pos += 1;
}
if bracket_count != 0 {
return None;
}
let rest = &aterm[pos..];
let rest = rest.strip_prefix(",[")?;
let mut input_drvs = Vec::new();
let mut bracket_count: i32 = 1;
let mut start = 0;
pos = 0;
let bytes = rest.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
b'(' if bracket_count == 1 => {
start = pos;
}
b')' if bracket_count == 1 => {
let entry = &rest[start + 1..pos];
if let Some((drv_path, outputs)) = parse_input_drv_entry(entry) {
input_drvs.push((drv_path, outputs));
}
}
_ => {}
}
pos += 1;
}
let rest = &rest[pos..];
let rest = rest.strip_prefix(",[")?;
let mut input_srcs = Vec::new();
bracket_count = 1;
pos = 0;
let bytes = rest.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
b'"' if bracket_count == 1 => {
pos += 1;
let src_start = pos;
while pos < bytes.len() && bytes[pos] != b'"' {
if bytes[pos] == b'\\' && pos + 1 < bytes.len() {
pos += 2;
} else {
pos += 1;
}
}
let src = std::str::from_utf8(&bytes[src_start..pos]).ok()?;
input_srcs.push(src.to_string());
}
_ => {}
}
pos += 1;
}
Some(DerivationInputs {
input_drvs,
input_srcs,
})
}
fn parse_input_drv_entry(entry: &str) -> Option<(String, Vec<String>)> {
let entry = entry.strip_prefix('"')?;
let quote_end = entry.find('"')?;
let drv_path = entry[..quote_end].to_string();
let rest = &entry[quote_end + 1..];
let rest = rest.strip_prefix(",[")?;
let rest = rest.strip_suffix(']')?;
let mut outputs = Vec::new();
for part in rest.split(',') {
let part = part.trim();
if let Some(name) = part.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
outputs.push(name.to_string());
}
}
Some((drv_path, outputs))
}
+372
View File
@@ -0,0 +1,372 @@
use core::fmt::{Debug, Display, Formatter, Result as FmtResult};
use core::hash::Hash;
use core::ops::Deref;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::ops::DerefMut;
use derive_more::{Constructor, IsVariant, Unwrap};
/// Represents a Nix symbol, which is used as a key in attribute sets.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Constructor)]
pub struct Symbol<'a>(Cow<'a, str>);
pub type StaticSymbol = Symbol<'static>;
impl From<String> for Symbol<'_> {
fn from(value: String) -> Self {
Symbol(Cow::Owned(value))
}
}
impl<'a> From<&'a str> for Symbol<'a> {
fn from(value: &'a str) -> Self {
Symbol(Cow::Borrowed(value))
}
}
/// Formats a string slice as a Nix symbol, quoting it if necessary.
pub fn format_symbol<'a>(sym: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
let sym = sym.into();
if Symbol::NORMAL_REGEX.test(&sym) {
sym
} else {
Cow::Owned(escape_quote_string(&sym))
}
}
impl Display for Symbol<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if self.normal() {
write!(f, "{}", self.0)
} else {
write!(f, "{}", escape_quote_string(&self.0))
}
}
}
impl Symbol<'_> {
const NORMAL_REGEX: ere::Regex<1> = ere::compile_regex!("^[a-zA-Z_][a-zA-Z0-9_'-]*$");
/// Checks if the symbol is a "normal" identifier that doesn't require quotes.
fn normal(&self) -> bool {
Self::NORMAL_REGEX.test(self)
}
}
impl Deref for Symbol<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// Represents a Nix attribute set, which is a map from symbols to values.
#[derive(Constructor, Default, Clone, PartialEq)]
pub struct AttrSet {
data: BTreeMap<StaticSymbol, Value>,
}
impl AttrSet {
/// Gets a value by key (string or Symbol).
pub fn get<'a, 'sym: 'a>(&'a self, key: impl Into<Symbol<'sym>>) -> Option<&'a Value> {
self.data.get(&key.into())
}
/// Checks if a key exists in the attribute set.
pub fn contains_key<'a, 'sym: 'a>(&'a self, key: impl Into<Symbol<'sym>>) -> bool {
self.data.contains_key(&key.into())
}
}
impl Deref for AttrSet {
type Target = BTreeMap<StaticSymbol, Value>;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl DerefMut for AttrSet {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}
impl Debug for AttrSet {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
use Value::*;
write!(f, "{{")?;
for (k, v) in self.data.iter() {
write!(f, " {k:?} = ")?;
match v {
List(_) => write!(f, "[ ... ];")?,
AttrSet(_) => write!(f, "{{ ... }};")?,
v => write!(f, "{v:?};")?,
}
}
write!(f, " }}")
}
}
impl Display for AttrSet {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
use Value::*;
if self.data.len() > 1 {
writeln!(f, "{{")?;
for (k, v) in self.data.iter() {
write!(f, " {k} = ")?;
match v {
List(_) => writeln!(f, "[ ... ];")?,
AttrSet(_) => writeln!(f, "{{ ... }};")?,
v => writeln!(f, "{v};")?,
}
}
write!(f, "}}")
} else {
write!(f, "{{")?;
for (k, v) in self.data.iter() {
write!(f, " {k} = ")?;
match v {
List(_) => write!(f, "[ ... ];")?,
AttrSet(_) => write!(f, "{{ ... }};")?,
v => write!(f, "{v};")?,
}
}
write!(f, " }}")
}
}
}
impl AttrSet {
pub fn display_compat(&self) -> AttrSetCompatDisplay<'_> {
AttrSetCompatDisplay(self)
}
}
pub struct AttrSetCompatDisplay<'a>(&'a AttrSet);
impl Display for AttrSetCompatDisplay<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{{")?;
for (k, v) in self.0.data.iter() {
write!(f, " {k} = {};", v.display_compat())?;
}
write!(f, " }}")
}
}
/// Represents a Nix list, which is a vector of values.
#[derive(Constructor, Default, Clone, Debug, PartialEq)]
pub struct List {
data: Vec<Value>,
}
impl Deref for List {
type Target = Vec<Value>;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl DerefMut for List {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}
impl Display for List {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
use Value::*;
if self.data.len() > 1 {
writeln!(f, "[")?;
for v in self.data.iter() {
match v {
List(_) => writeln!(f, " [ ... ]")?,
AttrSet(_) => writeln!(f, " {{ ... }}")?,
v => writeln!(f, " {v}")?,
}
}
write!(f, "]")
} else {
write!(f, "[ ")?;
for v in self.data.iter() {
match v {
List(_) => write!(f, "[ ... ] ")?,
AttrSet(_) => write!(f, "{{ ... }} ")?,
v => write!(f, "{v} ")?,
}
}
write!(f, "]")
}
}
}
impl List {
pub fn display_compat(&self) -> ListCompatDisplay<'_> {
ListCompatDisplay(self)
}
}
pub struct ListCompatDisplay<'a>(&'a List);
impl Display for ListCompatDisplay<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "[ ")?;
for v in self.0.data.iter() {
write!(f, "{} ", v.display_compat())?;
}
write!(f, "]")
}
}
/// Represents any possible Nix value that can be returned from an evaluation.
#[derive(IsVariant, Unwrap, Clone, Debug, PartialEq)]
pub enum Value {
/// An integer value.
Int(i64),
/// An floating-point value.
Float(f64),
/// An boolean value.
Bool(bool),
/// An null value.
Null,
/// A string value.
String(String),
/// A path value (absolute path string).
Path(String),
/// An attribute set.
AttrSet(AttrSet),
/// A list.
List(List),
/// A thunk, representing a delayed computation.
Thunk,
/// A function (lambda).
Func,
/// A primitive (built-in) operation.
PrimOp(String),
/// A partially applied primitive operation.
PrimOpApp(String),
/// A marker for a value that has been seen before during serialization, to break cycles.
/// This is used to prevent infinite recursion when printing or serializing cyclic data structures.
Repeated,
}
fn escape_quote_string(s: &str) -> String {
let mut ret = String::with_capacity(s.len() + 2);
ret.push('"');
let mut iter = s.chars().peekable();
while let Some(c) = iter.next() {
match c {
'\\' => ret.push_str("\\\\"),
'"' => ret.push_str("\\\""),
'\n' => ret.push_str("\\n"),
'\r' => ret.push_str("\\r"),
'\t' => ret.push_str("\\t"),
'$' if iter.peek() == Some(&'{') => ret.push_str("\\$"),
c => ret.push(c),
}
}
ret.push('"');
ret
}
/// Format a float matching C's `printf("%g", x)` with default precision 6.
fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult {
if !x.is_finite() {
return write!(f, "{x}");
}
if x == 0.0 {
return if x.is_sign_negative() {
write!(f, "-0")
} else {
write!(f, "0")
};
}
let precision: i32 = 6;
let exp = x.abs().log10().floor() as i32;
let formatted = if exp >= -4 && exp < precision {
let decimal_places = (precision - 1 - exp) as usize;
format!("{x:.decimal_places$}")
} else {
let sig_digits = (precision - 1) as usize;
let s = format!("{x:.sig_digits$e}");
let (mantissa, exp_part) = s
.split_once('e')
.expect("scientific notation must contain 'e'");
let (sign, digits) = if let Some(d) = exp_part.strip_prefix('-') {
("-", d)
} else if let Some(d) = exp_part.strip_prefix('+') {
("+", d)
} else {
("+", exp_part)
};
if digits.len() < 2 {
format!("{mantissa}e{sign}0{digits}")
} else {
format!("{mantissa}e{sign}{digits}")
}
};
if formatted.contains('.') {
if let Some(e_pos) = formatted.find('e') {
let trimmed = formatted[..e_pos]
.trim_end_matches('0')
.trim_end_matches('.');
write!(f, "{}{}", trimmed, &formatted[e_pos..])
} else {
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
write!(f, "{trimmed}")
}
} else {
write!(f, "{formatted}")
}
}
impl Display for Value {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
use Value::*;
match self {
&Int(x) => write!(f, "{x}"),
&Float(x) => fmt_nix_float(f, x),
&Bool(x) => write!(f, "{x}"),
Null => write!(f, "null"),
String(x) => write!(f, "{}", escape_quote_string(x)),
Path(x) => write!(f, "{x}"),
AttrSet(x) => write!(f, "{x}"),
List(x) => write!(f, "{x}"),
Thunk => write!(f, "«code»"),
Func => write!(f, "«lambda»"),
PrimOp(name) => write!(f, "«primop {name}»"),
PrimOpApp(name) => write!(f, "«partially applied primop {name}»"),
Repeated => write!(f, "«repeated»"),
}
}
}
impl Value {
pub fn display_compat(&self) -> ValueCompatDisplay<'_> {
ValueCompatDisplay(self)
}
}
pub struct ValueCompatDisplay<'a>(&'a Value);
impl Display for ValueCompatDisplay<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
use Value::*;
match self.0 {
&Int(x) => write!(f, "{x}"),
&Float(x) => fmt_nix_float(f, x),
&Bool(x) => write!(f, "{x}"),
Null => write!(f, "null"),
String(x) => write!(f, "{}", escape_quote_string(x)),
Path(x) => write!(f, "{x}"),
AttrSet(x) => write!(f, "{}", x.display_compat()),
List(x) => write!(f, "{}", x.display_compat()),
Thunk => write!(f, "«thunk»"),
Func => write!(f, "<LAMBDA>"),
PrimOp(_) => write!(f, "<PRIMOP>"),
PrimOpApp(_) => write!(f, "<PRIMOP-APP>"),
Repeated => write!(f, "«repeated»"),
}
}
}