extern crate chrono;
use chrono::{DateTime, Datelike, TimeZone, Timelike, Utc};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::fs::{File, OpenOptions};
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::string::String;
use std::cmp::{Ord, Ordering};
#[derive(Debug, PartialEq)]
pub enum TSLiteError {
IOError(String),
IndexOutOfBound,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Timestamp {
pub year: u16,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
}
impl From<chrono::DateTime<Utc>> for Timestamp {
fn from(d: chrono::DateTime<Utc>) -> Timestamp {
Timestamp {
year: d.year() as u16,
month: d.month() as u8,
day: d.day() as u8,
hour: d.hour() as u8,
minute: d.minute() as u8,
second: d.second() as u8,
}
}
}
impl From<&[u8]> for Timestamp {
fn from(d: &[u8]) -> Timestamp {
let mut reader = Cursor::new(d);
Timestamp {
year: reader.read_u16::<LittleEndian>().unwrap(),
month: reader.read_u8().unwrap(),
day: reader.read_u8().unwrap(),
hour: reader.read_u8().unwrap(),
minute: reader.read_u8().unwrap(),
second: reader.read_u8().unwrap(),
}
}
}
impl PartialOrd for Timestamp {
fn partial_cmp(&self, other: &Timestamp) -> Option<Ordering> {
Some(
self.year
.cmp(&other.year)
.then(self.month.cmp(&other.month))
.then(self.day.cmp(&other.day))
.then(self.hour.cmp(&other.hour))
.then(self.minute.cmp(&other.minute))
.then(self.second.cmp(&other.second)),
)
}
}
impl Ord for Timestamp {
fn cmp(&self, other: &Self) -> Ordering {
self.year
.cmp(&other.year)
.then(self.month.cmp(&other.month))
.then(self.day.cmp(&other.day))
.then(self.hour.cmp(&other.hour))
.then(self.minute.cmp(&other.minute))
.then(self.second.cmp(&other.second))
}
}
impl Into<DateTime<Utc>> for &Timestamp {
fn into(self) -> DateTime<Utc> {
Utc.ymd(self.year as i32, self.month as u32, self.day as u32)
.and_hms(self.hour as u32, self.minute as u32, self.second as u32)
}
}
impl Timestamp {
pub fn as_bytes(&self) -> Vec<u8> {
let mut store: Vec<u8> = Vec::with_capacity(7);
store.write_u16::<LittleEndian>(self.year).unwrap();
store.push(self.month);
store.push(self.day);
store.push(self.hour);
store.push(self.minute);
store.push(self.second);
store
}
pub fn offset(&self, date: &Timestamp) -> u32 {
let me: DateTime<Utc> = self.into();
let other: DateTime<Utc> = date.into();
(other - me).num_seconds() as u32
}
pub fn is_valid(&self) -> bool {
let mut valid = true;
valid &= 1 <= self.month && self.month <= 12;
valid &= 1 <= self.day;
valid &= self.hour < 24;
valid &= self.minute < 60;
valid &= self.second < 60;
if self.month == 2 {
let factor = |x| self.year % x == 0;
let leap = factor(4) && (!factor(100) || factor(400));
if leap {
valid &= self.day <= 29;
} else {
valid &= self.day <= 28;
}
} else {
valid &=
(self.month % 2 == 0 && self.day <= 30) || (self.month % 2 == 1 && self.day <= 31);
}
valid
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct RecordInfo {
pub time_offset: u32,
pub value: u8,
}
impl From<&[u8]> for RecordInfo {
fn from(d: &[u8]) -> RecordInfo {
let mut reader = Cursor::new(d);
RecordInfo {
time_offset: reader.read_u32::<LittleEndian>().unwrap(),
value: reader.read_u8().unwrap(),
}
}
}
impl PartialOrd for RecordInfo {
fn partial_cmp(&self, other: &RecordInfo) -> Option<Ordering> {
Some(self.time_offset.cmp(&other.time_offset))
}
}
impl Ord for RecordInfo {
fn cmp(&self, other: &Self) -> Ordering {
self.time_offset.cmp(&other.time_offset)
}
}
impl RecordInfo {
pub fn as_bytes(&self) -> Vec<u8> {
let mut store: Vec<u8> = Vec::with_capacity(4 + 1);
store.write_u32::<LittleEndian>(self.time_offset).unwrap();
store.write_u8(self.value).unwrap();
store
}
}
#[derive(Debug, Copy, Clone)]
pub struct DbHeader {
pub origin_date: Timestamp,
pub records_number: u64,
}
impl From<&[u8]> for DbHeader {
fn from(d: &[u8]) -> DbHeader {
let timestamp = Timestamp::from(d);
let mut reader = Cursor::new(d);
reader.set_position(7);
DbHeader {
origin_date: timestamp,
records_number: reader.read_u64::<LittleEndian>().unwrap(),
}
}
}
impl DbHeader {
pub fn as_bytes(&self) -> Vec<u8> {
let mut store: Vec<u8> = Vec::with_capacity(7 + 8);
store.extend(self.origin_date.as_bytes());
store
.write_u64::<LittleEndian>(self.records_number)
.unwrap();
store
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum DbIssue {
UnorderedRecord,
HeaderCorrupted,
OriginDateInvalid,
RecordCorrupted(u64),
MismatchRecordAmount,
None,
}
#[derive(Debug)]
pub struct PhysicalDB {
pub path: PathBuf,
pub file: Option<File>,
pub header: DbHeader,
}
impl PhysicalDB {
pub fn new(
path: &Path,
origin_date: Option<chrono::DateTime<Utc>>,
) -> Result<PhysicalDB, TSLiteError> {
if path.exists() {
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
file.seek(SeekFrom::Start(0))
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
let mut buffer = [0; 15];
let n = file
.read(&mut buffer[..])
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
if n == 15 {
let header: DbHeader = DbHeader::from(&buffer[..]);
return Ok(PhysicalDB {
path: PathBuf::from(path),
file: Some(file),
header,
});
} else {
return Err(TSLiteError::IOError(
"DB File header is corrupted.".to_string(),
));
}
}
PhysicalDB::create(path, origin_date)
}
pub fn create(
path: &Path,
origin_date: Option<chrono::DateTime<Utc>>,
) -> Result<PhysicalDB, TSLiteError> {
let mut file = File::create(path).map_err(|e| TSLiteError::IOError(e.to_string()))?;
let date = Timestamp::from(origin_date.unwrap_or_else(Utc::now));
let header = DbHeader {
origin_date: date,
records_number: 0,
};
file.write(&header.as_bytes())
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
Ok(PhysicalDB {
path: PathBuf::from(path),
file: None,
header,
})
}
pub fn open(&mut self) -> Result<(), TSLiteError> {
if self.file.is_some() {
return Ok(());
}
self.file = Some(
OpenOptions::new()
.read(true)
.write(true)
.open(&self.path)
.map_err(|e| TSLiteError::IOError(e.to_string()))?,
);
Ok(())
}
pub fn close(&mut self) -> Result<(), TSLiteError> {
if self.file.is_some() {
self.file
.as_ref()
.unwrap()
.sync_all()
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
self.file = None;
}
Ok(())
}
pub fn read_header(&mut self) -> Result<DbHeader, TSLiteError> {
if self.file.is_none() {
self.open()?;
}
let mut fref = self.file.as_ref().unwrap();
fref.seek(SeekFrom::Start(0))
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
let mut buffer = [0; 15];
let n = fref
.read(&mut buffer[..])
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
if n == 15 {
let header: DbHeader = DbHeader::from(&buffer[..]);
return Ok(header);
}
Err(TSLiteError::IOError(
"Could not read header: not enough octets.".to_string(),
))
}
fn check_record_index(&self, rec_id: u64) -> Result<bool, TSLiteError> {
let metadata = self
.file
.as_ref()
.unwrap()
.metadata()
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
if metadata.len() >= ((7+8) + (4+1) * rec_id) {
return Ok(true);
}
Ok(false)
}
pub fn read_record(&mut self, rec_id: u64) -> Result<RecordInfo, TSLiteError> {
if self.file.is_none() {
self.open()?;
}
let id_exist = self.check_record_index(rec_id)?;
if !id_exist {
return Err(TSLiteError::IndexOutOfBound);
}
let pos = (7 + 8) + (rec_id * 5);
let mut fref = self.file.as_ref().unwrap();
fref.seek(SeekFrom::Start(pos))
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
let mut buffer = [0; 5];
let n = fref
.read(&mut buffer[..])
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
if n == 5 {
let record: RecordInfo = RecordInfo::from(&buffer[..]);
return Ok(record);
}
Err(TSLiteError::IOError(
"Could not read record: not enough octets.".to_string(),
))
}
pub fn update_record_number(&mut self, drn: u64) -> Result<(), TSLiteError> {
if self.file.is_none() {
self.open()?;
}
let mut fref = self.file.as_ref().unwrap();
fref.seek(SeekFrom::Start(7))
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
fref.write_u64::<LittleEndian>(self.header.records_number + drn)
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
fref.sync_data()
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
self.header.records_number += drn;
Ok(())
}
pub fn append_record(&mut self, rec_nfo: RecordInfo) -> Result<(), TSLiteError> {
if self.file.is_none() {
self.open()?;
}
let mut fref = self.file.as_ref().unwrap();
fref.seek(SeekFrom::End(0))
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
fref.write(&rec_nfo.as_bytes())
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
fref.sync_all()
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
self.update_record_number(1)?;
Ok(())
}
pub fn append_record_now(&mut self, value: u8) -> Result<(), TSLiteError> {
let origin = self.header.origin_date;
let now = Timestamp::from(Utc::now());
let off = origin.offset(&now);
let nfo = RecordInfo {
value,
time_offset: off,
};
self.append_record(nfo)
}
pub fn update_record(&mut self, rec_id: u64, value: u8) -> Result<(), TSLiteError> {
if self.file.is_none() {
self.open()?;
}
let id_exist = self.check_record_index(rec_id)?;
if !id_exist {
return Err(TSLiteError::IndexOutOfBound);
}
let pos = (7 + 8) + (rec_id * 5) + 4;
let mut fref = self.file.as_ref().unwrap();
fref.seek(SeekFrom::Start(pos))
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
fref.write(&[value])
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
fref.sync_all()
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
Ok(())
}
pub fn check_db_file(&mut self) -> Result<DbIssue, TSLiteError> {
if self.file.is_none() {
self.open()?;
}
let res_header = self.read_header();
if res_header.is_err() {
return Ok(DbIssue::HeaderCorrupted);
}
let header = res_header.unwrap();
if !header.origin_date.is_valid() {
return Ok(DbIssue::OriginDateInvalid);
}
let mut time_offset = 0;
for i in 0..header.records_number {
let res_record = self.read_record(i);
if res_record.is_err() {
return Ok(DbIssue::RecordCorrupted(i));
}
if time_offset > res_record.as_ref().unwrap().time_offset {
return Ok(DbIssue::UnorderedRecord);
}
time_offset = res_record.as_ref().unwrap().time_offset;
}
let id_exist = self.check_record_index(header.records_number)?;
if !id_exist {
return Ok(DbIssue::MismatchRecordAmount);
}
Ok(DbIssue::None)
}
pub fn reorder_record(&mut self) -> Result<(), TSLiteError> {
if self.file.is_none() {
self.open()?;
}
let mut records: Vec<RecordInfo> = Vec::with_capacity(self.header.records_number as usize);
for i in 0..(self.header.records_number) {
records.push(self.read_record(i)?);
}
records.sort_unstable();
let mut fref = self.file.as_ref().unwrap();
fref.seek(SeekFrom::Start( 15))
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
for r in &records {
fref.write(&r.as_bytes())
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
}
fref.sync_all()
.map_err(|e| TSLiteError::IOError(e.to_string()))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::prelude::*;
use std::error::Error;
use std::fs;
use std::io::prelude::*;
use std::path::Path;
#[test]
fn create_db_origin_now() {
fs::remove_file("create_db_origin_now.db");
let r = PhysicalDB::create(&Path::new("create_db_origin_now.db"), None);
assert!(r.is_ok());
fs::remove_file("create_db_origin_now.db");
}
#[test]
fn create_db_origin_specific() {
fs::remove_file("create_db_origin_specific.db");
let origin_date = Utc.ymd(1994, 07, 08).and_hms(6, 55, 34);
let wr = PhysicalDB::create(
&Path::new("create_db_origin_specific.db"),
Some(origin_date),
);
assert!(wr.is_ok());
let mut f = File::open("create_db_origin_specific.db").unwrap();
let mut buf: Vec<u8> = Vec::with_capacity(7 + 8);
let rr = f.read_to_end(&mut buf).map_err(|e| e.to_string());
assert!(rr.is_ok());
assert!(rr.map(|v| v == (7 + 8)).unwrap_or(false));
let dbHeader = DbHeader::from(buf.as_slice());
assert_eq!(dbHeader.records_number, 0);
assert_eq!(dbHeader.origin_date.year, 1994);
assert_eq!(dbHeader.origin_date.month, 07);
assert_eq!(dbHeader.origin_date.day, 08);
assert_eq!(dbHeader.origin_date.hour, 6);
assert_eq!(dbHeader.origin_date.minute, 55);
assert_eq!(dbHeader.origin_date.second, 34);
fs::remove_file("create_db_origin_specific.db");
}
#[test]
fn append_record() {
let path = "append_record.db";
fs::remove_file(path);
let mut db = PhysicalDB::create(&Path::new(path), None).expect("could not create db.");
let header = db.read_header().expect("could not read header.");
assert_eq!(header.records_number, 0);
let origin_record = RecordInfo {
time_offset: 5,
value: 10,
};
db.append_record(origin_record)
.expect("could not append record.");
let fs_record = db.read_record(0).expect("could not get record.");
assert_eq!(origin_record, fs_record);
let header = db.read_header().expect("could not read header.");
assert_eq!(header.records_number, 1);
fs::remove_file(path);
}
#[test]
fn today_is_valid() {
let today = Timestamp::from(Utc::now());
assert_eq!(today.is_valid(), true);
}
#[test]
fn date_ord() {
let d1 = Timestamp {
year: 1994,
month: 7,
day: 8,
hour: 5,
minute: 24,
second: 23,
};
let d2 = Timestamp {
year: 1993,
month: 6,
day: 18,
hour: 8,
minute: 0,
second: 1,
};
assert_eq!(d1 > d2, true);
assert_eq!(d1 < d2, false);
assert_eq!(d1 == d2, false);
}
#[test]
fn check_healthy_db() {
let path = "healthy.db";
fs::remove_file(path);
let mut db = PhysicalDB::create(&Path::new(path), None).expect("could not create db.");
let header = db.read_header().expect("could not read header.");
for i in 0..10 {
let origin_record = RecordInfo {
time_offset: 5 + i,
value: i as u8,
};
db.append_record(origin_record)
.expect("could not append record.");
}
let err = db.check_db_file().expect("could not check db file.");
assert_eq!(err, DbIssue::None);
fs::remove_file(path);
}
#[test]
fn check_unordered_db() {
let path = "unordered.db";
fs::remove_file(path);
let mut db = PhysicalDB::create(&Path::new(path), None).expect("could not create db.");
let header = db.read_header().expect("could not read header.");
for i in 0..10 {
let origin_record = RecordInfo {
time_offset: 9 - i,
value: i as u8,
};
db.append_record(origin_record)
.expect("could not append record.");
}
let err = db.check_db_file().expect("could not check db file.");
assert_eq!(err, DbIssue::UnorderedRecord);
fs::remove_file(path);
}
#[test]
fn reorder_db() {
let path = "reordered.db";
fs::remove_file(path);
let mut db = PhysicalDB::create(&Path::new(path), None).expect("could not create db.");
let header = db.read_header().expect("could not read header.");
for i in 0..10 {
let origin_record = RecordInfo {
time_offset: 9 - i,
value: i as u8,
};
db.append_record(origin_record)
.expect("could not append record.");
}
let err = db.check_db_file().expect("could not check db file.");
assert_eq!(err, DbIssue::UnorderedRecord);
let res = db.reorder_record();
assert_eq!(res.is_ok(), true);
let err = db.check_db_file().expect("could not check db file.");
assert_eq!(err, DbIssue::None);
fs::remove_file(path);
}
#[test]
fn update_record() {
let path = "update_record.db";
fs::remove_file(path);
let mut db = PhysicalDB::create(&Path::new(path), None).expect("could not create db.");
let header = db.read_header().expect("could not read header.");
assert_eq!(header.records_number, 0);
let origin_record = RecordInfo {
time_offset: 5,
value: 10,
};
db.append_record(origin_record)
.expect("could not append record.");
let mut fs_record = db.read_record(0).expect("could not get record.");
assert_eq!(origin_record, fs_record);
let updated_value = 8;
db.update_record(0, updated_value)
.expect("Could not update record.");
fs_record = db.read_record(0).expect("could not get record.");
assert_eq!(updated_value, fs_record.value);
fs::remove_file(path);
}
}