From 1bf12f8150ace74abde0a8e988d75deb2f1de4c2 Mon Sep 17 00:00:00 2001 From: gwenn Date: Sat, 1 Aug 2015 18:51:02 +0200 Subject: [PATCH 01/17] Feature blob IO. --- Cargo.toml | 1 + src/blob.rs | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 +- 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 src/blob.rs diff --git a/Cargo.toml b/Cargo.toml index 5d05ed5..c9a45d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ name = "rusqlite" [features] load_extension = ["libsqlite3-sys/load_extension"] +blob = [] [dependencies] time = "~0.1.0" diff --git a/src/blob.rs b/src/blob.rs new file mode 100644 index 0000000..747c729 --- /dev/null +++ b/src/blob.rs @@ -0,0 +1,137 @@ +use std::mem; +use std::ptr; + +use super::ffi; +use {SqliteError, SqliteResult, SqliteConnection}; + +pub struct SqliteBlob<'conn> { + conn: &'conn SqliteConnection, + blob: *mut ffi::sqlite3_blob, + pos: i32, +} + +pub enum SeekFrom { + Start(i32), + End(i32), + Current(i32), +} + +impl SqliteConnection { + pub fn blob_open<'a>(&'a self, db: &str, table: &str, column: &str, row: i64, read_only: bool) -> SqliteResult> { + let mut c = self.db.borrow_mut(); + let mut blob = ptr::null_mut(); + let db = try!(super::str_to_cstring(db)); + let table = try!(super::str_to_cstring(table)); + let column = try!(super::str_to_cstring(column)); + let rc = unsafe{ ffi::sqlite3_blob_open(c.db(), db.as_ptr(), table.as_ptr(), column.as_ptr(), row, if read_only { 0 } else { 1 }, &mut blob) }; + c.decode_result(rc).map(|_| { + SqliteBlob{ conn: self, blob: blob, pos: 0 } + }) + } +} + +impl<'conn> SqliteBlob<'conn> { + pub fn reopen(&mut self, row: i64) -> SqliteResult<()> { + let rc = unsafe{ ffi::sqlite3_blob_reopen(self.blob, row) }; + if rc != ffi::SQLITE_OK { + return self.conn.decode_result(rc); + } + self.pos = 0; + Ok(()) + } + + pub fn size(&self) -> i32 { + unsafe{ ffi::sqlite3_blob_bytes(self.blob) } + } + + pub fn read(&mut self, buf: &mut [u8]) -> SqliteResult { + let mut n = buf.len() as i32; + let size = self.size(); + if self.pos + n > size { + n = size - self.pos; + } + if n <= 0 { + return Ok(0); + } + let rc = unsafe { ffi::sqlite3_blob_read(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) }; + self.conn.decode_result(rc).map(|_| { + self.pos += n; + n + }) + } + + pub fn write(&mut self, buf: &[u8]) -> SqliteResult { + let n = buf.len() as i32; + let size = self.size(); + if self.pos + n > size { + return Err(SqliteError{code: ffi::SQLITE_MISUSE, message: format!("pos = {} + n = {} > size = {}", self.pos, n, size)}); + } + if n <= 0 { + return Ok(0); + } + let rc = unsafe { ffi::sqlite3_blob_write(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) }; + self.conn.decode_result(rc).map(|_| { + self.pos += n; + n + }) + } + + pub fn seek(&mut self, pos: SeekFrom) { + self.pos = match pos { + SeekFrom::Start(offset) => offset, + SeekFrom::Current(offset) => self.pos + offset, + SeekFrom::End(offset) => self.size() + offset + }; + } + + pub fn close(mut self) -> SqliteResult<()> { + self.close_() + } + + fn close_(&mut self) -> SqliteResult<()> { + let rc = unsafe { ffi::sqlite3_blob_close(self.blob) }; + self.blob = ptr::null_mut(); + self.conn.decode_result(rc) + } +} + +#[allow(unused_must_use)] +impl<'conn> Drop for SqliteBlob<'conn> { + fn drop(&mut self) { + self.close_(); + } +} + +#[cfg(test)] +mod test { + use SqliteConnection; + + #[test] + fn test_blob() { + let db = SqliteConnection::open_in_memory().unwrap(); + let sql = "BEGIN; + CREATE TABLE test (content BLOB); + INSERT INTO test VALUES (ZEROBLOB(10)); + END;"; + db.execute_batch(sql).unwrap(); + let rowid = db.last_insert_rowid(); + + let mut blob = db.blob_open("main", "test", "content", rowid, false).unwrap(); + blob.write(b"Clob").unwrap(); + let err = blob.write(b"5678901"); + //writeln!(io::stderr(), "{:?}", err); + assert!(err.is_err()); + + assert!(blob.reopen(rowid).is_ok()); + assert!(blob.close().is_ok()); + + blob = db.blob_open("main", "test", "content", rowid, true).unwrap(); + let mut bytes = [0u8; 5]; + assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); + assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); + assert_eq!(0, blob.read(&mut bytes[..]).unwrap()); + + assert!(blob.reopen(rowid).is_ok()); + blob.seek(super::SeekFrom::Start(0)); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index f98780b..ef0b291 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,6 +79,7 @@ pub use transaction::{SqliteTransactionBehavior, pub mod types; mod transaction; #[cfg(feature = "load_extension")] mod load_extension_guard; +#[cfg(feature = "blob")] pub mod blob; /// A typedef of the result returned by many methods. pub type SqliteResult = Result; @@ -664,7 +665,7 @@ impl<'conn> SqliteStatement<'conn> { } /// Executes the prepared statement and maps a function over the resulting - /// rows. + /// rows. /// /// Unlike the iterator produced by `query`, the returned iterator does not expose the possibility /// for accessing stale rows. From d1f5ce24355f7fc124790e8cf7a2a7d24efa4cee Mon Sep 17 00:00:00 2001 From: Gwenael Treguier Date: Sat, 8 Aug 2015 09:39:35 +0200 Subject: [PATCH 02/17] Check when buffer is too long. --- src/blob.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/blob.rs b/src/blob.rs index 747c729..0d9691a 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -45,6 +45,12 @@ impl<'conn> SqliteBlob<'conn> { } pub fn read(&mut self, buf: &mut [u8]) -> SqliteResult { + if buf.len() > ::std::i32::MAX as usize { + return Err(SqliteError { + code: ffi::SQLITE_TOOBIG, + message: "buffer too long".to_string() + }); + } let mut n = buf.len() as i32; let size = self.size(); if self.pos + n > size { @@ -61,6 +67,12 @@ impl<'conn> SqliteBlob<'conn> { } pub fn write(&mut self, buf: &[u8]) -> SqliteResult { + if buf.len() > ::std::i32::MAX as usize { + return Err(SqliteError { + code: ffi::SQLITE_TOOBIG, + message: "buffer too long".to_string() + }); + } let n = buf.len() as i32; let size = self.size(); if self.pos + n > size { From 3b830b4bcec8c46445e256a069195e160f5f3b55 Mon Sep 17 00:00:00 2001 From: Gwenael Treguier Date: Sat, 8 Aug 2015 16:11:31 +0200 Subject: [PATCH 03/17] Add documentation --- src/blob.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/blob.rs b/src/blob.rs index 0d9691a..c404e2a 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1,15 +1,18 @@ +//! incremental BLOB I/O use std::mem; use std::ptr; use super::ffi; use {SqliteError, SqliteResult, SqliteConnection}; +/// Handle to an open BLOB pub struct SqliteBlob<'conn> { conn: &'conn SqliteConnection, blob: *mut ffi::sqlite3_blob, pos: i32, } +/// Enumeration of possible methods to seek within an BLOB. pub enum SeekFrom { Start(i32), End(i32), @@ -17,6 +20,7 @@ pub enum SeekFrom { } impl SqliteConnection { + /// Open a handle to the BLOB located in `row`, `column`, `table` in database `db` ('main', 'temp', ...) pub fn blob_open<'a>(&'a self, db: &str, table: &str, column: &str, row: i64, read_only: bool) -> SqliteResult> { let mut c = self.db.borrow_mut(); let mut blob = ptr::null_mut(); @@ -31,6 +35,7 @@ impl SqliteConnection { } impl<'conn> SqliteBlob<'conn> { + /// Move a BLOB handle to a new row pub fn reopen(&mut self, row: i64) -> SqliteResult<()> { let rc = unsafe{ ffi::sqlite3_blob_reopen(self.blob, row) }; if rc != ffi::SQLITE_OK { @@ -40,10 +45,12 @@ impl<'conn> SqliteBlob<'conn> { Ok(()) } + /// Return the size in bytes of the BLOB pub fn size(&self) -> i32 { unsafe{ ffi::sqlite3_blob_bytes(self.blob) } } + /// Read data from a BLOB incrementally pub fn read(&mut self, buf: &mut [u8]) -> SqliteResult { if buf.len() > ::std::i32::MAX as usize { return Err(SqliteError { @@ -66,6 +73,9 @@ impl<'conn> SqliteBlob<'conn> { }) } + /// Write data into a BLOB incrementally + /// + /// This function may only modify the contents of the BLOB; it is not possible to increase the size of a BLOB using this API. pub fn write(&mut self, buf: &[u8]) -> SqliteResult { if buf.len() > ::std::i32::MAX as usize { return Err(SqliteError { @@ -88,6 +98,7 @@ impl<'conn> SqliteBlob<'conn> { }) } + /// Seek to an offset, in bytes, in BLOB. pub fn seek(&mut self, pos: SeekFrom) { self.pos = match pos { SeekFrom::Start(offset) => offset, @@ -96,6 +107,7 @@ impl<'conn> SqliteBlob<'conn> { }; } + /// Close a BLOB handle pub fn close(mut self) -> SqliteResult<()> { self.close_() } From 2cf0455f8d646d9feaa6aa7d59f38e181135245b Mon Sep 17 00:00:00 2001 From: Gwenael Treguier Date: Sat, 5 Dec 2015 11:28:33 +0100 Subject: [PATCH 04/17] Add Failure documentation --- src/blob.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/blob.rs b/src/blob.rs index c404e2a..98be5ad 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -21,6 +21,11 @@ pub enum SeekFrom { impl SqliteConnection { /// Open a handle to the BLOB located in `row`, `column`, `table` in database `db` ('main', 'temp', ...) + /// + /// # Failure + /// + /// Will return `Err` if `db`/`table`/`column` cannot be converted to a C-compatible string or if the + /// underlying SQLite BLOB open call fails. pub fn blob_open<'a>(&'a self, db: &str, table: &str, column: &str, row: i64, read_only: bool) -> SqliteResult> { let mut c = self.db.borrow_mut(); let mut blob = ptr::null_mut(); @@ -36,6 +41,10 @@ impl SqliteConnection { impl<'conn> SqliteBlob<'conn> { /// Move a BLOB handle to a new row + /// + /// # Failure + /// + /// Will return `Err` if the underlying SQLite BLOB reopen call fails. pub fn reopen(&mut self, row: i64) -> SqliteResult<()> { let rc = unsafe{ ffi::sqlite3_blob_reopen(self.blob, row) }; if rc != ffi::SQLITE_OK { @@ -51,6 +60,10 @@ impl<'conn> SqliteBlob<'conn> { } /// Read data from a BLOB incrementally + /// + /// # Failure + /// + /// Will return `Err` if `buf` length > i32 max value or if the underlying SQLite read call fails. pub fn read(&mut self, buf: &mut [u8]) -> SqliteResult { if buf.len() > ::std::i32::MAX as usize { return Err(SqliteError { @@ -76,6 +89,11 @@ impl<'conn> SqliteBlob<'conn> { /// Write data into a BLOB incrementally /// /// This function may only modify the contents of the BLOB; it is not possible to increase the size of a BLOB using this API. + /// + /// # Failure + /// + /// Will return `Err` if `buf` length > i32 max value or if `buf` length + offset > BLOB size + /// or if the underlying SQLite write call fails. pub fn write(&mut self, buf: &[u8]) -> SqliteResult { if buf.len() > ::std::i32::MAX as usize { return Err(SqliteError { @@ -108,6 +126,10 @@ impl<'conn> SqliteBlob<'conn> { } /// Close a BLOB handle + /// + /// # Failure + /// + /// Will return `Err` if the underlying SQLite close call fails. pub fn close(mut self) -> SqliteResult<()> { self.close_() } From 888dce0d8fcf71ea4c94c5af4897c31b0c630aa2 Mon Sep 17 00:00:00 2001 From: Gwenael Treguier Date: Sun, 13 Dec 2015 11:05:11 +0100 Subject: [PATCH 05/17] Rustfmt --- src/blob.rs | 60 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index ebc24fb..30b4c2e 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -26,15 +26,37 @@ impl Connection { /// /// Will return `Err` if `db`/`table`/`column` cannot be converted to a C-compatible string or if the /// underlying SQLite BLOB open call fails. - pub fn blob_open<'a>(&'a self, db: &str, table: &str, column: &str, row: i64, read_only: bool) -> Result> { + pub fn blob_open<'a>(&'a self, + db: &str, + table: &str, + column: &str, + row: i64, + read_only: bool) + -> Result> { let mut c = self.db.borrow_mut(); let mut blob = ptr::null_mut(); let db = try!(super::str_to_cstring(db)); let table = try!(super::str_to_cstring(table)); let column = try!(super::str_to_cstring(column)); - let rc = unsafe{ ffi::sqlite3_blob_open(c.db(), db.as_ptr(), table.as_ptr(), column.as_ptr(), row, if read_only { 0 } else { 1 }, &mut blob) }; + let rc = unsafe { + ffi::sqlite3_blob_open(c.db(), + db.as_ptr(), + table.as_ptr(), + column.as_ptr(), + row, + if read_only { + 0 + } else { + 1 + }, + &mut blob) + }; c.decode_result(rc).map(|_| { - Blob{ conn: self, blob: blob, pos: 0 } + Blob { + conn: self, + blob: blob, + pos: 0, + } }) } } @@ -46,7 +68,7 @@ impl<'conn> Blob<'conn> { /// /// Will return `Err` if the underlying SQLite BLOB reopen call fails. pub fn reopen(&mut self, row: i64) -> Result<()> { - let rc = unsafe{ ffi::sqlite3_blob_reopen(self.blob, row) }; + let rc = unsafe { ffi::sqlite3_blob_reopen(self.blob, row) }; if rc != ffi::SQLITE_OK { return self.conn.decode_result(rc); } @@ -56,7 +78,7 @@ impl<'conn> Blob<'conn> { /// Return the size in bytes of the BLOB pub fn size(&self) -> i32 { - unsafe{ ffi::sqlite3_blob_bytes(self.blob) } + unsafe { ffi::sqlite3_blob_bytes(self.blob) } } /// Read data from a BLOB incrementally @@ -68,7 +90,7 @@ impl<'conn> Blob<'conn> { if buf.len() > ::std::i32::MAX as usize { return Err(Error { code: ffi::SQLITE_TOOBIG, - message: "buffer too long".to_string() + message: "buffer too long".to_string(), }); } let mut n = buf.len() as i32; @@ -79,7 +101,9 @@ impl<'conn> Blob<'conn> { if n <= 0 { return Ok(0); } - let rc = unsafe { ffi::sqlite3_blob_read(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) }; + let rc = unsafe { + ffi::sqlite3_blob_read(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) + }; self.conn.decode_result(rc).map(|_| { self.pos += n; n @@ -98,18 +122,23 @@ impl<'conn> Blob<'conn> { if buf.len() > ::std::i32::MAX as usize { return Err(Error { code: ffi::SQLITE_TOOBIG, - message: "buffer too long".to_string() + message: "buffer too long".to_string(), }); } let n = buf.len() as i32; let size = self.size(); if self.pos + n > size { - return Err(Error{code: ffi::SQLITE_MISUSE, message: format!("pos = {} + n = {} > size = {}", self.pos, n, size)}); + return Err(Error { + code: ffi::SQLITE_MISUSE, + message: format!("pos = {} + n = {} > size = {}", self.pos, n, size), + }); } if n <= 0 { return Ok(0); } - let rc = unsafe { ffi::sqlite3_blob_write(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) }; + let rc = unsafe { + ffi::sqlite3_blob_write(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) + }; self.conn.decode_result(rc).map(|_| { self.pos += n; n @@ -118,10 +147,10 @@ impl<'conn> Blob<'conn> { /// Seek to an offset, in bytes, in BLOB. pub fn seek(&mut self, pos: SeekFrom) { - self.pos = match pos { + self.pos = match pos { SeekFrom::Start(offset) => offset, SeekFrom::Current(offset) => self.pos + offset, - SeekFrom::End(offset) => self.size() + offset + SeekFrom::End(offset) => self.size() + offset, }; } @@ -152,7 +181,8 @@ impl<'conn> Drop for Blob<'conn> { mod test { use Connection; - #[test] + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] fn test_blob() { let db = Connection::open_in_memory().unwrap(); let sql = "BEGIN; @@ -165,7 +195,7 @@ mod test { let mut blob = db.blob_open("main", "test", "content", rowid, false).unwrap(); blob.write(b"Clob").unwrap(); let err = blob.write(b"5678901"); - //writeln!(io::stderr(), "{:?}", err); + // writeln!(io::stderr(), "{:?}", err); assert!(err.is_err()); assert!(blob.reopen(rowid).is_ok()); @@ -180,4 +210,4 @@ mod test { assert!(blob.reopen(rowid).is_ok()); blob.seek(super::SeekFrom::Start(0)); } -} \ No newline at end of file +} From 5ac5f3e9b597660ddc28df702650b08ce02469fc Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 14 Dec 2015 13:38:36 -0500 Subject: [PATCH 06/17] Make `blob_open` take a `DatabaseName` instead of a str. --- src/blob.rs | 14 +++++++------- src/lib.rs | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index 30b4c2e..2227045 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -3,7 +3,7 @@ use std::mem; use std::ptr; use super::ffi; -use {Error, Result, Connection}; +use {Error, Result, Connection, DatabaseName}; /// Handle to an open BLOB pub struct Blob<'conn> { @@ -20,14 +20,14 @@ pub enum SeekFrom { } impl Connection { - /// Open a handle to the BLOB located in `row`, `column`, `table` in database `db` ('main', 'temp', ...) + /// Open a handle to the BLOB located in `row`, `column`, `table` in database `db`. /// /// # Failure /// /// Will return `Err` if `db`/`table`/`column` cannot be converted to a C-compatible string or if the /// underlying SQLite BLOB open call fails. pub fn blob_open<'a>(&'a self, - db: &str, + db: DatabaseName, table: &str, column: &str, row: i64, @@ -35,7 +35,7 @@ impl Connection { -> Result> { let mut c = self.db.borrow_mut(); let mut blob = ptr::null_mut(); - let db = try!(super::str_to_cstring(db)); + let db = try!(db.to_cstring()); let table = try!(super::str_to_cstring(table)); let column = try!(super::str_to_cstring(column)); let rc = unsafe { @@ -179,7 +179,7 @@ impl<'conn> Drop for Blob<'conn> { #[cfg(test)] mod test { - use Connection; + use {Connection, DatabaseName}; #[test] #[cfg_attr(rustfmt, rustfmt_skip)] @@ -192,7 +192,7 @@ mod test { db.execute_batch(sql).unwrap(); let rowid = db.last_insert_rowid(); - let mut blob = db.blob_open("main", "test", "content", rowid, false).unwrap(); + let mut blob = db.blob_open(DatabaseName::Main, "test", "content", rowid, false).unwrap(); blob.write(b"Clob").unwrap(); let err = blob.write(b"5678901"); // writeln!(io::stderr(), "{:?}", err); @@ -201,7 +201,7 @@ mod test { assert!(blob.reopen(rowid).is_ok()); assert!(blob.close().is_ok()); - blob = db.blob_open("main", "test", "content", rowid, true).unwrap(); + blob = db.blob_open(DatabaseName::Main, "test", "content", rowid, true).unwrap(); let mut bytes = [0u8; 5]; assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); diff --git a/src/lib.rs b/src/lib.rs index f0451c2..e41dc91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,9 +171,9 @@ pub enum DatabaseName<'a> { Attached(&'a str), } -// Currently DatabaseName is only used by the backup mod, so hide this (private) +// Currently DatabaseName is only used by the backup and blob mods, so hide this (private) // impl to avoid dead code warnings. -#[cfg(feature = "backup")] +#[cfg(any(feature = "backup", feature = "blob"))] impl<'a> DatabaseName<'a> { fn to_cstring(self) -> Result { use self::DatabaseName::{Main, Temp, Attached}; From f290ce11abc8f5e91208a1c42c31271630a57369 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 14 Dec 2015 13:45:44 -0500 Subject: [PATCH 07/17] Move Blob's `read` to an impl of std::io::Read. --- src/blob.rs | 62 ++++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index 2227045..a344450 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1,4 +1,5 @@ //! incremental BLOB I/O +use std::io; use std::mem; use std::ptr; @@ -81,35 +82,6 @@ impl<'conn> Blob<'conn> { unsafe { ffi::sqlite3_blob_bytes(self.blob) } } - /// Read data from a BLOB incrementally - /// - /// # Failure - /// - /// Will return `Err` if `buf` length > i32 max value or if the underlying SQLite read call fails. - pub fn read(&mut self, buf: &mut [u8]) -> Result { - if buf.len() > ::std::i32::MAX as usize { - return Err(Error { - code: ffi::SQLITE_TOOBIG, - message: "buffer too long".to_string(), - }); - } - let mut n = buf.len() as i32; - let size = self.size(); - if self.pos + n > size { - n = size - self.pos; - } - if n <= 0 { - return Ok(0); - } - let rc = unsafe { - ffi::sqlite3_blob_read(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) - }; - self.conn.decode_result(rc).map(|_| { - self.pos += n; - n - }) - } - /// Write data into a BLOB incrementally /// /// This function may only modify the contents of the BLOB; it is not possible to increase the size of a BLOB using this API. @@ -170,6 +142,37 @@ impl<'conn> Blob<'conn> { } } +impl<'conn> io::Read for Blob<'conn> { + /// Read data from a BLOB incrementally + /// + /// # Failure + /// + /// Will return `Err` if `buf` length > i32 max value or if the underlying SQLite read call fails. + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if buf.len() > ::std::i32::MAX as usize { + return Err(io::Error::new(io::ErrorKind::InvalidInput, Error { + code: ffi::SQLITE_TOOBIG, + message: "buffer too long".to_string(), + })); + } + let mut n = buf.len() as i32; + let size = self.size(); + if self.pos + n > size { + n = size - self.pos; + } + if n <= 0 { + return Ok(0); + } + let rc = unsafe { + ffi::sqlite3_blob_read(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) + }; + self.conn.decode_result(rc).map(|_| { + self.pos += n; + n as usize + }).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + } +} + #[allow(unused_must_use)] impl<'conn> Drop for Blob<'conn> { fn drop(&mut self) { @@ -179,6 +182,7 @@ impl<'conn> Drop for Blob<'conn> { #[cfg(test)] mod test { + use std::io::Read; use {Connection, DatabaseName}; #[test] From 439f8583e70d1d477910eebd249d226b323fc724 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 14 Dec 2015 13:50:18 -0500 Subject: [PATCH 08/17] Move Blob's `write` to an impl of std::io::Write. --- src/blob.rs | 78 ++++++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index a344450..6799592 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -82,41 +82,6 @@ impl<'conn> Blob<'conn> { unsafe { ffi::sqlite3_blob_bytes(self.blob) } } - /// Write data into a BLOB incrementally - /// - /// This function may only modify the contents of the BLOB; it is not possible to increase the size of a BLOB using this API. - /// - /// # Failure - /// - /// Will return `Err` if `buf` length > i32 max value or if `buf` length + offset > BLOB size - /// or if the underlying SQLite write call fails. - pub fn write(&mut self, buf: &[u8]) -> Result { - if buf.len() > ::std::i32::MAX as usize { - return Err(Error { - code: ffi::SQLITE_TOOBIG, - message: "buffer too long".to_string(), - }); - } - let n = buf.len() as i32; - let size = self.size(); - if self.pos + n > size { - return Err(Error { - code: ffi::SQLITE_MISUSE, - message: format!("pos = {} + n = {} > size = {}", self.pos, n, size), - }); - } - if n <= 0 { - return Ok(0); - } - let rc = unsafe { - ffi::sqlite3_blob_write(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) - }; - self.conn.decode_result(rc).map(|_| { - self.pos += n; - n - }) - } - /// Seek to an offset, in bytes, in BLOB. pub fn seek(&mut self, pos: SeekFrom) { self.pos = match pos { @@ -173,6 +138,47 @@ impl<'conn> io::Read for Blob<'conn> { } } +impl<'conn> io::Write for Blob<'conn> { + /// Write data into a BLOB incrementally + /// + /// This function may only modify the contents of the BLOB; it is not possible to increase the size of a BLOB using this API. + /// + /// # Failure + /// + /// Will return `Err` if `buf` length > i32 max value or if `buf` length + offset > BLOB size + /// or if the underlying SQLite write call fails. + fn write(&mut self, buf: &[u8]) -> io::Result { + if buf.len() > ::std::i32::MAX as usize { + return Err(io::Error::new(io::ErrorKind::InvalidInput, Error { + code: ffi::SQLITE_TOOBIG, + message: "buffer too long".to_string(), + })); + } + let n = buf.len() as i32; + let size = self.size(); + if self.pos + n > size { + return Err(io::Error::new(io::ErrorKind::Other, Error { + code: ffi::SQLITE_MISUSE, + message: format!("pos = {} + n = {} > size = {}", self.pos, n, size), + })); + } + if n <= 0 { + return Ok(0); + } + let rc = unsafe { + ffi::sqlite3_blob_write(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) + }; + self.conn.decode_result(rc).map(|_| { + self.pos += n; + n as usize + }).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + #[allow(unused_must_use)] impl<'conn> Drop for Blob<'conn> { fn drop(&mut self) { @@ -182,7 +188,7 @@ impl<'conn> Drop for Blob<'conn> { #[cfg(test)] mod test { - use std::io::Read; + use std::io::{Read, Write}; use {Connection, DatabaseName}; #[test] From 7a7d13f5200af8e71d86b1068d1352e6935f11b4 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 14 Dec 2015 14:12:11 -0500 Subject: [PATCH 09/17] Move Blob's `seek` to an impl of std::io::Seek. --- src/blob.rs | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index 6799592..47a1a28 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -13,13 +13,6 @@ pub struct Blob<'conn> { pos: i32, } -/// Enumeration of possible methods to seek within an BLOB. -pub enum SeekFrom { - Start(i32), - End(i32), - Current(i32), -} - impl Connection { /// Open a handle to the BLOB located in `row`, `column`, `table` in database `db`. /// @@ -82,15 +75,6 @@ impl<'conn> Blob<'conn> { unsafe { ffi::sqlite3_blob_bytes(self.blob) } } - /// Seek to an offset, in bytes, in BLOB. - pub fn seek(&mut self, pos: SeekFrom) { - self.pos = match pos { - SeekFrom::Start(offset) => offset, - SeekFrom::Current(offset) => self.pos + offset, - SeekFrom::End(offset) => self.size() + offset, - }; - } - /// Close a BLOB handle /// /// # Failure @@ -179,6 +163,26 @@ impl<'conn> io::Write for Blob<'conn> { } } +impl<'conn> io::Seek for Blob<'conn> { + /// Seek to an offset, in bytes, in BLOB. + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + let pos = match pos { + io::SeekFrom::Start(offset) => offset as i64, + io::SeekFrom::Current(offset) => self.pos as i64 + offset, + io::SeekFrom::End(offset) => self.size() as i64 + offset, + }; + + if pos < 0 { + Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid seek to negative position")) + } else if pos > ::std::i32::MAX as i64 { + Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid seek to position > i32::MAX")) + } else { + self.pos = pos as i32; + Ok(pos as u64) + } + } +} + #[allow(unused_must_use)] impl<'conn> Drop for Blob<'conn> { fn drop(&mut self) { @@ -188,7 +192,7 @@ impl<'conn> Drop for Blob<'conn> { #[cfg(test)] mod test { - use std::io::{Read, Write}; + use std::io::{Read, Write, Seek, SeekFrom}; use {Connection, DatabaseName}; #[test] @@ -218,6 +222,6 @@ mod test { assert_eq!(0, blob.read(&mut bytes[..]).unwrap()); assert!(blob.reopen(rowid).is_ok()); - blob.seek(super::SeekFrom::Start(0)); + blob.seek(SeekFrom::Start(0)); } } From a43da3ef73a3d6f98d905c4f87dd7b67bae9c7ce Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 14 Dec 2015 14:13:14 -0500 Subject: [PATCH 10/17] rustfmt --- src/blob.rs | 58 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index 47a1a28..a12df37 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -99,10 +99,11 @@ impl<'conn> io::Read for Blob<'conn> { /// Will return `Err` if `buf` length > i32 max value or if the underlying SQLite read call fails. fn read(&mut self, buf: &mut [u8]) -> io::Result { if buf.len() > ::std::i32::MAX as usize { - return Err(io::Error::new(io::ErrorKind::InvalidInput, Error { - code: ffi::SQLITE_TOOBIG, - message: "buffer too long".to_string(), - })); + return Err(io::Error::new(io::ErrorKind::InvalidInput, + Error { + code: ffi::SQLITE_TOOBIG, + message: "buffer too long".to_string(), + })); } let mut n = buf.len() as i32; let size = self.size(); @@ -115,10 +116,13 @@ impl<'conn> io::Read for Blob<'conn> { let rc = unsafe { ffi::sqlite3_blob_read(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) }; - self.conn.decode_result(rc).map(|_| { - self.pos += n; - n as usize - }).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + self.conn + .decode_result(rc) + .map(|_| { + self.pos += n; + n as usize + }) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) } } @@ -133,18 +137,23 @@ impl<'conn> io::Write for Blob<'conn> { /// or if the underlying SQLite write call fails. fn write(&mut self, buf: &[u8]) -> io::Result { if buf.len() > ::std::i32::MAX as usize { - return Err(io::Error::new(io::ErrorKind::InvalidInput, Error { - code: ffi::SQLITE_TOOBIG, - message: "buffer too long".to_string(), - })); + return Err(io::Error::new(io::ErrorKind::InvalidInput, + Error { + code: ffi::SQLITE_TOOBIG, + message: "buffer too long".to_string(), + })); } let n = buf.len() as i32; let size = self.size(); if self.pos + n > size { - return Err(io::Error::new(io::ErrorKind::Other, Error { - code: ffi::SQLITE_MISUSE, - message: format!("pos = {} + n = {} > size = {}", self.pos, n, size), - })); + return Err(io::Error::new(io::ErrorKind::Other, + Error { + code: ffi::SQLITE_MISUSE, + message: format!("pos = {} + n = {} > size = {}", + self.pos, + n, + size), + })); } if n <= 0 { return Ok(0); @@ -152,10 +161,13 @@ impl<'conn> io::Write for Blob<'conn> { let rc = unsafe { ffi::sqlite3_blob_write(self.blob, mem::transmute(buf.as_ptr()), n, self.pos) }; - self.conn.decode_result(rc).map(|_| { - self.pos += n; - n as usize - }).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + self.conn + .decode_result(rc) + .map(|_| { + self.pos += n; + n as usize + }) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) } fn flush(&mut self) -> io::Result<()> { @@ -173,9 +185,11 @@ impl<'conn> io::Seek for Blob<'conn> { }; if pos < 0 { - Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid seek to negative position")) + Err(io::Error::new(io::ErrorKind::InvalidInput, + "invalid seek to negative position")) } else if pos > ::std::i32::MAX as i64 { - Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid seek to position > i32::MAX")) + Err(io::Error::new(io::ErrorKind::InvalidInput, + "invalid seek to position > i32::MAX")) } else { self.pos = pos as i32; Ok(pos as u64) From c15a8dba793d07ba5cc53e444482e07bb60351a5 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 14 Dec 2015 15:03:29 -0500 Subject: [PATCH 11/17] More extensive unit tests for Blob. --- src/blob.rs | 78 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index a12df37..b4f702e 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -206,36 +206,78 @@ impl<'conn> Drop for Blob<'conn> { #[cfg(test)] mod test { - use std::io::{Read, Write, Seek, SeekFrom}; - use {Connection, DatabaseName}; + use std::io::{BufReader, BufRead, Read, Write, Seek, SeekFrom}; + use {Connection, DatabaseName, Result}; + + #[cfg_attr(rustfmt, rustfmt_skip)] + fn db_with_test_blob() -> Result<(Connection, i64)> { + let db = try!(Connection::open_in_memory()); + let sql = "BEGIN; + CREATE TABLE test (content BLOB); + INSERT INTO test VALUES (ZEROBLOB(10)); + END;"; + try!(db.execute_batch(sql)); + let rowid = db.last_insert_rowid(); + Ok((db, rowid)) + } #[test] - #[cfg_attr(rustfmt, rustfmt_skip)] fn test_blob() { - let db = Connection::open_in_memory().unwrap(); - let sql = "BEGIN; - CREATE TABLE test (content BLOB); - INSERT INTO test VALUES (ZEROBLOB(10)); - END;"; - db.execute_batch(sql).unwrap(); - let rowid = db.last_insert_rowid(); + let (db, rowid) = db_with_test_blob().unwrap(); let mut blob = db.blob_open(DatabaseName::Main, "test", "content", rowid, false).unwrap(); - blob.write(b"Clob").unwrap(); - let err = blob.write(b"5678901"); - // writeln!(io::stderr(), "{:?}", err); - assert!(err.is_err()); + assert_eq!(4, blob.write(b"Clob").unwrap()); + assert!(blob.write(b"5678901").is_err()); // cannot write past 10 + assert_eq!(4, blob.write(b"5678").unwrap()); - assert!(blob.reopen(rowid).is_ok()); - assert!(blob.close().is_ok()); + blob.reopen(rowid).unwrap(); + blob.close().unwrap(); blob = db.blob_open(DatabaseName::Main, "test", "content", rowid, true).unwrap(); let mut bytes = [0u8; 5]; assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); + assert_eq!(&bytes, b"Clob5"); assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); + assert_eq!(&bytes, b"678\0\0"); assert_eq!(0, blob.read(&mut bytes[..]).unwrap()); - assert!(blob.reopen(rowid).is_ok()); - blob.seek(SeekFrom::Start(0)); + blob.seek(SeekFrom::Start(2)).unwrap(); + assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); + assert_eq!(&bytes, b"ob567"); + + blob.seek(SeekFrom::Current(-6)).unwrap(); + assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); + assert_eq!(&bytes, b"lob56"); + + blob.seek(SeekFrom::End(-6)).unwrap(); + assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); + assert_eq!(&bytes, b"5678\0"); + + blob.reopen(rowid).unwrap(); + assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); + assert_eq!(&bytes, b"Clob5"); + } + + #[test] + fn test_blob_in_bufreader() { + let (db, rowid) = db_with_test_blob().unwrap(); + + let mut blob = db.blob_open(DatabaseName::Main, "test", "content", rowid, false).unwrap(); + assert_eq!(8, blob.write(b"one\ntwo\n").unwrap()); + + blob.reopen(rowid).unwrap(); + let mut reader = BufReader::new(blob); + + let mut line = String::new(); + assert_eq!(4, reader.read_line(&mut line).unwrap()); + assert_eq!("one\n", line); + + line.truncate(0); + assert_eq!(4, reader.read_line(&mut line).unwrap()); + assert_eq!("two\n", line); + + line.truncate(0); + assert_eq!(2, reader.read_line(&mut line).unwrap()); + assert_eq!("\0\0", line); } } From af9b45851aec5c8a923b46a465fbe33a06085e36 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 14 Dec 2015 16:06:53 -0500 Subject: [PATCH 12/17] Truncate instead of erroring if asked to read/write too much data from a Blob. --- src/blob.rs | 103 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 59 insertions(+), 44 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index b4f702e..c7e3b51 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1,10 +1,11 @@ //! incremental BLOB I/O use std::io; +use std::cmp::min; use std::mem; use std::ptr; use super::ffi; -use {Error, Result, Connection, DatabaseName}; +use {Result, Connection, DatabaseName}; /// Handle to an open BLOB pub struct Blob<'conn> { @@ -96,20 +97,10 @@ impl<'conn> io::Read for Blob<'conn> { /// /// # Failure /// - /// Will return `Err` if `buf` length > i32 max value or if the underlying SQLite read call fails. + /// Will return `Err` if the underlying SQLite read call fails. fn read(&mut self, buf: &mut [u8]) -> io::Result { - if buf.len() > ::std::i32::MAX as usize { - return Err(io::Error::new(io::ErrorKind::InvalidInput, - Error { - code: ffi::SQLITE_TOOBIG, - message: "buffer too long".to_string(), - })); - } - let mut n = buf.len() as i32; - let size = self.size(); - if self.pos + n > size { - n = size - self.pos; - } + let max_allowed_len = (self.size() - self.pos) as usize; + let n = min(buf.len(), max_allowed_len) as i32; if n <= 0 { return Ok(0); } @@ -129,32 +120,15 @@ impl<'conn> io::Read for Blob<'conn> { impl<'conn> io::Write for Blob<'conn> { /// Write data into a BLOB incrementally /// - /// This function may only modify the contents of the BLOB; it is not possible to increase the size of a BLOB using this API. + /// This function may only modify the contents of the BLOB; it is not possible to increase + /// the size of a BLOB using this API. /// /// # Failure /// - /// Will return `Err` if `buf` length > i32 max value or if `buf` length + offset > BLOB size - /// or if the underlying SQLite write call fails. + /// Will return `Err` if the underlying SQLite write call fails. fn write(&mut self, buf: &[u8]) -> io::Result { - if buf.len() > ::std::i32::MAX as usize { - return Err(io::Error::new(io::ErrorKind::InvalidInput, - Error { - code: ffi::SQLITE_TOOBIG, - message: "buffer too long".to_string(), - })); - } - let n = buf.len() as i32; - let size = self.size(); - if self.pos + n > size { - return Err(io::Error::new(io::ErrorKind::Other, - Error { - code: ffi::SQLITE_MISUSE, - message: format!("pos = {} + n = {} > size = {}", - self.pos, - n, - size), - })); - } + let max_allowed_len = (self.size() - self.pos) as usize; + let n = min(buf.len(), max_allowed_len) as i32; if n <= 0 { return Ok(0); } @@ -206,7 +180,7 @@ impl<'conn> Drop for Blob<'conn> { #[cfg(test)] mod test { - use std::io::{BufReader, BufRead, Read, Write, Seek, SeekFrom}; + use std::io::{BufReader, BufRead, BufWriter, Read, Write, Seek, SeekFrom}; use {Connection, DatabaseName, Result}; #[cfg_attr(rustfmt, rustfmt_skip)] @@ -227,8 +201,8 @@ mod test { let mut blob = db.blob_open(DatabaseName::Main, "test", "content", rowid, false).unwrap(); assert_eq!(4, blob.write(b"Clob").unwrap()); - assert!(blob.write(b"5678901").is_err()); // cannot write past 10 - assert_eq!(4, blob.write(b"5678").unwrap()); + assert_eq!(6, blob.write(b"567890xxxxxx").unwrap()); // cannot write past 10 + assert_eq!(0, blob.write(b"5678").unwrap()); // still cannot write past 10 blob.reopen(rowid).unwrap(); blob.close().unwrap(); @@ -238,20 +212,21 @@ mod test { assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); assert_eq!(&bytes, b"Clob5"); assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); - assert_eq!(&bytes, b"678\0\0"); + assert_eq!(&bytes, b"67890"); assert_eq!(0, blob.read(&mut bytes[..]).unwrap()); blob.seek(SeekFrom::Start(2)).unwrap(); assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); assert_eq!(&bytes, b"ob567"); - blob.seek(SeekFrom::Current(-6)).unwrap(); - assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); - assert_eq!(&bytes, b"lob56"); + // only first 4 bytes of `bytes` should be read into + blob.seek(SeekFrom::Current(-1)).unwrap(); + assert_eq!(4, blob.read(&mut bytes[..]).unwrap()); + assert_eq!(&bytes, b"78907"); blob.seek(SeekFrom::End(-6)).unwrap(); assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); - assert_eq!(&bytes, b"5678\0"); + assert_eq!(&bytes, b"56789"); blob.reopen(rowid).unwrap(); assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); @@ -280,4 +255,44 @@ mod test { assert_eq!(2, reader.read_line(&mut line).unwrap()); assert_eq!("\0\0", line); } + + #[test] + fn test_blob_in_bufwriter() { + let (db, rowid) = db_with_test_blob().unwrap(); + + { + let blob = db.blob_open(DatabaseName::Main, "test", "content", rowid, false).unwrap(); + let mut writer = BufWriter::new(blob); + + // trying to write too much and then flush should fail + assert_eq!(8, writer.write(b"01234567").unwrap()); + assert_eq!(8, writer.write(b"01234567").unwrap()); + assert!(writer.flush().is_err()); + } + + { + // ... but it should've written the first 10 bytes + let mut blob = db.blob_open(DatabaseName::Main, "test", "content", rowid, false).unwrap(); + let mut bytes = [0u8; 10]; + assert_eq!(10, blob.read(&mut bytes[..]).unwrap()); + assert_eq!(b"0123456701", &bytes); + } + + { + let blob = db.blob_open(DatabaseName::Main, "test", "content", rowid, false).unwrap(); + let mut writer = BufWriter::new(blob); + + // trying to write_all too much should fail + writer.write_all(b"aaaaaaaaaabbbbb").unwrap(); + assert!(writer.flush().is_err()); + } + + { + // ... but it should've written the first 10 bytes + let mut blob = db.blob_open(DatabaseName::Main, "test", "content", rowid, false).unwrap(); + let mut bytes = [0u8; 10]; + assert_eq!(10, blob.read(&mut bytes[..]).unwrap()); + assert_eq!(b"aaaaaaaaaa", &bytes); + } + } } From 900c241c4e66da2ea01a2bdeed455c8d92fea3d3 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 14 Dec 2015 16:11:07 -0500 Subject: [PATCH 13/17] Fix logic in seek to disallow seeking past the end --- src/blob.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index c7e3b51..d0eebc7 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -161,9 +161,9 @@ impl<'conn> io::Seek for Blob<'conn> { if pos < 0 { Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid seek to negative position")) - } else if pos > ::std::i32::MAX as i64 { + } else if pos > self.size() as i64 { Err(io::Error::new(io::ErrorKind::InvalidInput, - "invalid seek to position > i32::MAX")) + "invalid seek to position past end of blob")) } else { self.pos = pos as i32; Ok(pos as u64) @@ -231,6 +231,11 @@ mod test { blob.reopen(rowid).unwrap(); assert_eq!(5, blob.read(&mut bytes[..]).unwrap()); assert_eq!(&bytes, b"Clob5"); + + // should not be able to seek negative or past end + assert!(blob.seek(SeekFrom::Current(-20)).is_err()); + assert!(blob.seek(SeekFrom::End(0)).is_ok()); + assert!(blob.seek(SeekFrom::Current(1)).is_err()); } #[test] From d24968db157b52430b0ee6ff58bf725b6a6684be Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 14 Dec 2015 16:21:38 -0500 Subject: [PATCH 14/17] Expand comments. --- src/blob.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/src/blob.rs b/src/blob.rs index d0eebc7..520280e 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1,4 +1,52 @@ -//! incremental BLOB I/O +//! Incremental BLOB I/O. +//! +//! Note that SQLite does not provide API-level access to change the size of a BLOB; that must +//! be performed through SQL statements. +//! +//! `Blob` conforms to `std::io::Read`, `std::io::Write`, and `std::io::Seek`, so it plays +//! nicely with other types that build on these (such as `std::io::BufReader` and +//! `std::io::BufWriter`). However, you must be careful with the size of the blob. For example, +//! when using a `BufWriter`, the `BufWriter` will accept more data than the `Blob` will allow, +//! so make sure to call `flush` and check for errors. (See the unit tests in this module for +//! an example.) +//! +//! ## Example +//! +//! ```rust +//! extern crate libsqlite3_sys; +//! extern crate rusqlite; +//! +//! use rusqlite::{Connection, DatabaseName}; +//! use std::io::{Read, Write, Seek, SeekFrom}; +//! +//! fn main() { +//! let db = Connection::open_in_memory().unwrap(); +//! db.execute_batch("CREATE TABLE test (content BLOB);").unwrap(); +//! db.execute("INSERT INTO test (content) VALUES (ZEROBLOB(10))", &[]).unwrap(); +//! +//! let rowid = db.last_insert_rowid(); +//! let mut blob = db.blob_open(DatabaseName::Main, "test", "content", rowid, false).unwrap(); +//! +//! // Make sure to test that the number of bytes written matches what you expect; +//! // if you try to write too much, the data will be truncated to the size of the BLOB. +//! let bytes_written = blob.write(b"01234567").unwrap(); +//! assert_eq!(bytes_written, 8); +//! +//! // Same guidance - make sure you check the number of bytes read! +//! blob.seek(SeekFrom::Start(0)).unwrap(); +//! let mut buf = [0u8; 20]; +//! let bytes_read = blob.read(&mut buf[..]).unwrap(); +//! assert_eq!(bytes_read, 10); // note we read 10 bytes because the blob has size 10 +//! +//! db.execute("INSERT INTO test (content) VALUES (ZEROBLOB(64))", &[]).unwrap(); +//! +//! // given a new row ID, we can reopen the blob on that row +//! let rowid = db.last_insert_rowid(); +//! blob.reopen(rowid).unwrap(); +//! +//! assert_eq!(blob.size(), 64); +//! } +//! ``` use std::io; use std::cmp::min; use std::mem; @@ -7,7 +55,7 @@ use std::ptr; use super::ffi; use {Result, Connection, DatabaseName}; -/// Handle to an open BLOB +/// Handle to an open BLOB. pub struct Blob<'conn> { conn: &'conn Connection, blob: *mut ffi::sqlite3_blob, @@ -19,8 +67,8 @@ impl Connection { /// /// # Failure /// - /// Will return `Err` if `db`/`table`/`column` cannot be converted to a C-compatible string or if the - /// underlying SQLite BLOB open call fails. + /// Will return `Err` if `db`/`table`/`column` cannot be converted to a C-compatible string + /// or if the underlying SQLite BLOB open call fails. pub fn blob_open<'a>(&'a self, db: DatabaseName, table: &str, @@ -57,7 +105,7 @@ impl Connection { } impl<'conn> Blob<'conn> { - /// Move a BLOB handle to a new row + /// Move a BLOB handle to a new row. /// /// # Failure /// @@ -71,12 +119,15 @@ impl<'conn> Blob<'conn> { Ok(()) } - /// Return the size in bytes of the BLOB + /// Return the size in bytes of the BLOB. pub fn size(&self) -> i32 { unsafe { ffi::sqlite3_blob_bytes(self.blob) } } - /// Close a BLOB handle + /// Close a BLOB handle. + /// + /// Calling `close` explicitly is not required (the BLOB will be closed when the + /// `Blob` is dropped), but it is available so you can get any errors that occur. /// /// # Failure /// @@ -93,7 +144,8 @@ impl<'conn> Blob<'conn> { } impl<'conn> io::Read for Blob<'conn> { - /// Read data from a BLOB incrementally + /// Read data from a BLOB incrementally. Will return Ok(0) if the end of the blob + /// has been reached. /// /// # Failure /// @@ -118,7 +170,8 @@ impl<'conn> io::Read for Blob<'conn> { } impl<'conn> io::Write for Blob<'conn> { - /// Write data into a BLOB incrementally + /// Write data into a BLOB incrementally. Will return `Ok(0)` if the end of the blob + /// has been reached. /// /// This function may only modify the contents of the BLOB; it is not possible to increase /// the size of a BLOB using this API. From f8540062f863a265738a9b8dccfce189c1c033e8 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 14 Dec 2015 16:24:11 -0500 Subject: [PATCH 15/17] Add blob feature to README and Changelog --- Changelog.md | 1 + README.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index 00f2b20..e47480a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -18,6 +18,7 @@ The old, prefixed names are still exported but are deprecated. * Adds a variety of `..._named` methods for executing queries using named placeholder parameters. * Adds `backup` feature that exposes SQLite's online backup API. +* Adds `blob` feature that exposes SQLite's Incremental I/O for BLOB API. * Adds `functions` feature that allows user-defined scalar functions to be added to open `SqliteConnection`s. diff --git a/README.md b/README.md index 30e91c7..a813174 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ features](http://doc.crates.io/manifest.html#the-features-section). They are: allows you to load Rust closures into SQLite connections for use in queries. * [`trace`](http://jgallagher.github.io/rusqlite/rusqlite/trace/index.html) allows hooks into SQLite's tracing and profiling APIs. +* [`blob`](http://jgallagher.github.io/rusqlite/rusqlite/blob/index.html) + gives `std::io::{Read, Write, Seek}` access to SQL BLOBs. ### Design of Rows and Row From 3482e1c4534a2cb6d1897fac8f692ab60fbaa979 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Tue, 15 Dec 2015 13:39:47 -0500 Subject: [PATCH 16/17] Add unit test confirming write_all to a Blob fails if given too much data. --- src/blob.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/blob.rs b/src/blob.rs index 520280e..e8e7851 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -289,6 +289,11 @@ mod test { assert!(blob.seek(SeekFrom::Current(-20)).is_err()); assert!(blob.seek(SeekFrom::End(0)).is_ok()); assert!(blob.seek(SeekFrom::Current(1)).is_err()); + + // write_all should detect when we return Ok(0) because there is no space left, + // and return a write error + blob.reopen(rowid).unwrap(); + assert!(blob.write_all(b"0123456789x").is_err()); } #[test] From c63238108c8fa3f825a66ca236a5301e5b1265e6 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Tue, 15 Dec 2015 14:24:05 -0500 Subject: [PATCH 17/17] Add comment to write recommending write_all. --- src/blob.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/blob.rs b/src/blob.rs index e8e7851..a13f3e7 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -171,7 +171,8 @@ impl<'conn> io::Read for Blob<'conn> { impl<'conn> io::Write for Blob<'conn> { /// Write data into a BLOB incrementally. Will return `Ok(0)` if the end of the blob - /// has been reached. + /// has been reached; consider using `Write::write_all(buf)` if you want to get an + /// error if the entirety of the buffer cannot be written. /// /// This function may only modify the contents of the BLOB; it is not possible to increase /// the size of a BLOB using this API.