don't read file into a string buffer to parse (#9)

* perform utf8 validation manually while parsing

* perform manual utf8 validation in async

* cleaned up reader

* fixed parsing bugs

* handle utf-16

* skip whitespace before file format

* dont introduce new error variant

* removed new error variant

* simplify macros
This commit is contained in:
Max
2022-06-13 22:35:57 +02:00
committed by GitHub
parent dc07a21844
commit bfe61f0fb2
3 changed files with 355 additions and 188 deletions
+1 -3
View File
@@ -1,5 +1,3 @@
use super::OSU_FILE_HEADER;
use std::{
error::Error as StdError,
fmt,
@@ -41,7 +39,7 @@ impl fmt::Display for ParseError {
match self {
Self::IOError(_) => f.write_str("IO error"),
Self::IncorrectFileHeader => {
write!(f, "expected `{}` at file begin", OSU_FILE_HEADER)
write!(f, "expected `osu file format v` at file begin")
}
Self::BadLine => f.write_str("line not in `Key:Value` pattern"),
Self::InvalidCurvePoints => f.write_str("invalid curve point"),
+103 -185
View File
@@ -4,6 +4,7 @@ mod error;
mod hitobject;
mod hitsound;
mod pos2;
mod reader;
mod sort;
pub use attributes::BeatmapAttributes;
@@ -12,33 +13,24 @@ pub use error::{ParseError, ParseResult};
pub use hitobject::{HitObject, HitObjectKind};
pub use hitsound::HitSound;
pub use pos2::Pos2;
pub use slider_parsing::*;
use reader::FileReader;
use sort::legacy_sort;
use std::cmp::Ordering;
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
use std::{
fs::File,
io::{BufRead, BufReader, Read},
};
use std::{fs::File, io::Read};
#[cfg(feature = "async_tokio")]
use tokio::{
fs::File,
io::{AsyncBufReadExt, AsyncRead, BufReader},
};
use tokio::{fs::File, io::AsyncRead};
#[cfg(not(feature = "async_std"))]
use std::path::Path;
#[cfg(feature = "async_std")]
use async_std::{
fs::File,
io::{prelude::BufReadExt, BufReader as AsyncBufReader, Read as AsyncRead},
path::Path,
};
pub use slider_parsing::*;
use async_std::{fs::File, io::Read as AsyncRead, path::Path};
fn sort_unstable<T: PartialOrd>(slice: &mut [T]) {
slice.sort_unstable_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
@@ -66,70 +58,50 @@ impl FloatExt for f64 {
}
}
macro_rules! line_prepare {
($buf:ident) => {{
let mut line = $buf.trim_end();
if skip_line(line) {
$buf.clear();
continue;
}
if let Some(idx) = line.find("//") {
line = &line[..idx];
}
line
}};
}
macro_rules! section {
($map:ident, $func:ident, $reader:ident, $buf:ident, $section:ident) => {{
($map:ident, $func:ident, $reader:ident, $section:ident) => {{
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
if $map.$func(&mut $reader, &mut $buf, &mut $section)? {
if $map.$func(&mut $reader, &mut $section)? {
break;
}
#[cfg(any(feature = "async_std", feature = "async_tokio"))]
if $map.$func(&mut $reader, &mut $buf, &mut $section).await? {
if $map.$func(&mut $reader, &mut $section).await? {
break;
}
}};
}
macro_rules! read_line {
($reader:ident, $buf:expr) => {{
macro_rules! next_line {
($reader:ident) => {{
#[cfg(any(feature = "async_std", feature = "async_tokio"))]
{
$reader.read_line($buf).await
$reader.next_line().await
}
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
{
$reader.read_line($buf)
$reader.next_line()
}
}};
}
macro_rules! parse_general_body {
($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
($self:ident, $reader:ident, $section:ident) => {{
let mut mode = None;
let mut empty = true;
let mut stack_leniency = None;
while read_line!($reader, $buf)? != 0 {
let line = line_prepare!($buf);
if line.starts_with('[') && line.ends_with(']') {
*$section = Section::from_str(&line[1..line.len() - 1]);
while next_line!($reader)? != 0 {
if let Some(bytes) = $reader.get_section() {
*$section = Section::from_bytes(bytes);
empty = false;
$buf.clear();
break;
}
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
let (key, value) = $reader.split_colon().ok_or(ParseError::BadLine)?;
if key == "Mode" {
if key == b"Mode" {
mode = match value {
"0" => Some(GameMode::STD),
"1" => Some(GameMode::TKO),
@@ -139,11 +111,9 @@ macro_rules! parse_general_body {
};
}
if key == "StackLeniency" {
if key == b"StackLeniency" {
stack_leniency = Some(value.parse()?);
}
$buf.clear();
}
$self.mode = mode.unwrap_or(GameMode::STD);
@@ -157,28 +127,26 @@ macro_rules! parse_general {
() => {
fn parse_general<R: Read>(
&mut self,
reader: &mut BufReader<R>,
buf: &mut String,
reader: &mut FileReader<R>,
section: &mut Section,
) -> ParseResult<bool> {
parse_general_body!(self, reader, buf, section)
parse_general_body!(self, reader, section)
}
};
(async $reader:ident<$inner:ident>) => {
async fn parse_general<R: $inner + Unpin>(
(async) => {
async fn parse_general<R: AsyncRead + Unpin>(
&mut self,
reader: &mut $reader<R>,
buf: &mut String,
reader: &mut FileReader<R>,
section: &mut Section,
) -> ParseResult<bool> {
parse_general_body!(self, reader, buf, section)
parse_general_body!(self, reader, section)
}
};
}
macro_rules! parse_difficulty_body {
($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
($self:ident, $reader:ident, $section:ident) => {{
let mut ar = None;
let mut od = None;
let mut cs = None;
@@ -188,29 +156,24 @@ macro_rules! parse_difficulty_body {
let mut empty = true;
while read_line!($reader, $buf)? != 0 {
let line = line_prepare!($buf);
if line.starts_with('[') && line.ends_with(']') {
*$section = Section::from_str(&line[1..line.len() - 1]);
while next_line!($reader)? != 0 {
if let Some(bytes) = $reader.get_section() {
*$section = Section::from_bytes(bytes);
empty = false;
$buf.clear();
break;
}
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
let (key, value) = $reader.split_colon().ok_or(ParseError::BadLine)?;
match key {
"ApproachRate" => ar = Some(value.parse()?),
"OverallDifficulty" => od = Some(value.parse()?),
"CircleSize" => cs = Some(value.parse()?),
"HPDrainRate" => hp = Some(value.parse()?),
"SliderTickRate" => tick_rate = Some(value.parse()?),
"SliderMultiplier" => sv = Some(value.parse()?),
b"ApproachRate" => ar = Some(value.parse()?),
b"OverallDifficulty" => od = Some(value.parse()?),
b"CircleSize" => cs = Some(value.parse()?),
b"HPDrainRate" => hp = Some(value.parse()?),
b"SliderTickRate" => tick_rate = Some(value.parse()?),
b"SliderMultiplier" => sv = Some(value.parse()?),
_ => {}
}
$buf.clear();
}
const DEFAULT_DIFFICULTY: f32 = 5.0;
@@ -230,28 +193,26 @@ macro_rules! parse_difficulty {
() => {
fn parse_difficulty<R: Read>(
&mut self,
reader: &mut BufReader<R>,
buf: &mut String,
reader: &mut FileReader<R>,
section: &mut Section,
) -> ParseResult<bool> {
parse_difficulty_body!(self, reader, buf, section)
parse_difficulty_body!(self, reader, section)
}
};
(async $reader:ident<$inner:ident>) => {
async fn parse_difficulty<R: $inner + Unpin>(
(async) => {
async fn parse_difficulty<R: AsyncRead + Unpin>(
&mut self,
reader: &mut $reader<R>,
buf: &mut String,
reader: &mut FileReader<R>,
section: &mut Section,
) -> ParseResult<bool> {
parse_difficulty_body!(self, reader, buf, section)
parse_difficulty_body!(self, reader, section)
}
};
}
macro_rules! parse_timingpoints_body {
($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
($self:ident, $reader:ident, $section:ident) => {{
let mut unsorted_timings = false;
let mut unsorted_difficulties = false;
@@ -260,16 +221,14 @@ macro_rules! parse_timingpoints_body {
let mut empty = true;
while read_line!($reader, $buf)? != 0 {
let line = line_prepare!($buf);
if line.starts_with('[') && line.ends_with(']') {
*$section = Section::from_str(&line[1..line.len() - 1]);
while next_line!($reader)? != 0 {
if let Some(bytes) = $reader.get_section() {
*$section = Section::from_bytes(bytes);
empty = false;
$buf.clear();
break;
}
let line = $reader.get_line()?;
let mut split = line.split(',');
let time = split
@@ -311,8 +270,6 @@ macro_rules! parse_timingpoints_body {
prev_diff = time;
}
}
$buf.clear();
}
if unsorted_timings {
@@ -331,28 +288,26 @@ macro_rules! parse_timingpoints {
() => {
fn parse_timingpoints<R: Read>(
&mut self,
reader: &mut BufReader<R>,
buf: &mut String,
reader: &mut FileReader<R>,
section: &mut Section,
) -> ParseResult<bool> {
parse_timingpoints_body!(self, reader, buf, section)
parse_timingpoints_body!(self, reader, section)
}
};
(async $reader:ident<$inner:ident>) => {
async fn parse_timingpoints<R: $inner + Unpin>(
(async) => {
async fn parse_timingpoints<R: AsyncRead + Unpin>(
&mut self,
reader: &mut $reader<R>,
buf: &mut String,
reader: &mut FileReader<R>,
section: &mut Section,
) -> ParseResult<bool> {
parse_timingpoints_body!(self, reader, buf, section)
parse_timingpoints_body!(self, reader, section)
}
};
}
macro_rules! parse_hitobjects_body {
($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
($self:ident, $reader:ident, $section:ident) => {{
let mut unsorted = false;
let mut prev_time = 0.0;
let mut empty = true;
@@ -366,16 +321,14 @@ macro_rules! parse_hitobjects_body {
// Buffer to re-use for all sliders
let mut vertices = Vec::new();
while read_line!($reader, $buf)? != 0 {
let line = line_prepare!($buf);
if line.starts_with('[') && line.ends_with(']') {
*$section = Section::from_str(&line[1..line.len() - 1]);
while next_line!($reader)? != 0 {
if let Some(bytes) = $reader.get_section() {
*$section = Section::from_bytes(bytes);
empty = false;
$buf.clear();
break;
}
let line = $reader.get_line()?;
let mut split = line.split(',');
let pos = Pos2 {
@@ -511,7 +464,6 @@ macro_rules! parse_hitobjects_body {
$self.sounds.push(sound);
prev_time = time;
$buf.clear();
}
// BUG: If [General] section comes after [HitObjects] then the mode
@@ -536,52 +488,35 @@ macro_rules! parse_hitobjects {
() => {
fn parse_hitobjects<R: Read>(
&mut self,
reader: &mut BufReader<R>,
buf: &mut String,
reader: &mut FileReader<R>,
section: &mut Section,
) -> ParseResult<bool> {
parse_hitobjects_body!(self, reader, buf, section)
parse_hitobjects_body!(self, reader, section)
}
};
(async $reader:ident<$inner:ident>) => {
async fn parse_hitobjects<R: $inner + Unpin>(
(async) => {
async fn parse_hitobjects<R: AsyncRead + Unpin>(
&mut self,
reader: &mut $reader<R>,
buf: &mut String,
reader: &mut FileReader<R>,
section: &mut Section,
) -> ParseResult<bool> {
parse_hitobjects_body!(self, reader, buf, section)
parse_hitobjects_body!(self, reader, section)
}
};
}
macro_rules! parse_body {
($reader:ident<$inner:ident>: $input:ident) => {{
let mut reader = $reader::new($input);
let mut buf = String::new();
($input:ident) => {{
let mut reader = FileReader::new($input);
next_line!(reader)?;
while read_line!(reader, &mut buf)? != 0 {
// Check for character U+FEFF specifically thanks to map id 797130
if !buf
.trim_matches(|c: char| c.is_whitespace() || c == '')
.is_empty()
{
break;
}
buf.clear();
if reader.is_initial_empty_line() {
next_line!(reader)?;
}
let version = match buf.find(OSU_FILE_HEADER) {
Some(idx) => buf[idx + OSU_FILE_HEADER.len()..].trim_end().parse()?,
None => return Err(ParseError::IncorrectFileHeader),
};
buf.clear();
let mut map = Beatmap {
version,
version: reader.version()?,
hit_objects: Vec::with_capacity(256),
sounds: Vec::with_capacity(256),
..Default::default()
@@ -591,22 +526,18 @@ macro_rules! parse_body {
loop {
match section {
Section::General => section!(map, parse_general, reader, buf, section),
Section::Difficulty => section!(map, parse_difficulty, reader, buf, section),
Section::TimingPoints => section!(map, parse_timingpoints, reader, buf, section),
Section::HitObjects => section!(map, parse_hitobjects, reader, buf, section),
Section::General => section!(map, parse_general, reader, section),
Section::Difficulty => section!(map, parse_difficulty, reader, section),
Section::TimingPoints => section!(map, parse_timingpoints, reader, section),
Section::HitObjects => section!(map, parse_hitobjects, reader, section),
Section::None => {
if read_line!(reader, &mut buf)? == 0 {
if next_line!(reader)? == 0 {
break;
}
let line = line_prepare!(buf);
if line.starts_with('[') && line.ends_with(']') {
section = Section::from_str(&line[1..line.len() - 1]);
if let Some(bytes) = reader.get_section() {
section = Section::from_bytes(bytes);
}
buf.clear();
}
}
}
@@ -623,19 +554,19 @@ macro_rules! parse {
/// You'll likely want to pass (a reference of) a [`File`](std::fs::File)
/// or the file's content as a slice of bytes (`&[u8]`).
pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
parse_body!(BufReader<Read>: input)
parse_body!(input)
}
};
(async $reader:ident<$inner:ident>) => {
(async) => {
/// Parse a beatmap from a `.osu` file.
///
/// As argument you can give anything that implements `tokio::io::AsyncRead`
/// or `async_std::io::Read`, depending which feature you chose.
/// You'll likely want to pass a `File`
/// or the file's content as a slice of bytes (`&[u8]`).
pub async fn parse<R: $inner + Unpin>(input: R) -> ParseResult<Self> {
parse_body!($reader<$inner>: input)
pub async fn parse<R: AsyncRead + Unpin>(input: R) -> ParseResult<Self> {
parse_body!(input)
}
};
}
@@ -652,11 +583,11 @@ macro_rules! from_path {
}
};
(async $path:ident) => {
(async) => {
/// Pass the path to a `.osu` file.
///
/// Useful when you don't want to create the file manually.
pub async fn from_path<P: AsRef<$path>>(path: P) -> ParseResult<Self> {
pub async fn from_path<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
Self::parse(File::open(path).await?).await
}
};
@@ -728,8 +659,6 @@ pub struct Beatmap {
pub stack_leniency: f32,
}
pub(crate) const OSU_FILE_HEADER: &str = "osu file format v";
impl Beatmap {
const CIRCLE_FLAG: u8 = 1 << 0;
const SLIDER_FLAG: u8 = 1 << 1;
@@ -916,34 +845,24 @@ impl Beatmap {
#[cfg(feature = "async_tokio")]
impl Beatmap {
parse!(async BufReader<AsyncRead>);
parse_general!(async BufReader<AsyncRead>);
parse_difficulty!(async BufReader<AsyncRead>);
parse_timingpoints!(async BufReader<AsyncRead>);
parse_hitobjects!(async BufReader<AsyncRead>);
parse!(async);
parse_general!(async);
parse_difficulty!(async);
parse_timingpoints!(async);
parse_hitobjects!(async);
from_path!(async Path);
from_path!(async);
}
#[cfg(feature = "async_std")]
impl Beatmap {
parse!(async AsyncBufReader<AsyncRead>);
parse_general!(async AsyncBufReader<AsyncRead>);
parse_difficulty!(async AsyncBufReader<AsyncRead>);
parse_timingpoints!(async AsyncBufReader<AsyncRead>);
parse_hitobjects!(async AsyncBufReader<AsyncRead>);
parse!(async);
parse_general!(async);
parse_difficulty!(async);
parse_timingpoints!(async);
parse_hitobjects!(async);
from_path!(async Path);
}
fn skip_line(line: &str) -> bool {
line.is_empty() || line.starts_with("//") || line.starts_with(' ') || line.starts_with('_')
}
fn split_colon(line: &str) -> Option<(&str, &str)> {
let (front, back) = line.split_once(':')?;
Some((front, back.trim()))
from_path!(async);
}
#[derive(Copy, Clone, Debug)]
@@ -956,13 +875,12 @@ enum Section {
}
impl Section {
#[inline]
fn from_str(s: &str) -> Self {
match s {
"General" => Self::General,
"Difficulty" => Self::Difficulty,
"TimingPoints" => Self::TimingPoints,
"HitObjects" => Self::HitObjects,
fn from_bytes(bytes: &[u8]) -> Self {
match bytes {
b"General" => Self::General,
b"Difficulty" => Self::Difficulty,
b"TimingPoints" => Self::TimingPoints,
b"HitObjects" => Self::HitObjects,
_ => Self::None,
}
}
+251
View File
@@ -0,0 +1,251 @@
use std::io::{Error as IoError, ErrorKind as IoErrorKind};
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
use std::io::{BufRead, BufReader, Read};
#[cfg(feature = "async_tokio")]
use tokio::io::{AsyncBufReadExt, AsyncRead as Read, BufReader};
#[cfg(feature = "async_std")]
use async_std::io::{prelude::BufReadExt, BufReader, Read};
use crate::ParseError;
#[derive(Eq, PartialEq)]
enum Encoding {
Utf8,
Utf16,
}
pub(crate) struct FileReader<R> {
buf: Vec<u8>,
encoding: Encoding,
#[cfg(feature = "async_std")]
inner: BufReader<R>,
#[cfg(feature = "async_tokio")]
inner: BufReader<R>,
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
inner: BufReader<R>,
}
macro_rules! read_until {
($self:expr) => {{
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
{
$self.inner.read_until(b'\n', &mut $self.buf)
}
#[cfg(any(feature = "async_std", feature = "async_tokio"))]
{
$self.inner.read_until(b'\n', &mut $self.buf).await
}
}};
}
macro_rules! impl_reader {
() => {
impl<R: Read> FileReader<R> {
impl_reader!(@NEW);
impl_reader!(@NEXT_LINE);
}
};
(async) => {
impl<R: Read + Unpin> FileReader<R> {
impl_reader!(@NEW);
impl_reader!(@ASYNC NEXT_LINE);
}
};
(@NEW) => {
pub(crate) fn new(src: R) -> Self {
Self {
buf: Vec::with_capacity(32),
encoding: Encoding::Utf8,
inner: BufReader::new(src),
}
}
};
(@NEXT_LINE) => {
pub(crate) fn next_line(&mut self) -> Result<usize, IoError> {
impl_reader!(@NEXT_LINE_BODY, self);
}
};
(@ASYNC NEXT_LINE) => {
pub(crate) async fn next_line(&mut self) -> Result<usize, IoError> {
impl_reader!(@NEXT_LINE_BODY, self);
}
};
(@NEXT_LINE_BODY, $self:ident) => {
loop {
$self.buf.clear();
let bytes = read_until!($self)?;
if bytes == 0 {
return Ok(bytes);
}
$self.truncate();
if !$self.buf.is_empty() {
return Ok(bytes);
}
}
};
}
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
impl_reader!();
#[cfg(feature = "async_tokio")]
impl_reader!(async);
#[cfg(feature = "async_std")]
impl_reader!(async);
impl<R> FileReader<R> {
pub(crate) fn is_initial_empty_line(&mut self) -> bool {
if self.buf.starts_with(&[239, 187, 191]) {
// UTF-8
self.buf.rotate_left(3);
self.buf.len() == 3
} else {
if self.buf.starts_with(&[255, 254]) {
// UTF-16
self.encoding = Encoding::Utf16;
// will rotate by 1 so this truncates `255`
let mut sub = 1;
if self.buf.len() >= 3 {
// truncate one `0` that was left after truncating `\n` when reading the line.
// additionally truncate `0\r` if possible.
sub += 1 + 2
* (self.buf.len() >= 4 && self.buf[self.buf.len() - 2] == b'\r') as usize;
}
self.buf.rotate_left(1);
self.buf.truncate(self.buf.len() - sub);
self.decode_utf16();
}
self.buf.is_empty()
}
}
pub(crate) fn version(&self) -> Result<u8, ParseError> {
self.buf
.iter()
.position(|&byte| byte == b'o')
.and_then(|idx| {
self.buf[idx..]
.starts_with(b"osu file format v")
.then(|| idx + 17)
})
.map(|idx| {
let mut n = 0;
for byte in &self.buf[idx..] {
if !(b'0'..=b'9').contains(byte) {
break;
}
n = 10 * n + (*byte & 0xF);
}
n
})
.ok_or(ParseError::IncorrectFileHeader)
}
/// Returns the bytes inbetween '[' and ']'.
pub(crate) fn get_section(&self) -> Option<&[u8]> {
if self.buf[0] == b'[' {
if let Some(end) = self.buf[1..].iter().position(|&byte| byte == b']') {
return Some(&self.buf[1..=end]);
}
}
None
}
/// Parse the buffer into a string, returning `None` if the UTF-8 validation fails.
pub(crate) fn get_line(&self) -> Result<&str, ParseError> {
std::str::from_utf8(&self.buf)
.map_err(|e| ParseError::IOError(IoError::new(IoErrorKind::InvalidData, Box::new(e))))
}
/// Split the buffer at the first ':', then parse the second half into a string.
///
/// Returns `None` if there is no ':' or if the second half is invalid UTF-8.
pub(crate) fn split_colon(&self) -> Option<(&[u8], &str)> {
let idx = self.buf.iter().position(|&byte| byte == b':')?;
let front = &self.buf[..idx];
let back = std::str::from_utf8(&self.buf[idx + 1..]).ok()?;
Some((front, back.trim_start()))
}
/// Truncate away trailing `\r\n`, `\n`, content after `//` and whitespace
fn truncate(&mut self) {
if self.encoding == Encoding::Utf16 {
self.decode_utf16();
}
// necessary check for the edge case `//\r\n`
if self.buf.starts_with(&[b'/', b'/']) {
return self.buf.clear();
}
// len without "//" or alternatively len without trailing "(\r)\n"
let len = self
.buf
.windows(3)
.rev()
.step_by(2)
.zip(1..)
.find_map(|(window, i)| {
if window[1] == b'/' {
if window[0] == b'/' {
return Some(self.buf.len() - 2 * i - 1);
} else if window[2] == b'/' {
return Some(self.buf.len() - 2 * i);
}
}
None
})
.unwrap_or_else(|| match &self.buf[..] {
[.., b'\r', b'\n'] => self.buf.len() - 2,
[.., b'\n'] => self.buf.len() - 1,
_ => self.buf.len(),
});
// trim whitespace
let len = self.buf[..len]
.iter()
.enumerate()
.rev()
.find_map(|(i, byte)| (!matches!(byte, b' ' | b'\t')).then(|| i + 1))
.unwrap_or(0);
self.buf.truncate(len);
}
/// Assumes the buffer is of the form `[_, a, 0, b, 0, c, ...]` so it removes
/// the first element and all the 0's, turning it into `[a, b, c, ...]`.
fn decode_utf16(&mut self) {
// remove the 0's
let limit = self.buf.len() / 2 + 1 + (self.buf.len() % 2);
for i in 2..limit {
self.buf.swap(i, i * 2 - 1);
}
self.buf.truncate(limit);
// remove the first element
// panics if buffer is empty
self.buf.rotate_left(1);
self.buf.truncate(self.buf.len() - 1);
}
}