Implement almost all of printf
This commit is contained in:
+523
-107
@@ -1,120 +1,536 @@
|
||||
use alloc::string::String;
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
use core::fmt::Write as CoreWrite;
|
||||
use core::{ptr, slice, str};
|
||||
use core::ops::Range;
|
||||
use core::{fmt, ptr, slice, str};
|
||||
|
||||
use c_str::CStr;
|
||||
use io::{self, Write};
|
||||
use platform::types::*;
|
||||
use platform::{self, WriteByte};
|
||||
use va_list::VaList;
|
||||
use va_list::{VaList, VaPrimitive};
|
||||
|
||||
pub unsafe fn printf<W: WriteByte>(w: W, format: *const c_char, mut ap: VaList) -> c_int {
|
||||
let mut w = platform::CountingWriter::new(w);
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum IntKind {
|
||||
Byte,
|
||||
Short,
|
||||
Int,
|
||||
Long,
|
||||
LongLong,
|
||||
IntMax,
|
||||
PtrDiff,
|
||||
Size,
|
||||
}
|
||||
|
||||
let format = slice::from_raw_parts(format as *const u8, usize::max_value());
|
||||
|
||||
let mut found_percent = false;
|
||||
for &b in format.iter() {
|
||||
// check for NUL
|
||||
if b == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if found_percent {
|
||||
if match b as char {
|
||||
'%' => {
|
||||
found_percent = false;
|
||||
w.write_char('%')
|
||||
}
|
||||
'c' => {
|
||||
let a = ap.get::<u32>();
|
||||
|
||||
found_percent = false;
|
||||
|
||||
w.write_u8(a as u8)
|
||||
}
|
||||
'd' | 'i' => {
|
||||
let a = ap.get::<c_int>();
|
||||
|
||||
found_percent = false;
|
||||
|
||||
w.write_fmt(format_args!("{}", a))
|
||||
}
|
||||
'f' | 'F' => {
|
||||
let a = ap.get::<f64>();
|
||||
|
||||
found_percent = false;
|
||||
|
||||
w.write_fmt(format_args!("{}", a))
|
||||
}
|
||||
'n' => {
|
||||
let _a = ap.get::<c_int>();
|
||||
|
||||
found_percent = false;
|
||||
Ok(())
|
||||
}
|
||||
'p' => {
|
||||
let a = ap.get::<usize>();
|
||||
|
||||
found_percent = false;
|
||||
|
||||
w.write_fmt(format_args!("0x{:x}", a))
|
||||
}
|
||||
's' => {
|
||||
let a = ap.get::<*const c_char>();
|
||||
|
||||
found_percent = false;
|
||||
if a != ptr::null() {
|
||||
let a_cstr = CStr::from_ptr(a);
|
||||
w.write_str(str::from_utf8_unchecked(a_cstr.to_bytes()))
|
||||
} else {
|
||||
w.write_str("NULL")
|
||||
}
|
||||
}
|
||||
'u' => {
|
||||
let a = ap.get::<c_uint>();
|
||||
|
||||
found_percent = false;
|
||||
|
||||
w.write_fmt(format_args!("{}", a))
|
||||
}
|
||||
'x' => {
|
||||
let a = ap.get::<c_uint>();
|
||||
|
||||
found_percent = false;
|
||||
|
||||
w.write_fmt(format_args!("{:x}", a))
|
||||
}
|
||||
'X' => {
|
||||
let a = ap.get::<c_uint>();
|
||||
|
||||
found_percent = false;
|
||||
|
||||
w.write_fmt(format_args!("{:X}", a))
|
||||
}
|
||||
'o' => {
|
||||
let a = ap.get::<c_uint>();
|
||||
|
||||
found_percent = false;
|
||||
|
||||
w.write_fmt(format_args!("{:o}", a))
|
||||
}
|
||||
'-' => Ok(()),
|
||||
'+' => Ok(()),
|
||||
' ' => Ok(()),
|
||||
'#' => Ok(()),
|
||||
'0'...'9' => Ok(()),
|
||||
_ => Ok(()),
|
||||
}.is_err()
|
||||
{
|
||||
return -1;
|
||||
trait IntoUsize {
|
||||
fn into_usize(self) -> usize;
|
||||
fn from_usize(i: usize) -> Self;
|
||||
}
|
||||
macro_rules! impl_intousize {
|
||||
($($kind:tt;)*) => {
|
||||
$(impl IntoUsize for $kind {
|
||||
fn into_usize(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
} else if b == b'%' {
|
||||
found_percent = true;
|
||||
} else {
|
||||
if w.write_u8(b).is_err() {
|
||||
return -1;
|
||||
fn from_usize(i: usize) -> Self {
|
||||
i as Self
|
||||
}
|
||||
})*
|
||||
}
|
||||
}
|
||||
impl_intousize! {
|
||||
i32;
|
||||
u32;
|
||||
i64;
|
||||
u64;
|
||||
isize;
|
||||
usize;
|
||||
}
|
||||
impl<T> IntoUsize for *const T {
|
||||
fn into_usize(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
fn from_usize(i: usize) -> Self {
|
||||
i as Self
|
||||
}
|
||||
}
|
||||
impl<T> IntoUsize for *mut T {
|
||||
fn into_usize(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
fn from_usize(i: usize) -> Self {
|
||||
i as Self
|
||||
}
|
||||
}
|
||||
impl IntoUsize for f32 {
|
||||
fn into_usize(self) -> usize {
|
||||
self.to_bits() as usize
|
||||
}
|
||||
fn from_usize(i: usize) -> Self {
|
||||
Self::from_bits(i as u32)
|
||||
}
|
||||
}
|
||||
impl IntoUsize for f64 {
|
||||
fn into_usize(self) -> usize {
|
||||
self.to_bits() as usize
|
||||
}
|
||||
fn from_usize(i: usize) -> Self {
|
||||
Self::from_bits(i as u64)
|
||||
}
|
||||
}
|
||||
|
||||
struct BufferedVaList {
|
||||
list: VaList,
|
||||
buf: Vec<usize>,
|
||||
i: usize
|
||||
}
|
||||
impl BufferedVaList {
|
||||
fn new(list: VaList) -> Self {
|
||||
Self {
|
||||
list,
|
||||
buf: Vec::new(),
|
||||
i: 0
|
||||
}
|
||||
}
|
||||
unsafe fn get<T: VaPrimitive + IntoUsize>(&mut self, i: Option<usize>) -> T {
|
||||
match i {
|
||||
None => self.next(),
|
||||
Some(i) => self.index(i),
|
||||
}
|
||||
}
|
||||
unsafe fn next<T: VaPrimitive + IntoUsize>(&mut self) -> T {
|
||||
if self.i >= self.buf.len() {
|
||||
self.buf.push(self.list.get::<T>().into_usize());
|
||||
}
|
||||
let arg = T::from_usize(self.buf[self.i]);
|
||||
self.i += 1;
|
||||
arg
|
||||
}
|
||||
unsafe fn index<T: VaPrimitive + IntoUsize>(&mut self, i: usize) -> T {
|
||||
while self.buf.len() < i {
|
||||
// Getting a usize here most definitely isn't sane, however,
|
||||
// there's no way to know the type!
|
||||
// Just take this for example:
|
||||
//
|
||||
// printf("%*4$d\n", "hi", 0, "hello", 10);
|
||||
//
|
||||
// This chooses the width 10. How does it know the type of 0 and "hello"?
|
||||
// It clearly can't.
|
||||
|
||||
w.written as c_int
|
||||
self.buf.push(self.list.get::<usize>());
|
||||
}
|
||||
T::from_usize(self.buf[i - 1])
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn pop_int_raw(format: &mut *const u8) -> Option<usize> {
|
||||
let mut int = None;
|
||||
while let Some(digit) = (**format as char).to_digit(10) {
|
||||
*format = format.offset(1);
|
||||
if int.is_none() {
|
||||
int = Some(0);
|
||||
}
|
||||
*int.as_mut().unwrap() *= 10;
|
||||
*int.as_mut().unwrap() += digit as usize;
|
||||
}
|
||||
int
|
||||
}
|
||||
unsafe fn pop_int(format: &mut *const u8, ap: &mut BufferedVaList) -> Option<usize> {
|
||||
if **format == b'*' {
|
||||
*format = format.offset(1);
|
||||
|
||||
// Peek ahead for a positional argument:
|
||||
let mut format2 = *format;
|
||||
if let Some(i) = pop_int_raw(&mut format2) {
|
||||
if *format2 == b'$' {
|
||||
*format = format2.offset(1);
|
||||
return Some(ap.index::<usize>(i))
|
||||
}
|
||||
}
|
||||
|
||||
Some(ap.next::<usize>())
|
||||
} else {
|
||||
pop_int_raw(format)
|
||||
}
|
||||
}
|
||||
unsafe fn fmt_int<I>(fmt: u8, i: I) -> String
|
||||
where I: fmt::Display + fmt::Octal + fmt::LowerHex + fmt::UpperHex
|
||||
{
|
||||
match fmt {
|
||||
b'o' => format!("{:o}", i),
|
||||
b'u' => i.to_string(),
|
||||
b'x' => format!("{:x}", i),
|
||||
b'X' => format!("{:X}", i),
|
||||
_ => panic!("fmt_int should never be called with the fmt {}", fmt)
|
||||
}
|
||||
}
|
||||
fn pad<W: Write>(w: &mut W, current_side: bool, pad_char: u8, range: Range<usize>) -> io::Result<()> {
|
||||
if current_side {
|
||||
for _ in range {
|
||||
w.write_all(&[pad_char])?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn float_string(float: c_double, precision: usize, trim: bool) -> String {
|
||||
let mut string = format!("{:.p$}", float, p = precision);
|
||||
if trim {
|
||||
let truncate = {
|
||||
let mut slice = string.trim_right_matches('0');
|
||||
if slice.ends_with('.') {
|
||||
slice.len() - 1
|
||||
} else {
|
||||
slice.len()
|
||||
}
|
||||
};
|
||||
string.truncate(truncate);
|
||||
}
|
||||
string
|
||||
}
|
||||
fn fmt_float_exp<W: Write>(
|
||||
w: &mut W,
|
||||
exp_fmt: u8,
|
||||
range: Option<(isize, isize)>,
|
||||
trim: bool,
|
||||
precision: usize,
|
||||
mut float: c_double,
|
||||
left: bool,
|
||||
pad_space: usize,
|
||||
pad_zero: usize,
|
||||
) -> io::Result<bool> {
|
||||
let mut exp: isize = 0;
|
||||
while float >= 10.0 || float <= -10.0 {
|
||||
float /= 10.0;
|
||||
exp += 1;
|
||||
}
|
||||
while (float > 0.0 && float < 1.0) || (float > -1.0 && float < 0.0) {
|
||||
float *= 10.0;
|
||||
exp -= 1;
|
||||
}
|
||||
|
||||
if range.map(|(start, end)| exp >= start && exp < end).unwrap_or(false) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut exp2 = exp;
|
||||
let mut exp_len = 1;
|
||||
while exp2 >= 10 {
|
||||
exp2 /= 10;
|
||||
exp_len += 1;
|
||||
}
|
||||
|
||||
let string = float_string(float, precision, trim);
|
||||
let mut len = string.len() + 2 + 2.max(exp_len);
|
||||
|
||||
pad(w, !left, b' ', len..pad_space)?;
|
||||
let bytes = if string.starts_with('-') {
|
||||
w.write_all(&[b'-'])?;
|
||||
&string.as_bytes()[1..]
|
||||
} else {
|
||||
string.as_bytes()
|
||||
};
|
||||
pad(w, !left, b'0', len..pad_zero)?;
|
||||
w.write_all(bytes)?;
|
||||
write!(w, "{}{:+03}", exp_fmt as char, exp)?;
|
||||
pad(w, left, b' ', len..pad_space)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
fn fmt_float_normal<W: Write>(
|
||||
w: &mut W,
|
||||
trim: bool,
|
||||
precision: usize,
|
||||
float: c_double,
|
||||
left: bool,
|
||||
pad_space: usize,
|
||||
pad_zero: usize,
|
||||
) -> io::Result<usize> {
|
||||
let string = float_string(float, precision, trim);
|
||||
|
||||
pad(w, !left, b' ', string.len()..pad_space)?;
|
||||
let bytes = if string.starts_with('-') {
|
||||
w.write_all(&[b'-'])?;
|
||||
&string.as_bytes()[1..]
|
||||
} else {
|
||||
string.as_bytes()
|
||||
};
|
||||
pad(w, true, b'0', string.len()..pad_zero)?;
|
||||
w.write_all(bytes)?;
|
||||
pad(w, left, b' ', string.len()..pad_space)?;
|
||||
|
||||
Ok(string.len())
|
||||
}
|
||||
unsafe fn inner_printf<W: Write>(w: W, format: *const c_char, mut ap: VaList) -> io::Result<c_int> {
|
||||
let mut w = &mut platform::CountingWriter::new(w);
|
||||
let mut ap = BufferedVaList::new(ap);
|
||||
let mut format = format as *const u8;
|
||||
|
||||
while *format != 0 {
|
||||
if *format != b'%' {
|
||||
w.write_all(&[*format])?;
|
||||
} else {
|
||||
format = format.offset(1);
|
||||
|
||||
// Peek ahead to maybe specify argument to fetch from
|
||||
let mut index = None;
|
||||
let mut format2 = format;
|
||||
if let Some(i) = pop_int_raw(&mut format2) {
|
||||
if *format2 == b'$' {
|
||||
format = format2.offset(1);
|
||||
index = Some(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Flags:
|
||||
let mut alternate = false;
|
||||
let mut zero = false;
|
||||
let mut left = false;
|
||||
let mut sign_reserve = false;
|
||||
let mut sign_always = false;
|
||||
|
||||
loop {
|
||||
match *format {
|
||||
b'#' => alternate = true,
|
||||
b'0' => zero = true,
|
||||
b'-' => left = true,
|
||||
b' ' => sign_reserve = true,
|
||||
b'+' => sign_always = true,
|
||||
_ => break
|
||||
}
|
||||
format = format.offset(1);
|
||||
}
|
||||
|
||||
// Width and precision:
|
||||
let min_width = pop_int(&mut format, &mut ap).unwrap_or(0);
|
||||
let precision = if *format == b'.' {
|
||||
format = format.offset(1);
|
||||
match pop_int(&mut format, &mut ap) {
|
||||
int@Some(_) => int,
|
||||
None => return Ok(-1)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let pad_space = if zero { 0 } else { min_width };
|
||||
let pad_zero = if zero { min_width } else { 0 };
|
||||
|
||||
// Integer size:
|
||||
let mut kind = IntKind::Int;
|
||||
loop {
|
||||
kind = match *format {
|
||||
b'h' => if kind == IntKind::Short || kind == IntKind::Byte {
|
||||
IntKind::Byte
|
||||
} else {
|
||||
IntKind::Short
|
||||
},
|
||||
b'j' => IntKind::IntMax,
|
||||
b'l' => if kind == IntKind::Long || kind == IntKind::LongLong {
|
||||
IntKind::LongLong
|
||||
} else {
|
||||
IntKind::Long
|
||||
},
|
||||
b'q' | b'L' => IntKind::LongLong,
|
||||
b't' => IntKind::PtrDiff,
|
||||
b'z' => IntKind::Size,
|
||||
_ => break,
|
||||
};
|
||||
|
||||
format = format.offset(1);
|
||||
}
|
||||
|
||||
// Finally, type:
|
||||
match *format {
|
||||
b'%' => w.write_all(&[b'%'])?,
|
||||
b'd' | b'i' => {
|
||||
let string = match kind {
|
||||
// VaList does not seem to support these two:
|
||||
// IntKind::Byte => ap.get::<c_char>(index).to_string(),
|
||||
// IntKind::Short => ap.get::<c_short>(index).to_string(),
|
||||
IntKind::Byte => (ap.get::<c_int>(index) as c_char).to_string(),
|
||||
IntKind::Short => (ap.get::<c_int>(index) as c_short).to_string(),
|
||||
IntKind::Int => ap.get::<c_int>(index).to_string(),
|
||||
IntKind::Long => ap.get::<c_long>(index).to_string(),
|
||||
IntKind::LongLong => ap.get::<c_longlong>(index).to_string(),
|
||||
IntKind::PtrDiff => ap.get::<ptrdiff_t>(index).to_string(),
|
||||
IntKind::Size => ap.get::<ssize_t>(index).to_string(),
|
||||
IntKind::IntMax => ap.get::<intmax_t>(index).to_string(),
|
||||
};
|
||||
let positive = !string.starts_with('-');
|
||||
let zero = precision == Some(0) && string == "0";
|
||||
|
||||
let mut len = string.len();
|
||||
let mut final_len = string.len().max(precision.unwrap_or(0));
|
||||
if positive && (sign_reserve || sign_always) {
|
||||
final_len += 1;
|
||||
}
|
||||
if zero {
|
||||
len = 0;
|
||||
final_len = 0;
|
||||
}
|
||||
|
||||
pad(w, !left, b' ', final_len..pad_space)?;
|
||||
|
||||
let bytes = if positive {
|
||||
if sign_reserve {
|
||||
w.write_all(&[b' '])?;
|
||||
} else if sign_always {
|
||||
w.write_all(&[b'+'])?;
|
||||
}
|
||||
string.as_bytes()
|
||||
} else {
|
||||
w.write_all(&[b'-'])?;
|
||||
&string.as_bytes()[1..]
|
||||
};
|
||||
pad(w, true, b'0', len..precision.unwrap_or(pad_zero))?;
|
||||
|
||||
if !zero {
|
||||
w.write_all(bytes)?;
|
||||
}
|
||||
|
||||
pad(w, left, b' ', final_len..pad_space)?;
|
||||
},
|
||||
b'o' | b'u' | b'x' | b'X' => {
|
||||
let fmt = *format;
|
||||
let string = match kind {
|
||||
// VaList does not seem to support these two:
|
||||
// IntKind::Byte => fmt_int(kind ap.get::<c_char>(index)),
|
||||
// IntKind::Short => fmt_int(kind ap.get::<c_short>(index)),
|
||||
IntKind::Byte => fmt_int(fmt, ap.get::<c_uint>(index) as c_uchar),
|
||||
IntKind::Short => fmt_int(fmt, ap.get::<c_uint>(index) as c_ushort),
|
||||
IntKind::Int => fmt_int(fmt, ap.get::<c_uint>(index)),
|
||||
IntKind::Long => fmt_int(fmt, ap.get::<c_ulong>(index)),
|
||||
IntKind::LongLong => fmt_int(fmt, ap.get::<c_ulonglong>(index)),
|
||||
IntKind::PtrDiff => fmt_int(fmt, ap.get::<ptrdiff_t>(index)),
|
||||
IntKind::Size => fmt_int(fmt, ap.get::<size_t>(index)),
|
||||
IntKind::IntMax => fmt_int(fmt, ap.get::<uintmax_t>(index)),
|
||||
};
|
||||
let zero = precision == Some(0) && string == "0";
|
||||
|
||||
// If this int is padded out to be larger than it is, don't
|
||||
// add an extra zero if octal.
|
||||
let no_precision = precision.map(|pad| pad < string.len()).unwrap_or(true);
|
||||
|
||||
let mut len = string.len();
|
||||
let mut final_len = string.len().max(precision.unwrap_or(0)) +
|
||||
if alternate && string != "0" {
|
||||
match fmt {
|
||||
b'o' if no_precision => 1,
|
||||
b'x' |
|
||||
b'X' => 2,
|
||||
_ => 0
|
||||
}
|
||||
} else { 0 };
|
||||
|
||||
if zero {
|
||||
len = 0;
|
||||
final_len = 0;
|
||||
}
|
||||
|
||||
pad(w, !left, b' ', final_len..pad_space)?;
|
||||
|
||||
if alternate && string != "0" {
|
||||
match fmt {
|
||||
b'o' if no_precision => w.write_all(&[b'0'])?,
|
||||
b'x' => w.write_all(&[b'0', b'x'])?,
|
||||
b'X' => w.write_all(&[b'0', b'X'])?,
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
pad(w, true, b'0', len..precision.unwrap_or(pad_zero))?;
|
||||
|
||||
if !zero {
|
||||
w.write_all(string.as_bytes())?;
|
||||
}
|
||||
|
||||
pad(w, left, b' ', final_len..pad_space)?;
|
||||
},
|
||||
b'e' | b'E' => {
|
||||
let exp_fmt = *format;
|
||||
let mut float = ap.get::<c_double>(index);
|
||||
let precision = precision.unwrap_or(6);
|
||||
|
||||
fmt_float_exp(w, exp_fmt, None, false, precision, float, left, pad_space, pad_zero)?;
|
||||
},
|
||||
b'f' | b'F' => {
|
||||
let mut float = ap.get::<c_double>(index);
|
||||
let precision = precision.unwrap_or(6);
|
||||
|
||||
fmt_float_normal(w, false, precision, float, left, pad_space, pad_zero)?;
|
||||
},
|
||||
b'g' | b'G' => {
|
||||
let exp_fmt = b'E' | (*format & 32);
|
||||
let mut float = ap.get::<c_double>(index);
|
||||
let precision = precision.unwrap_or(6);
|
||||
|
||||
if !fmt_float_exp(w, exp_fmt, Some((-4, precision as isize)), true,
|
||||
precision, float, left, pad_space, pad_zero)? {
|
||||
fmt_float_normal(w, true, precision, float, left, pad_space, pad_zero)?;
|
||||
}
|
||||
}
|
||||
b's' => {
|
||||
// if kind == IntKind::Long || kind == IntKind::LongLong, handle *const wchar_t
|
||||
|
||||
let ptr = ap.get::<*const c_char>(index);
|
||||
|
||||
if ptr.is_null() {
|
||||
w.write_all(b"(null)")?;
|
||||
} else {
|
||||
let mut len = 0;
|
||||
while *ptr.offset(len) != 0 {
|
||||
len += 1;
|
||||
}
|
||||
|
||||
let len = (len as usize).min(precision.unwrap_or(::core::usize::MAX));
|
||||
|
||||
pad(w, !left, b' ', len..pad_space)?;
|
||||
w.write_all(slice::from_raw_parts(ptr as *const u8, len))?;
|
||||
pad(w, left, b' ', len..pad_space)?;
|
||||
}
|
||||
},
|
||||
b'c' => {
|
||||
// if kind == IntKind::Long || kind == IntKind::LongLong, handle wint_t
|
||||
|
||||
let c = ap.get::<c_int>(index) as c_char;
|
||||
|
||||
pad(w, !left, b' ', 1..pad_space)?;
|
||||
w.write_all(&[c as u8])?;
|
||||
pad(w, left, b' ', 1..pad_space)?;
|
||||
},
|
||||
b'p' => {
|
||||
let ptr = ap.get::<*const c_void>(index);
|
||||
|
||||
let mut len = 1;
|
||||
if ptr.is_null() {
|
||||
len = "(nil)".len();
|
||||
} else {
|
||||
let mut ptr = ptr as usize;
|
||||
while ptr >= 10 {
|
||||
ptr /= 10;
|
||||
len += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pad(w, !left, b' ', len..pad_space)?;
|
||||
if ptr.is_null() {
|
||||
write!(w, "(nil)")?;
|
||||
} else {
|
||||
write!(w, "0x{:x}", ptr as usize)?;
|
||||
}
|
||||
pad(w, left, b' ', len..pad_space)?;
|
||||
},
|
||||
b'n' => {
|
||||
let ptr = ap.get::<*mut c_int>(index);
|
||||
*ptr = w.written as c_int;
|
||||
},
|
||||
_ => return Ok(-1)
|
||||
}
|
||||
}
|
||||
format = format.offset(1);
|
||||
}
|
||||
Ok(w.written as c_int)
|
||||
}
|
||||
pub unsafe fn printf<W: Write>(w: W, format: *const c_char, ap: VaList) -> c_int {
|
||||
inner_printf(w, format, ap).unwrap_or(-1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user