diff --git a/crates/nu-cmd-extra/src/extra/bits/into.rs b/crates/nu-cmd-extra/src/extra/bits/into.rs index 7edc8d3584..3452c1eeee 100644 --- a/crates/nu-cmd-extra/src/extra/bits/into.rs +++ b/crates/nu-cmd-extra/src/extra/bits/into.rs @@ -203,7 +203,7 @@ pub fn action(input: &Value, _args: &Arguments, span: Span) -> Value { Value::string(raw_string.trim(), span) } Value::Int { val, .. } => convert_to_smallest_number_type(*val, span), - Value::Filesize { val, .. } => convert_to_smallest_number_type(*val, span), + Value::Filesize { val, .. } => convert_to_smallest_number_type(val.get(), span), Value::Duration { val, .. } => convert_to_smallest_number_type(*val, span), Value::String { val, .. } => { let raw_bytes = val.as_bytes(); diff --git a/crates/nu-cmd-extra/src/extra/conversions/fmt.rs b/crates/nu-cmd-extra/src/extra/conversions/fmt.rs index 15ca742c76..5bf2ae47a4 100644 --- a/crates/nu-cmd-extra/src/extra/conversions/fmt.rs +++ b/crates/nu-cmd-extra/src/extra/conversions/fmt.rs @@ -66,7 +66,7 @@ fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value { match input { Value::Float { val, .. } => fmt_it_64(*val, span), Value::Int { val, .. } => fmt_it(*val, span), - Value::Filesize { val, .. } => fmt_it(*val, span), + Value::Filesize { val, .. } => fmt_it(val.get(), span), // Propagate errors by explicitly matching them before the final case. Value::Error { .. } => input.clone(), other => Value::error( diff --git a/crates/nu-command/src/charting/hashable_value.rs b/crates/nu-command/src/charting/hashable_value.rs index b74b5f7761..636f5b7a05 100644 --- a/crates/nu-command/src/charting/hashable_value.rs +++ b/crates/nu-command/src/charting/hashable_value.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, FixedOffset}; -use nu_protocol::{ShellError, Span, Value}; +use nu_protocol::{Filesize, ShellError, Span, Value}; use std::hash::{Hash, Hasher}; /// A subset of [`Value`], which is hashable. @@ -30,7 +30,7 @@ pub enum HashableValue { span: Span, }, Filesize { - val: i64, + val: Filesize, span: Span, }, Duration { @@ -198,7 +198,10 @@ mod test { (Value::int(1, span), HashableValue::Int { val: 1, span }), ( Value::filesize(1, span), - HashableValue::Filesize { val: 1, span }, + HashableValue::Filesize { + val: 1.into(), + span, + }, ), ( Value::duration(1, span), diff --git a/crates/nu-command/src/conversions/fill.rs b/crates/nu-command/src/conversions/fill.rs index d125ee29b2..65d6b20b22 100644 --- a/crates/nu-command/src/conversions/fill.rs +++ b/crates/nu-command/src/conversions/fill.rs @@ -167,7 +167,7 @@ fn fill( fn action(input: &Value, args: &Arguments, span: Span) -> Value { match input { Value::Int { val, .. } => fill_int(*val, args, span), - Value::Filesize { val, .. } => fill_int(*val, args, span), + Value::Filesize { val, .. } => fill_int(val.get(), args, span), Value::Float { val, .. } => fill_float(*val, args, span), Value::String { val, .. } => fill_string(val, args, span), // Propagate errors by explicitly matching them before the final case. diff --git a/crates/nu-command/src/conversions/into/binary.rs b/crates/nu-command/src/conversions/into/binary.rs index d82f911506..fb549e50a7 100644 --- a/crates/nu-command/src/conversions/into/binary.rs +++ b/crates/nu-command/src/conversions/into/binary.rs @@ -147,7 +147,7 @@ pub fn action(input: &Value, _args: &Arguments, span: Span) -> Value { Value::Binary { .. } => input.clone(), Value::Int { val, .. } => Value::binary(val.to_ne_bytes().to_vec(), span), Value::Float { val, .. } => Value::binary(val.to_ne_bytes().to_vec(), span), - Value::Filesize { val, .. } => Value::binary(val.to_ne_bytes().to_vec(), span), + Value::Filesize { val, .. } => Value::binary(val.get().to_ne_bytes().to_vec(), span), Value::String { val, .. } => Value::binary(val.as_bytes().to_vec(), span), Value::Bool { val, .. } => Value::binary(i64::from(*val).to_ne_bytes().to_vec(), span), Value::Duration { val, .. } => Value::binary(val.to_ne_bytes().to_vec(), span), diff --git a/crates/nu-command/src/conversions/into/int.rs b/crates/nu-command/src/conversions/into/int.rs index f55770e7ff..67cba23090 100644 --- a/crates/nu-command/src/conversions/into/int.rs +++ b/crates/nu-command/src/conversions/into/int.rs @@ -253,7 +253,7 @@ fn action(input: &Value, args: &Arguments, span: Span) -> Value { convert_int(input, span, radix) } } - Value::Filesize { val, .. } => Value::int(*val, span), + Value::Filesize { val, .. } => Value::int(val.get(), span), Value::Float { val, .. } => Value::int( { if radix == 10 { diff --git a/crates/nu-command/src/database/values/sqlite.rs b/crates/nu-command/src/database/values/sqlite.rs index 5d0f7d1fe3..620ac95324 100644 --- a/crates/nu-command/src/database/values/sqlite.rs +++ b/crates/nu-command/src/database/values/sqlite.rs @@ -421,7 +421,7 @@ pub fn value_to_sql(value: Value) -> Result, ShellError Value::Bool { val, .. } => Box::new(val), Value::Int { val, .. } => Box::new(val), Value::Float { val, .. } => Box::new(val), - Value::Filesize { val, .. } => Box::new(val), + Value::Filesize { val, .. } => Box::new(val.get()), Value::Duration { val, .. } => Box::new(val), Value::Date { val, .. } => Box::new(val), Value::String { val, .. } => Box::new(val), diff --git a/crates/nu-command/src/formats/to/json.rs b/crates/nu-command/src/formats/to/json.rs index 9e68961092..27c0af856a 100644 --- a/crates/nu-command/src/formats/to/json.rs +++ b/crates/nu-command/src/formats/to/json.rs @@ -109,7 +109,7 @@ pub fn value_to_json_value(v: &Value) -> Result { let span = v.span(); Ok(match v { Value::Bool { val, .. } => nu_json::Value::Bool(*val), - Value::Filesize { val, .. } => nu_json::Value::I64(*val), + Value::Filesize { val, .. } => nu_json::Value::I64(val.get()), Value::Duration { val, .. } => nu_json::Value::I64(*val), Value::Date { val, .. } => nu_json::Value::String(val.to_string()), Value::Float { val, .. } => nu_json::Value::F64(*val), diff --git a/crates/nu-command/src/formats/to/msgpack.rs b/crates/nu-command/src/formats/to/msgpack.rs index 537aa9efdb..f7c4c7df3d 100644 --- a/crates/nu-command/src/formats/to/msgpack.rs +++ b/crates/nu-command/src/formats/to/msgpack.rs @@ -168,7 +168,7 @@ pub(crate) fn write_value( mp::write_f64(out, *val).err_span(span)?; } Value::Filesize { val, .. } => { - mp::write_sint(out, *val).err_span(span)?; + mp::write_sint(out, val.get()).err_span(span)?; } Value::Duration { val, .. } => { mp::write_sint(out, *val).err_span(span)?; diff --git a/crates/nu-command/src/formats/to/toml.rs b/crates/nu-command/src/formats/to/toml.rs index 9e5728ef1e..5f28284904 100644 --- a/crates/nu-command/src/formats/to/toml.rs +++ b/crates/nu-command/src/formats/to/toml.rs @@ -47,7 +47,7 @@ fn helper(engine_state: &EngineState, v: &Value) -> Result toml::Value::Boolean(*val), Value::Int { val, .. } => toml::Value::Integer(*val), - Value::Filesize { val, .. } => toml::Value::Integer(*val), + Value::Filesize { val, .. } => toml::Value::Integer(val.get()), Value::Duration { val, .. } => toml::Value::String(val.to_string()), Value::Date { val, .. } => toml::Value::Datetime(to_toml_datetime(val)), Value::Range { .. } => toml::Value::String("".to_string()), diff --git a/crates/nu-command/src/formats/to/yaml.rs b/crates/nu-command/src/formats/to/yaml.rs index 73c743a056..4fc4e62319 100644 --- a/crates/nu-command/src/formats/to/yaml.rs +++ b/crates/nu-command/src/formats/to/yaml.rs @@ -44,7 +44,9 @@ pub fn value_to_yaml_value(v: &Value) -> Result { Ok(match &v { Value::Bool { val, .. } => serde_yaml::Value::Bool(*val), Value::Int { val, .. } => serde_yaml::Value::Number(serde_yaml::Number::from(*val)), - Value::Filesize { val, .. } => serde_yaml::Value::Number(serde_yaml::Number::from(*val)), + Value::Filesize { val, .. } => { + serde_yaml::Value::Number(serde_yaml::Number::from(val.get())) + } Value::Duration { val, .. } => serde_yaml::Value::String(val.to_string()), Value::Date { val, .. } => serde_yaml::Value::String(val.to_string()), Value::Range { .. } => serde_yaml::Value::Null, diff --git a/crates/nu-command/src/math/avg.rs b/crates/nu-command/src/math/avg.rs index 07e2ed00bf..a7f6d3088a 100644 --- a/crates/nu-command/src/math/avg.rs +++ b/crates/nu-command/src/math/avg.rs @@ -90,7 +90,7 @@ pub fn average(values: &[Value], span: Span, head: Span) -> Result Ok(Value::filesize(val / values.len() as i64, span)), + Value::Filesize { val, .. } => Ok(Value::filesize(val.get() / values.len() as i64, span)), Value::Duration { val, .. } => Ok(Value::duration(val / values.len() as i64, span)), _ => total.div(head, &Value::int(values.len() as i64, head), head), } diff --git a/crates/nu-command/src/math/mode.rs b/crates/nu-command/src/math/mode.rs index b89896ca88..cf4ae36b2d 100644 --- a/crates/nu-command/src/math/mode.rs +++ b/crates/nu-command/src/math/mode.rs @@ -142,9 +142,10 @@ pub fn mode(values: &[Value], _span: Span, head: Span) -> Result { Ok(HashableType::new(val.to_ne_bytes(), NumberTypes::Float)) } - Value::Filesize { val, .. } => { - Ok(HashableType::new(val.to_ne_bytes(), NumberTypes::Filesize)) - } + Value::Filesize { val, .. } => Ok(HashableType::new( + val.get().to_ne_bytes(), + NumberTypes::Filesize, + )), Value::Error { error, .. } => Err(*error.clone()), other => Err(ShellError::UnsupportedInput { msg: "Unable to give a result with this input".to_string(), diff --git a/crates/nu-command/src/random/binary.rs b/crates/nu-command/src/random/binary.rs index 539796b8c5..36d0179e3a 100644 --- a/crates/nu-command/src/random/binary.rs +++ b/crates/nu-command/src/random/binary.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; - +use nu_protocol::format_filesize_from_conf; use rand::{thread_rng, RngCore}; #[derive(Clone)] @@ -37,7 +37,27 @@ impl Command for SubCommand { call: &Call, _input: PipelineData, ) -> Result { - let length = call.req(engine_state, stack, 0)?; + let length_val = call.req(engine_state, stack, 0)?; + let length = match length_val { + Value::Int { val, .. } => usize::try_from(val).map_err(|_| ShellError::InvalidValue { + valid: "a non-negative int or filesize".into(), + actual: val.to_string(), + span: length_val.span(), + }), + Value::Filesize { val, .. } => { + usize::try_from(val).map_err(|_| ShellError::InvalidValue { + valid: "a non-negative int or filesize".into(), + actual: format_filesize_from_conf(val, engine_state.get_config()), + span: length_val.span(), + }) + } + val => Err(ShellError::RuntimeTypeMismatch { + expected: Type::custom("int or filesize"), + actual: val.get_type(), + span: val.span(), + }), + }?; + let mut rng = thread_rng(); let mut out = vec![0u8; length]; diff --git a/crates/nu-command/src/random/chars.rs b/crates/nu-command/src/random/chars.rs index d445a4aab8..e426fb5dbc 100644 --- a/crates/nu-command/src/random/chars.rs +++ b/crates/nu-command/src/random/chars.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; - +use nu_protocol::format_filesize_from_conf; use rand::{ distributions::{Alphanumeric, Distribution}, thread_rng, @@ -73,14 +73,36 @@ fn chars( call: &Call, ) -> Result { let span = call.head; - let length: Option = call.get_flag(engine_state, stack, "length")?; + let length: Option = call.get_flag(engine_state, stack, "length")?; + let length = if let Some(length_val) = length { + match length_val { + Value::Int { val, .. } => usize::try_from(val).map_err(|_| ShellError::InvalidValue { + valid: "a non-negative int or filesize".into(), + actual: val.to_string(), + span: length_val.span(), + }), + Value::Filesize { val, .. } => { + usize::try_from(val).map_err(|_| ShellError::InvalidValue { + valid: "a non-negative int or filesize".into(), + actual: format_filesize_from_conf(val, engine_state.get_config()), + span: length_val.span(), + }) + } + val => Err(ShellError::RuntimeTypeMismatch { + expected: Type::custom("int or filesize"), + actual: val.get_type(), + span: val.span(), + }), + }? + } else { + DEFAULT_CHARS_LENGTH + }; - let chars_length = length.unwrap_or(DEFAULT_CHARS_LENGTH); let mut rng = thread_rng(); let random_string = Alphanumeric .sample_iter(&mut rng) - .take(chars_length) + .take(length) .map(char::from) .collect::(); diff --git a/crates/nu-command/tests/format_conversions/nuon.rs b/crates/nu-command/tests/format_conversions/nuon.rs index 8262ee8dfc..e4f6281c01 100644 --- a/crates/nu-command/tests/format_conversions/nuon.rs +++ b/crates/nu-command/tests/format_conversions/nuon.rs @@ -5,9 +5,9 @@ fn to_nuon_correct_compaction() { let actual = nu!( cwd: "tests/fixtures/formats", pipeline( r#" - open appveyor.yml - | to nuon - | str length + open appveyor.yml + | to nuon + | str length | $in > 500 "# )); diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index b6f5fa161e..b2b5756ef0 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -13,9 +13,9 @@ use itertools::Itertools; use log::trace; use nu_engine::DIR_VAR_PARSER_INFO; use nu_protocol::{ - ast::*, engine::StateWorkingSet, eval_const::eval_constant, BlockId, DeclId, DidYouMean, Flag, - ParseError, PositionalArg, Signature, Span, Spanned, SyntaxShape, Type, VarId, ENV_VARIABLE_ID, - IN_VARIABLE_ID, + ast::*, engine::StateWorkingSet, eval_const::eval_constant, BlockId, DeclId, DidYouMean, + FilesizeUnit, Flag, ParseError, PositionalArg, Signature, Span, Spanned, SyntaxShape, Type, + VarId, ENV_VARIABLE_ID, IN_VARIABLE_ID, }; use std::{ collections::{HashMap, HashSet}, @@ -2575,19 +2575,67 @@ pub fn parse_unit_value<'res>( } pub const FILESIZE_UNIT_GROUPS: &[UnitGroup] = &[ - (Unit::Kilobyte, "KB", Some((Unit::Byte, 1000))), - (Unit::Megabyte, "MB", Some((Unit::Kilobyte, 1000))), - (Unit::Gigabyte, "GB", Some((Unit::Megabyte, 1000))), - (Unit::Terabyte, "TB", Some((Unit::Gigabyte, 1000))), - (Unit::Petabyte, "PB", Some((Unit::Terabyte, 1000))), - (Unit::Exabyte, "EB", Some((Unit::Petabyte, 1000))), - (Unit::Kibibyte, "KIB", Some((Unit::Byte, 1024))), - (Unit::Mebibyte, "MIB", Some((Unit::Kibibyte, 1024))), - (Unit::Gibibyte, "GIB", Some((Unit::Mebibyte, 1024))), - (Unit::Tebibyte, "TIB", Some((Unit::Gibibyte, 1024))), - (Unit::Pebibyte, "PIB", Some((Unit::Tebibyte, 1024))), - (Unit::Exbibyte, "EIB", Some((Unit::Pebibyte, 1024))), - (Unit::Byte, "B", None), + ( + Unit::Filesize(FilesizeUnit::KB), + "KB", + Some((Unit::Filesize(FilesizeUnit::B), 1000)), + ), + ( + Unit::Filesize(FilesizeUnit::MB), + "MB", + Some((Unit::Filesize(FilesizeUnit::KB), 1000)), + ), + ( + Unit::Filesize(FilesizeUnit::GB), + "GB", + Some((Unit::Filesize(FilesizeUnit::MB), 1000)), + ), + ( + Unit::Filesize(FilesizeUnit::TB), + "TB", + Some((Unit::Filesize(FilesizeUnit::GB), 1000)), + ), + ( + Unit::Filesize(FilesizeUnit::PB), + "PB", + Some((Unit::Filesize(FilesizeUnit::TB), 1000)), + ), + ( + Unit::Filesize(FilesizeUnit::EB), + "EB", + Some((Unit::Filesize(FilesizeUnit::PB), 1000)), + ), + ( + Unit::Filesize(FilesizeUnit::KiB), + "KIB", + Some((Unit::Filesize(FilesizeUnit::B), 1024)), + ), + ( + Unit::Filesize(FilesizeUnit::MiB), + "MIB", + Some((Unit::Filesize(FilesizeUnit::KiB), 1024)), + ), + ( + Unit::Filesize(FilesizeUnit::GiB), + "GIB", + Some((Unit::Filesize(FilesizeUnit::MiB), 1024)), + ), + ( + Unit::Filesize(FilesizeUnit::TiB), + "TIB", + Some((Unit::Filesize(FilesizeUnit::GiB), 1024)), + ), + ( + Unit::Filesize(FilesizeUnit::PiB), + "PIB", + Some((Unit::Filesize(FilesizeUnit::TiB), 1024)), + ), + ( + Unit::Filesize(FilesizeUnit::EiB), + "EIB", + Some((Unit::Filesize(FilesizeUnit::EiB), 1024)), + ), + (Unit::Filesize(FilesizeUnit::B), "B", None), ]; pub const DURATION_UNIT_GROUPS: &[UnitGroup] = &[ diff --git a/crates/nu-protocol/src/ast/unit.rs b/crates/nu-protocol/src/ast/unit.rs index 0e51d8978f..ecc92806a0 100644 --- a/crates/nu-protocol/src/ast/unit.rs +++ b/crates/nu-protocol/src/ast/unit.rs @@ -1,24 +1,9 @@ -use crate::{ShellError, Span, Value}; +use crate::{Filesize, FilesizeUnit, IntoValue, ShellError, Span, Value}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Unit { - // Filesize units: metric - Byte, - Kilobyte, - Megabyte, - Gigabyte, - Terabyte, - Petabyte, - Exabyte, - - // Filesize units: ISO/IEC 80000 - Kibibyte, - Mebibyte, - Gibibyte, - Tebibyte, - Pebibyte, - Exbibyte, + Filesize(FilesizeUnit), // Duration units Nanosecond, @@ -34,33 +19,19 @@ pub enum Unit { impl Unit { pub fn build_value(self, size: i64, span: Span) -> Result { match self { - Unit::Byte => Ok(Value::filesize(size, span)), - Unit::Kilobyte => Ok(Value::filesize(size * 1000, span)), - Unit::Megabyte => Ok(Value::filesize(size * 1000 * 1000, span)), - Unit::Gigabyte => Ok(Value::filesize(size * 1000 * 1000 * 1000, span)), - Unit::Terabyte => Ok(Value::filesize(size * 1000 * 1000 * 1000 * 1000, span)), - Unit::Petabyte => Ok(Value::filesize( - size * 1000 * 1000 * 1000 * 1000 * 1000, - span, - )), - Unit::Exabyte => Ok(Value::filesize( - size * 1000 * 1000 * 1000 * 1000 * 1000 * 1000, - span, - )), - - Unit::Kibibyte => Ok(Value::filesize(size * 1024, span)), - Unit::Mebibyte => Ok(Value::filesize(size * 1024 * 1024, span)), - Unit::Gibibyte => Ok(Value::filesize(size * 1024 * 1024 * 1024, span)), - Unit::Tebibyte => Ok(Value::filesize(size * 1024 * 1024 * 1024 * 1024, span)), - Unit::Pebibyte => Ok(Value::filesize( - size * 1024 * 1024 * 1024 * 1024 * 1024, - span, - )), - Unit::Exbibyte => Ok(Value::filesize( - size * 1024 * 1024 * 1024 * 1024 * 1024 * 1024, - span, - )), - + Unit::Filesize(unit) => { + if let Some(filesize) = Filesize::from_unit(size, unit) { + Ok(filesize.into_value(span)) + } else { + Err(ShellError::GenericError { + error: "filesize too large".into(), + msg: "filesize too large".into(), + span: Some(span), + help: None, + inner: vec![], + }) + } + } Unit::Nanosecond => Ok(Value::duration(size, span)), Unit::Microsecond => Ok(Value::duration(size * 1000, span)), Unit::Millisecond => Ok(Value::duration(size * 1000 * 1000, span)), diff --git a/crates/nu-protocol/src/ir/mod.rs b/crates/nu-protocol/src/ir/mod.rs index 428a2667e0..9d657ba080 100644 --- a/crates/nu-protocol/src/ir/mod.rs +++ b/crates/nu-protocol/src/ir/mod.rs @@ -1,13 +1,11 @@ -use std::{fmt, sync::Arc}; - use crate::{ ast::{CellPath, Expression, Operator, Pattern, RangeInclusion}, engine::EngineState, - BlockId, DeclId, RegId, Span, Value, VarId, + BlockId, DeclId, Filesize, RegId, Span, Value, VarId, }; - use chrono::{DateTime, FixedOffset}; use serde::{Deserialize, Serialize}; +use std::{fmt, sync::Arc}; mod call; mod display; @@ -397,7 +395,7 @@ pub enum Literal { Bool(bool), Int(i64), Float(f64), - Filesize(i64), + Filesize(Filesize), Duration(i64), Binary(DataSlice), Block(BlockId), diff --git a/crates/nu-protocol/src/value/filesize.rs b/crates/nu-protocol/src/value/filesize.rs index 9d58c103df..9e291db5bb 100644 --- a/crates/nu-protocol/src/value/filesize.rs +++ b/crates/nu-protocol/src/value/filesize.rs @@ -1,13 +1,442 @@ -use crate::Config; +use crate::{Config, FromValue, IntoValue, ShellError, Span, Type, Value}; use byte_unit::UnitType; use nu_utils::get_system_locale; use num_format::ToFormattedString; +use serde::{Deserialize, Serialize}; +use std::{ + fmt, + iter::Sum, + ops::{Add, Mul, Neg, Sub}, + str::FromStr, +}; +use thiserror::Error; -pub fn format_filesize_from_conf(num_bytes: i64, config: &Config) -> String { +/// A signed number of bytes. +/// +/// [`Filesize`] is a wrapper around [`i64`]. Whereas [`i64`] is a dimensionless value, [`Filesize`] represents a +/// numerical value with a dimensional unit (byte). +/// +/// A [`Filesize`] can be created from an [`i64`] using [`Filesize::new`] or the `From` or `Into` trait implementations. +/// To get the underlying [`i64`] value, use [`Filesize::get`] or the `From` or `Into` trait implementations. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[repr(transparent)] +#[serde(transparent)] +pub struct Filesize(i64); + +impl Filesize { + /// A [`Filesize`] of 0 bytes. + pub const ZERO: Self = Self(0); + + /// The smallest possible [`Filesize`] value. + pub const MIN: Self = Self(i64::MIN); + + /// The largest possible [`Filesize`] value. + pub const MAX: Self = Self(i64::MAX); + + /// Create a new [`Filesize`] from a [`i64`] number of bytes. + pub const fn new(bytes: i64) -> Self { + Self(bytes) + } + + /// Creates a [`Filesize`] from a signed multiple of a [`FilesizeUnit`]. + /// + /// If the resulting number of bytes calculated by `value * unit.as_bytes()` overflows an + /// [`i64`], then `None` is returned. + pub const fn from_unit(value: i64, unit: FilesizeUnit) -> Option { + if let Some(bytes) = value.checked_mul(unit.as_bytes() as i64) { + Some(Self(bytes)) + } else { + None + } + } + + /// Returns the underlying [`i64`] number of bytes in a [`Filesize`]. + pub const fn get(&self) -> i64 { + self.0 + } + + /// Returns true if a [`Filesize`] is positive and false if it is zero or negative. + pub const fn is_positive(self) -> bool { + self.0.is_positive() + } + + /// Returns true if a [`Filesize`] is negative and false if it is zero or positive. + pub const fn is_negative(self) -> bool { + self.0.is_negative() + } + + /// Returns a [`Filesize`] representing the sign of `self`. + /// - 0 if the filesize is zero + /// - 1 if the filesize is positive + /// - -1 if the filesize is negative + pub const fn signum(self) -> Self { + Self(self.0.signum()) + } +} + +impl From for Filesize { + fn from(value: i64) -> Self { + Self(value) + } +} + +impl From for i64 { + fn from(filesize: Filesize) -> Self { + filesize.0 + } +} + +macro_rules! impl_from { + ($($ty:ty),* $(,)?) => { + $( + impl From<$ty> for Filesize { + #[inline] + fn from(value: $ty) -> Self { + Self(value.into()) + } + } + + impl TryFrom for $ty { + type Error = >::Error; + + #[inline] + fn try_from(filesize: Filesize) -> Result { + filesize.0.try_into() + } + } + )* + }; +} + +impl_from!(u8, i8, u16, i16, u32, i32); + +macro_rules! impl_try_from { + ($($ty:ty),* $(,)?) => { + $( + impl TryFrom<$ty> for Filesize { + type Error = <$ty as TryInto>::Error; + + #[inline] + fn try_from(value: $ty) -> Result { + value.try_into().map(Self) + } + } + + impl TryFrom for $ty { + type Error = >::Error; + + #[inline] + fn try_from(filesize: Filesize) -> Result { + filesize.0.try_into() + } + } + )* + }; +} + +impl_try_from!(u64, usize, isize); + +/// The error type returned when a checked conversion from a floating point type fails. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)] +pub struct TryFromFloatError(()); + +impl fmt::Display for TryFromFloatError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "out of range float type conversion attempted") + } +} + +impl TryFrom for Filesize { + type Error = TryFromFloatError; + + #[inline] + fn try_from(value: f64) -> Result { + if i64::MIN as f64 <= value && value <= i64::MAX as f64 { + Ok(Self(value as i64)) + } else { + Err(TryFromFloatError(())) + } + } +} + +impl TryFrom for Filesize { + type Error = TryFromFloatError; + + #[inline] + fn try_from(value: f32) -> Result { + if i64::MIN as f32 <= value && value <= i64::MAX as f32 { + Ok(Self(value as i64)) + } else { + Err(TryFromFloatError(())) + } + } +} + +impl FromValue for Filesize { + fn from_value(value: Value) -> Result { + value.as_filesize() + } + + fn expected_type() -> Type { + Type::Filesize + } +} + +impl IntoValue for Filesize { + fn into_value(self, span: Span) -> Value { + Value::filesize(self.0, span) + } +} + +impl Add for Filesize { + type Output = Option; + + fn add(self, rhs: Self) -> Self::Output { + self.0.checked_add(rhs.0).map(Self) + } +} + +impl Sub for Filesize { + type Output = Option; + + fn sub(self, rhs: Self) -> Self::Output { + self.0.checked_sub(rhs.0).map(Self) + } +} + +impl Mul for Filesize { + type Output = Option; + + fn mul(self, rhs: i64) -> Self::Output { + self.0.checked_mul(rhs).map(Self) + } +} + +impl Mul for i64 { + type Output = Option; + + fn mul(self, rhs: Filesize) -> Self::Output { + self.checked_mul(rhs.0).map(Filesize::new) + } +} + +impl Mul for Filesize { + type Output = Option; + + fn mul(self, rhs: f64) -> Self::Output { + let bytes = ((self.0 as f64) * rhs).round(); + if i64::MIN as f64 <= bytes && bytes <= i64::MAX as f64 { + Some(Self(bytes as i64)) + } else { + None + } + } +} + +impl Mul for f64 { + type Output = Option; + + fn mul(self, rhs: Filesize) -> Self::Output { + let bytes = (self * (rhs.0 as f64)).round(); + if i64::MIN as f64 <= bytes && bytes <= i64::MAX as f64 { + Some(Filesize(bytes as i64)) + } else { + None + } + } +} + +impl Neg for Filesize { + type Output = Option; + + fn neg(self) -> Self::Output { + self.0.checked_neg().map(Self) + } +} + +impl Sum for Option { + fn sum>(iter: I) -> Self { + let mut sum = Filesize::ZERO; + for filesize in iter { + sum = (sum + filesize)?; + } + Some(sum) + } +} + +impl fmt::Display for Filesize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + format_filesize(*self, "auto", Some(false)).fmt(f) + } +} + +/// All the possible filesize units for a [`Filesize`]. +/// +/// This type contains both units with metric (SI) decimal prefixes which are powers of 10 (e.g., kB = 1000 bytes) +/// and units with binary prefixes which are powers of 2 (e.g., KiB = 1024 bytes). +/// +/// The number of bytes in a [`FilesizeUnit`] can be obtained using +/// [`as_bytes`](FilesizeUnit::as_bytes). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum FilesizeUnit { + /// One byte + B, + /// Kilobyte = 1000 bytes + KB, + /// Megabyte = 106 bytes + MB, + /// Gigabyte = 109 bytes + GB, + /// Terabyte = 1012 bytes + TB, + /// Petabyte = 1015 bytes + PB, + /// Exabyte = 1018 bytes + EB, + /// Kibibyte = 1024 bytes + KiB, + /// Mebibyte = 220 bytes + MiB, + /// Gibibyte = 230 bytes + GiB, + /// Tebibyte = 240 bytes + TiB, + /// Pebibyte = 250 bytes + PiB, + /// Exbibyte = 260 bytes + EiB, +} + +impl FilesizeUnit { + /// Returns the number of bytes in a [`FilesizeUnit`]. + pub const fn as_bytes(&self) -> u64 { + match self { + Self::B => 1, + Self::KB => 10_u64.pow(3), + Self::MB => 10_u64.pow(6), + Self::GB => 10_u64.pow(9), + Self::TB => 10_u64.pow(12), + Self::PB => 10_u64.pow(15), + Self::EB => 10_u64.pow(18), + Self::KiB => 2_u64.pow(10), + Self::MiB => 2_u64.pow(20), + Self::GiB => 2_u64.pow(30), + Self::TiB => 2_u64.pow(40), + Self::PiB => 2_u64.pow(50), + Self::EiB => 2_u64.pow(60), + } + } + + /// Convert a [`FilesizeUnit`] to a [`Filesize`]. + /// + /// To create a [`Filesize`] from a multiple of a [`FilesizeUnit`] use [`Filesize::from_unit`]. + pub const fn as_filesize(&self) -> Filesize { + Filesize::new(self.as_bytes() as i64) + } + + /// Returns the abbreviated unit for a [`FilesizeUnit`] as a [`str`]. + /// + /// The abbreviated unit is exactly the same as the enum case name in Rust code. + /// + /// # Examples + /// ``` + /// # use nu_protocol::FilesizeUnit; + /// assert_eq!(FilesizeUnit::B.as_str(), "B"); + /// assert_eq!(FilesizeUnit::KB.as_str(), "kB"); + /// assert_eq!(FilesizeUnit::KiB.as_str(), "KiB"); + /// ``` + pub const fn as_str(&self) -> &'static str { + match self { + Self::B => "B", + Self::KB => "kB", + Self::MB => "MB", + Self::GB => "GB", + Self::TB => "TB", + Self::PB => "PB", + Self::EB => "EB", + Self::KiB => "KiB", + Self::MiB => "MiB", + Self::GiB => "GiB", + Self::TiB => "TiB", + Self::PiB => "PiB", + Self::EiB => "EiB", + } + } + + /// Returns `true` if a [`FilesizeUnit`] has a metric (SI) decimal prefix (a power of 10). + /// + /// Note that this returns `true` for [`FilesizeUnit::B`] as well. + pub const fn is_decimal(&self) -> bool { + match self { + Self::B | Self::KB | Self::MB | Self::GB | Self::TB | Self::PB | Self::EB => true, + Self::KiB | Self::MiB | Self::GiB | Self::TiB | Self::PiB | Self::EiB => false, + } + } + + /// Returns `true` if a [`FilesizeUnit`] has a binary prefix (a power of 2). + /// + /// Note that this returns `true` for [`FilesizeUnit::B`] as well. + pub const fn is_binary(&self) -> bool { + match self { + Self::KB | Self::MB | Self::GB | Self::TB | Self::PB | Self::EB => false, + Self::B | Self::KiB | Self::MiB | Self::GiB | Self::TiB | Self::PiB | Self::EiB => true, + } + } +} + +impl From for Filesize { + fn from(unit: FilesizeUnit) -> Self { + unit.as_filesize() + } +} + +/// An error returned when failing to parse a [`FilesizeUnit`]. +/// +/// This occurs when the string being parsed does not exactly match the name of one of the +/// enum cases in [`FilesizeUnit`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)] +pub struct ParseFilesizeUnitError(()); + +impl fmt::Display for ParseFilesizeUnitError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "invalid filesize unit") + } +} + +impl FromStr for FilesizeUnit { + type Err = ParseFilesizeUnitError; + + fn from_str(s: &str) -> Result { + Ok(match s { + "B" => Self::B, + "kB" => Self::KB, + "MB" => Self::MB, + "GB" => Self::GB, + "TB" => Self::TB, + "PB" => Self::PB, + "EB" => Self::EB, + "KiB" => Self::KiB, + "MiB" => Self::MiB, + "GiB" => Self::GiB, + "TiB" => Self::TiB, + "PiB" => Self::PiB, + "EiB" => Self::EiB, + _ => return Err(ParseFilesizeUnitError(())), + }) + } +} + +impl fmt::Display for FilesizeUnit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +pub fn format_filesize_from_conf(filesize: Filesize, config: &Config) -> String { // We need to take into account config.filesize_metric so, if someone asks for KB // and filesize_metric is false, return KiB format_filesize( - num_bytes, + filesize, &config.filesize.format, Some(config.filesize.metric), ) @@ -16,7 +445,7 @@ pub fn format_filesize_from_conf(num_bytes: i64, config: &Config) -> String { // filesize_metric is explicit when printed a value according to user config; // other places (such as `format filesize`) don't. pub fn format_filesize( - num_bytes: i64, + filesize: Filesize, format_value: &str, filesize_metric: Option, ) -> String { @@ -25,7 +454,7 @@ pub fn format_filesize( // When format_value is "auto" or an invalid value, the returned ByteUnit doesn't matter // and is always B. let filesize_unit = get_filesize_format(format_value, filesize_metric); - let byte = byte_unit::Byte::from_u64(num_bytes.unsigned_abs()); + let byte = byte_unit::Byte::from_u64(filesize.0.unsigned_abs()); let adj_byte = if let Some(unit) = filesize_unit { byte.get_adjusted_unit(unit) } else { @@ -43,7 +472,7 @@ pub fn format_filesize( let locale = get_system_locale(); let locale_byte = adj_byte.get_value() as u64; let locale_byte_string = locale_byte.to_formatted_string(&locale); - let locale_signed_byte_string = if num_bytes.is_negative() { + let locale_signed_byte_string = if filesize.is_negative() { format!("-{locale_byte_string}") } else { locale_byte_string @@ -56,7 +485,7 @@ pub fn format_filesize( } } _ => { - if num_bytes.is_negative() { + if filesize.is_negative() { format!("-{:.1}", adj_byte) } else { format!("{:.1}", adj_byte) @@ -116,6 +545,9 @@ mod tests { #[case] filesize_format: String, #[case] exp: &str, ) { - assert_eq!(exp, format_filesize(val, &filesize_format, filesize_metric)); + assert_eq!( + exp, + format_filesize(Filesize::new(val), &filesize_format, filesize_metric) + ); } } diff --git a/crates/nu-protocol/src/value/from_value.rs b/crates/nu-protocol/src/value/from_value.rs index 624d7dae39..fb6fc26be5 100644 --- a/crates/nu-protocol/src/value/from_value.rs +++ b/crates/nu-protocol/src/value/from_value.rs @@ -252,9 +252,7 @@ impl FromValue for i64 { fn from_value(v: Value) -> Result { match v { Value::Int { val, .. } => Ok(val), - Value::Filesize { val, .. } => Ok(val), Value::Duration { val, .. } => Ok(val), - v => Err(ShellError::CantConvert { to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), @@ -308,9 +306,7 @@ macro_rules! impl_from_value_for_uint { let span = v.span(); const MAX: i64 = $max; match v { - Value::Int { val, .. } - | Value::Filesize { val, .. } - | Value::Duration { val, .. } => { + Value::Int { val, .. } | Value::Duration { val, .. } => { match val { i64::MIN..=-1 => Err(ShellError::NeedsPositiveValue { span }), 0..=MAX => Ok(val as $type), diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 28b468a151..c12dd3d71d 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -83,7 +83,7 @@ pub enum Value { internal_span: Span, }, Filesize { - val: i64, + val: Filesize, // note: spans are being refactored out of Value // please use .span() instead of matching this span value #[serde(rename = "span")] @@ -301,7 +301,7 @@ impl Value { } /// Returns the inner `i64` filesize value or an error if this `Value` is not a filesize - pub fn as_filesize(&self) -> Result { + pub fn as_filesize(&self) -> Result { if let Value::Filesize { val, .. } = self { Ok(*val) } else { @@ -1819,9 +1819,9 @@ impl Value { } } - pub fn filesize(val: i64, span: Span) -> Value { + pub fn filesize(val: impl Into, span: Span) -> Value { Value::Filesize { - val, + val: val.into(), internal_span: span, } } @@ -1938,7 +1938,7 @@ impl Value { /// Note: Only use this for test data, *not* live data, as it will point into unknown source /// when used in errors. - pub fn test_filesize(val: i64) -> Value { + pub fn test_filesize(val: impl Into) -> Value { Value::filesize(val, Span::test_data()) } @@ -2478,7 +2478,7 @@ impl Value { } } (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - if let Some(val) = lhs.checked_add(*rhs) { + if let Some(val) = *lhs + *rhs { Ok(Value::filesize(val, span)) } else { Err(ShellError::OperatorOverflow { @@ -2585,7 +2585,7 @@ impl Value { } } (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - if let Some(val) = lhs.checked_sub(*rhs) { + if let Some(val) = *lhs - *rhs { Ok(Value::filesize(val, span)) } else { Err(ShellError::OperatorOverflow { @@ -2633,16 +2633,48 @@ impl Value { Ok(Value::float(lhs * rhs, span)) } (Value::Int { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - Ok(Value::filesize(*lhs * *rhs, span)) + if let Some(val) = *lhs * *rhs { + Ok(Value::filesize(val, span)) + } else { + Err(ShellError::OperatorOverflow { + msg: "multiply operation overflowed".into(), + span, + help: None, + }) + } } (Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => { - Ok(Value::filesize(*lhs * *rhs, span)) + if let Some(val) = *lhs * *rhs { + Ok(Value::filesize(val, span)) + } else { + Err(ShellError::OperatorOverflow { + msg: "multiply operation overflowed".into(), + span, + help: None, + }) + } } (Value::Float { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - Ok(Value::filesize((*lhs * *rhs as f64) as i64, span)) + if let Some(val) = *lhs * *rhs { + Ok(Value::filesize(val, span)) + } else { + Err(ShellError::OperatorOverflow { + msg: "multiply operation overflowed".into(), + span, + help: None, + }) + } } (Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => { - Ok(Value::filesize((*lhs as f64 * *rhs) as i64, span)) + if let Some(val) = *lhs * *rhs { + Ok(Value::filesize(val, span)) + } else { + Err(ShellError::OperatorOverflow { + msg: "multiply operation overflowed".into(), + span, + help: None, + }) + } } (Value::Int { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { Ok(Value::duration(*lhs * *rhs, span)) @@ -2700,14 +2732,14 @@ impl Value { } } (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - if *rhs == 0 { + if *rhs == Filesize::ZERO { Err(ShellError::DivisionByZero { span: op }) } else { - Ok(Value::float(*lhs as f64 / *rhs as f64, span)) + Ok(Value::float(lhs.get() as f64 / rhs.get() as f64, span)) } } (Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => { - if let Some(val) = lhs.checked_div(*rhs) { + if let Some(val) = lhs.get().checked_div(*rhs) { Ok(Value::filesize(val, span)) } else if *rhs == 0 { Err(ShellError::DivisionByZero { span: op }) @@ -2721,9 +2753,8 @@ impl Value { } (Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => { if *rhs != 0.0 { - let val = *lhs as f64 / rhs; - if i64::MIN as f64 <= val && val <= i64::MAX as f64 { - Ok(Value::filesize(val as i64, span)) + if let Ok(val) = Filesize::try_from(lhs.get() as f64 / rhs) { + Ok(Value::filesize(val, span)) } else { Err(ShellError::OperatorOverflow { msg: "division operation overflowed".into(), @@ -2785,163 +2816,6 @@ impl Value { } } - pub fn modulo(&self, op: Span, rhs: &Value, span: Span) -> Result { - // Based off the unstable `div_floor` function in the std library. - fn checked_mod_i64(dividend: i64, divisor: i64) -> Option { - let remainder = dividend.checked_rem(divisor)?; - if (remainder > 0 && divisor < 0) || (remainder < 0 && divisor > 0) { - // Note that `remainder + divisor` cannot overflow, because `remainder` and - // `divisor` have opposite signs. - Some(remainder + divisor) - } else { - Some(remainder) - } - } - - fn checked_mod_f64(dividend: f64, divisor: f64) -> Option { - if divisor == 0.0 { - None - } else { - let remainder = dividend % divisor; - if (remainder > 0.0 && divisor < 0.0) || (remainder < 0.0 && divisor > 0.0) { - Some(remainder + divisor) - } else { - Some(remainder) - } - } - } - - match (self, rhs) { - (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => { - if let Some(val) = checked_mod_i64(*lhs, *rhs) { - Ok(Value::int(val, span)) - } else if *rhs == 0 { - Err(ShellError::DivisionByZero { span: op }) - } else { - Err(ShellError::OperatorOverflow { - msg: "modulo operation overflowed".into(), - span, - help: None, - }) - } - } - (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => { - if let Some(val) = checked_mod_f64(*lhs as f64, *rhs) { - Ok(Value::float(val, span)) - } else { - Err(ShellError::DivisionByZero { span: op }) - } - } - (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => { - if let Some(val) = checked_mod_f64(*lhs, *rhs as f64) { - Ok(Value::float(val, span)) - } else { - Err(ShellError::DivisionByZero { span: op }) - } - } - (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => { - if let Some(val) = checked_mod_f64(*lhs, *rhs) { - Ok(Value::float(val, span)) - } else { - Err(ShellError::DivisionByZero { span: op }) - } - } - (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - if let Some(val) = checked_mod_i64(*lhs, *rhs) { - Ok(Value::filesize(val, span)) - } else if *rhs == 0 { - Err(ShellError::DivisionByZero { span: op }) - } else { - Err(ShellError::OperatorOverflow { - msg: "modulo operation overflowed".into(), - span, - help: None, - }) - } - } - (Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => { - if let Some(val) = checked_mod_i64(*lhs, *rhs) { - Ok(Value::filesize(val, span)) - } else if *rhs == 0 { - Err(ShellError::DivisionByZero { span: op }) - } else { - Err(ShellError::OperatorOverflow { - msg: "modulo operation overflowed".into(), - span, - help: None, - }) - } - } - (Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => { - if let Some(val) = checked_mod_f64(*lhs as f64, *rhs) { - if i64::MIN as f64 <= val && val <= i64::MAX as f64 { - Ok(Value::filesize(val as i64, span)) - } else { - Err(ShellError::OperatorOverflow { - msg: "modulo operation overflowed".into(), - span, - help: None, - }) - } - } else { - Err(ShellError::DivisionByZero { span: op }) - } - } - (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { - if let Some(val) = checked_mod_i64(*lhs, *rhs) { - Ok(Value::duration(val, span)) - } else if *rhs == 0 { - Err(ShellError::DivisionByZero { span: op }) - } else { - Err(ShellError::OperatorOverflow { - msg: "division operation overflowed".into(), - span, - help: None, - }) - } - } - (Value::Duration { val: lhs, .. }, Value::Int { val: rhs, .. }) => { - if let Some(val) = checked_mod_i64(*lhs, *rhs) { - Ok(Value::duration(val, span)) - } else if *rhs == 0 { - Err(ShellError::DivisionByZero { span: op }) - } else { - Err(ShellError::OperatorOverflow { - msg: "division operation overflowed".into(), - span, - help: None, - }) - } - } - (Value::Duration { val: lhs, .. }, Value::Float { val: rhs, .. }) => { - if let Some(val) = checked_mod_f64(*lhs as f64, *rhs) { - if i64::MIN as f64 <= val && val <= i64::MAX as f64 { - Ok(Value::duration(val as i64, span)) - } else { - Err(ShellError::OperatorOverflow { - msg: "division operation overflowed".into(), - span, - help: None, - }) - } - } else { - Err(ShellError::DivisionByZero { span: op }) - } - } - (Value::Custom { val: lhs, .. }, rhs) => { - lhs.operation(span, Operator::Math(Math::Modulo), op, rhs) - } - - _ => Err(ShellError::OperatorMismatch { - op_span: op, - lhs_ty: self.get_type().to_string(), - lhs_span: self.span(), - rhs_ty: rhs.get_type().to_string(), - rhs_span: rhs.span(), - }), - } - } - pub fn floor_div(&self, op: Span, rhs: &Value, span: Span) -> Result { // Taken from the unstable `div_floor` function in the std library. fn checked_div_floor_i64(dividend: i64, divisor: i64) -> Option { @@ -3003,9 +2877,9 @@ impl Value { } } (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - if let Some(val) = checked_div_floor_i64(*lhs, *rhs) { + if let Some(val) = checked_div_floor_i64(lhs.get(), rhs.get()) { Ok(Value::int(val, span)) - } else if *rhs == 0 { + } else if *rhs == Filesize::ZERO { Err(ShellError::DivisionByZero { span: op }) } else { Err(ShellError::OperatorOverflow { @@ -3016,7 +2890,7 @@ impl Value { } } (Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => { - if let Some(val) = checked_div_floor_i64(*lhs, *rhs) { + if let Some(val) = checked_div_floor_i64(lhs.get(), *rhs) { Ok(Value::filesize(val, span)) } else if *rhs == 0 { Err(ShellError::DivisionByZero { span: op }) @@ -3029,9 +2903,9 @@ impl Value { } } (Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => { - if let Some(val) = checked_div_floor_f64(*lhs as f64, *rhs) { - if i64::MIN as f64 <= val && val <= i64::MAX as f64 { - Ok(Value::filesize(val as i64, span)) + if let Some(val) = checked_div_floor_f64(lhs.get() as f64, *rhs) { + if let Ok(val) = Filesize::try_from(val) { + Ok(Value::filesize(val, span)) } else { Err(ShellError::OperatorOverflow { msg: "division operation overflowed".into(), @@ -3097,6 +2971,163 @@ impl Value { } } + pub fn modulo(&self, op: Span, rhs: &Value, span: Span) -> Result { + // Based off the unstable `div_floor` function in the std library. + fn checked_mod_i64(dividend: i64, divisor: i64) -> Option { + let remainder = dividend.checked_rem(divisor)?; + if (remainder > 0 && divisor < 0) || (remainder < 0 && divisor > 0) { + // Note that `remainder + divisor` cannot overflow, because `remainder` and + // `divisor` have opposite signs. + Some(remainder + divisor) + } else { + Some(remainder) + } + } + + fn checked_mod_f64(dividend: f64, divisor: f64) -> Option { + if divisor == 0.0 { + None + } else { + let remainder = dividend % divisor; + if (remainder > 0.0 && divisor < 0.0) || (remainder < 0.0 && divisor > 0.0) { + Some(remainder + divisor) + } else { + Some(remainder) + } + } + } + + match (self, rhs) { + (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if let Some(val) = checked_mod_i64(*lhs, *rhs) { + Ok(Value::int(val, span)) + } else if *rhs == 0 { + Err(ShellError::DivisionByZero { span: op }) + } else { + Err(ShellError::OperatorOverflow { + msg: "modulo operation overflowed".into(), + span, + help: None, + }) + } + } + (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + if let Some(val) = checked_mod_f64(*lhs as f64, *rhs) { + Ok(Value::float(val, span)) + } else { + Err(ShellError::DivisionByZero { span: op }) + } + } + (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if let Some(val) = checked_mod_f64(*lhs, *rhs as f64) { + Ok(Value::float(val, span)) + } else { + Err(ShellError::DivisionByZero { span: op }) + } + } + (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + if let Some(val) = checked_mod_f64(*lhs, *rhs) { + Ok(Value::float(val, span)) + } else { + Err(ShellError::DivisionByZero { span: op }) + } + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + if let Some(val) = checked_mod_i64(lhs.get(), rhs.get()) { + Ok(Value::filesize(val, span)) + } else if *rhs == Filesize::ZERO { + Err(ShellError::DivisionByZero { span: op }) + } else { + Err(ShellError::OperatorOverflow { + msg: "modulo operation overflowed".into(), + span, + help: None, + }) + } + } + (Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if let Some(val) = checked_mod_i64(lhs.get(), *rhs) { + Ok(Value::filesize(val, span)) + } else if *rhs == 0 { + Err(ShellError::DivisionByZero { span: op }) + } else { + Err(ShellError::OperatorOverflow { + msg: "modulo operation overflowed".into(), + span, + help: None, + }) + } + } + (Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + if let Some(val) = checked_mod_f64(lhs.get() as f64, *rhs) { + if let Ok(val) = Filesize::try_from(val) { + Ok(Value::filesize(val, span)) + } else { + Err(ShellError::OperatorOverflow { + msg: "modulo operation overflowed".into(), + span, + help: None, + }) + } + } else { + Err(ShellError::DivisionByZero { span: op }) + } + } + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + if let Some(val) = checked_mod_i64(*lhs, *rhs) { + Ok(Value::duration(val, span)) + } else if *rhs == 0 { + Err(ShellError::DivisionByZero { span: op }) + } else { + Err(ShellError::OperatorOverflow { + msg: "division operation overflowed".into(), + span, + help: None, + }) + } + } + (Value::Duration { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if let Some(val) = checked_mod_i64(*lhs, *rhs) { + Ok(Value::duration(val, span)) + } else if *rhs == 0 { + Err(ShellError::DivisionByZero { span: op }) + } else { + Err(ShellError::OperatorOverflow { + msg: "division operation overflowed".into(), + span, + help: None, + }) + } + } + (Value::Duration { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + if let Some(val) = checked_mod_f64(*lhs as f64, *rhs) { + if i64::MIN as f64 <= val && val <= i64::MAX as f64 { + Ok(Value::duration(val as i64, span)) + } else { + Err(ShellError::OperatorOverflow { + msg: "division operation overflowed".into(), + span, + help: None, + }) + } + } else { + Err(ShellError::DivisionByZero { span: op }) + } + } + (Value::Custom { val: lhs, .. }, rhs) => { + lhs.operation(span, Operator::Math(Math::Modulo), op, rhs) + } + + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type().to_string(), + lhs_span: self.span(), + rhs_ty: rhs.get_type().to_string(), + rhs_span: rhs.span(), + }), + } + } + pub fn lt(&self, op: Span, rhs: &Value, span: Span) -> Result { if let (Value::Custom { val: lhs, .. }, rhs) = (self, rhs) { return lhs.operation( diff --git a/crates/nu_plugin_formats/src/to/plist.rs b/crates/nu_plugin_formats/src/to/plist.rs index caf039263e..b7a15e03fe 100644 --- a/crates/nu_plugin_formats/src/to/plist.rs +++ b/crates/nu_plugin_formats/src/to/plist.rs @@ -1,10 +1,8 @@ -use std::time::SystemTime; - +use crate::FormatCmdsPlugin; use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand, SimplePluginCommand}; use nu_protocol::{Category, Example, LabeledError, Record, Signature, Span, Value as NuValue}; -use plist::{Integer, Value as PlistValue}; - -use crate::FormatCmdsPlugin; +use plist::Value as PlistValue; +use std::time::SystemTime; pub(crate) struct IntoPlist; @@ -68,7 +66,7 @@ fn convert_nu_value(nu_val: &NuValue) -> Result { NuValue::String { val, .. } => Ok(PlistValue::String(val.to_owned())), NuValue::Bool { val, .. } => Ok(PlistValue::Boolean(*val)), NuValue::Float { val, .. } => Ok(PlistValue::Real(*val)), - NuValue::Int { val, .. } => Ok(PlistValue::Integer(Into::::into(*val))), + NuValue::Int { val, .. } => Ok(PlistValue::Integer((*val).into())), NuValue::Binary { val, .. } => Ok(PlistValue::Data(val.to_owned())), NuValue::Record { val, .. } => convert_nu_dict(val), NuValue::List { vals, .. } => Ok(PlistValue::Array( @@ -77,7 +75,7 @@ fn convert_nu_value(nu_val: &NuValue) -> Result { .collect::>()?, )), NuValue::Date { val, .. } => Ok(PlistValue::Date(SystemTime::from(val.to_owned()).into())), - NuValue::Filesize { val, .. } => Ok(PlistValue::Integer(Into::::into(*val))), + NuValue::Filesize { val, .. } => Ok(PlistValue::Integer(val.get().into())), _ => Err(build_label_error( format!("{:?} is not convertible", nu_val), span, diff --git a/crates/nuon/src/from.rs b/crates/nuon/src/from.rs index ded1512a6a..0c94434a1a 100644 --- a/crates/nuon/src/from.rs +++ b/crates/nuon/src/from.rs @@ -1,7 +1,7 @@ use nu_protocol::{ ast::{Expr, Expression, ListItem, RecordItem}, engine::{EngineState, StateWorkingSet}, - Range, Record, ShellError, Span, Type, Unit, Value, + Filesize, IntoValue, Range, Record, ShellError, Span, Type, Unit, Value, }; use std::sync::Arc; @@ -9,7 +9,7 @@ use std::sync::Arc; /// // WARNING: please leave the following two trailing spaces, they matter for the documentation // formatting -/// > **Note** +/// > **Note** /// > [`Span`] can be passed to [`from_nuon`] if there is context available to the caller, e.g. when /// > using this function in a command implementation such as /// > [`from nuon`](https://www.nushell.sh/commands/docs/from_nuon.html). @@ -407,32 +407,15 @@ fn convert_to_value( }; match value.unit.item { - Unit::Byte => Ok(Value::filesize(size, span)), - Unit::Kilobyte => Ok(Value::filesize(size * 1000, span)), - Unit::Megabyte => Ok(Value::filesize(size * 1000 * 1000, span)), - Unit::Gigabyte => Ok(Value::filesize(size * 1000 * 1000 * 1000, span)), - Unit::Terabyte => Ok(Value::filesize(size * 1000 * 1000 * 1000 * 1000, span)), - Unit::Petabyte => Ok(Value::filesize( - size * 1000 * 1000 * 1000 * 1000 * 1000, - span, - )), - Unit::Exabyte => Ok(Value::filesize( - size * 1000 * 1000 * 1000 * 1000 * 1000 * 1000, - span, - )), - - Unit::Kibibyte => Ok(Value::filesize(size * 1024, span)), - Unit::Mebibyte => Ok(Value::filesize(size * 1024 * 1024, span)), - Unit::Gibibyte => Ok(Value::filesize(size * 1024 * 1024 * 1024, span)), - Unit::Tebibyte => Ok(Value::filesize(size * 1024 * 1024 * 1024 * 1024, span)), - Unit::Pebibyte => Ok(Value::filesize( - size * 1024 * 1024 * 1024 * 1024 * 1024, - span, - )), - Unit::Exbibyte => Ok(Value::filesize( - size * 1024 * 1024 * 1024 * 1024 * 1024 * 1024, - span, - )), + Unit::Filesize(unit) => match Filesize::from_unit(size, unit) { + Some(val) => Ok(val.into_value(span)), + None => Err(ShellError::OutsideSpannedLabeledError { + src: original_text.into(), + error: "filesize too large".into(), + msg: "filesize too large".into(), + span: expr.span, + }), + }, Unit::Nanosecond => Ok(Value::duration(size, span)), Unit::Microsecond => Ok(Value::duration(size * 1000, span)), diff --git a/crates/nuon/src/lib.rs b/crates/nuon/src/lib.rs index 304c8f977c..24a84dfd5f 100644 --- a/crates/nuon/src/lib.rs +++ b/crates/nuon/src/lib.rs @@ -155,7 +155,7 @@ mod tests { #[test] fn filesize() { nuon_end_to_end("1024b", Some(Value::test_filesize(1024))); - assert_eq!(from_nuon("1kib", None).unwrap(), Value::test_filesize(1024),); + assert_eq!(from_nuon("1kib", None).unwrap(), Value::test_filesize(1024)); } #[test] diff --git a/crates/nuon/src/to.rs b/crates/nuon/src/to.rs index b6c4f7829b..b8281e8ffe 100644 --- a/crates/nuon/src/to.rs +++ b/crates/nuon/src/to.rs @@ -39,7 +39,7 @@ pub enum ToStyle { /// // WARNING: please leave the following two trailing spaces, they matter for the documentation // formatting -/// > **Note** +/// > **Note** /// > a [`Span`] can be passed to [`to_nuon`] if there is context available to the caller, e.g. when /// > using this function in a command implementation such as [`to nuon`](https://www.nushell.sh/commands/docs/to_nuon.html). /// @@ -110,7 +110,7 @@ fn value_to_string( // Propagate existing errors Value::Error { error, .. } => Err(*error.clone()), // FIXME: make filesizes use the shortest lossless representation. - Value::Filesize { val, .. } => Ok(format!("{}b", *val)), + Value::Filesize { val, .. } => Ok(format!("{}b", val.get())), Value::Float { val, .. } => { // This serialises these as 'nan', 'inf' and '-inf', respectively. if &val.round() == val && val.is_finite() {