Repository: datanymizer/datanymizer (language: rust)

Issue:
Update the chrono and time crates (security issue)
https://github.com/datanymizer/datanymizer/actions/runs/1432731164

One option is to get rid of `chrono` and only use `time`.
But `chrono` supports more date and time formats (when parsing a configuration).


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56232c2d..91734adf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   ([@evgeniy-r](https://github.com/evgeniy-r))
 
 ### ⚙️ Changed
+- Remove the dependency from `crono` ([security issue](https://github.com/chronotope/chrono/pull/578)) 
+  [#152](https://github.com/datanymizer/datanymizer/pull/152) ([@evgeniy-r](https://github.com/evgeniy-r)).
+  Remove the `raw_date` and `raw_datetime` transformers (duplication with the `datetime` transformer).
 
 ### 🛠 Fixed
 - Fix Postgres COPY syntax when dumping a table with zero defined fields
diff --git a/Cargo.lock b/Cargo.lock
index 6ae4d730..eabb9a53 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -150,41 +150,6 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
-[[package]]
-name = "chrono"
-version = "0.4.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
-dependencies = [
- "libc",
- "num-integer",
- "num-traits 0.2.12",
- "time",
- "winapi",
-]
-
-[[package]]
-name = "chrono-tz"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64c01c1c607d25c71bbaa67c113d6c6b36c434744b4fd66691d711b5b1bc0c8b"
-dependencies = [
- "chrono",
- "chrono-tz-build",
- "phf 0.10.0",
-]
-
-[[package]]
-name = "chrono-tz-build"
-version = "0.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069"
-dependencies = [
- "parse-zoneinfo",
- "phf 0.10.0",
- "phf_codegen",
-]
-
 [[package]]
 name = "clap"
 version = "2.33.3"
@@ -295,7 +260,7 @@ checksum = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279"
 dependencies = [
  "bstr",
  "csv-core",
- "itoa",
+ "itoa 0.4.6",
  "ryu",
  "serde 1.0.117",
 ]
@@ -328,7 +293,6 @@ name = "datanymizer_engine"
 version = "0.5.0"
 dependencies = [
  "anyhow",
- "chrono",
  "config",
  "csv",
  "fake",
@@ -339,6 +303,7 @@ dependencies = [
  "serde_yaml",
  "tera",
  "thiserror",
+ "time",
  "unicode-segmentation",
  "wildmatch",
 ]
@@ -385,7 +350,6 @@ version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0a1b9884322ee3bae3545a2a2eaa9a45007136e0dd0e62db69211f1c42005ef3"
 dependencies = [
- "chrono",
  "rand",
 ]
 
@@ -610,9 +574,9 @@ dependencies = [
 
 [[package]]
 name = "humansize"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
+checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
 
 [[package]]
 name = "idna"
@@ -670,6 +634,12 @@ version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
 
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -691,9 +661,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.105"
+version = "0.2.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013"
+checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c"
 
 [[package]]
 name = "linked-hash-map"
@@ -818,16 +788,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "num-integer"
-version = "0.1.44"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
-dependencies = [
- "autocfg",
- "num-traits 0.2.12",
-]
-
 [[package]]
 name = "num-traits"
 version = "0.1.43"
@@ -846,6 +806,15 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "num_threads"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "number_prefix"
 version = "0.3.0"
@@ -929,15 +898,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "parse-zoneinfo"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
-dependencies = [
- "regex",
-]
-
 [[package]]
 name = "percent-encoding"
 version = "2.1.0"
@@ -1004,36 +964,7 @@ version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
 dependencies = [
- "phf_shared 0.8.0",
-]
-
-[[package]]
-name = "phf"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9fc3db1018c4b59d7d582a739436478b6035138b6aecbce989fc91c3e98409f"
-dependencies = [
- "phf_shared 0.10.0",
-]
-
-[[package]]
-name = "phf_codegen"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
-dependencies = [
- "phf_generator",
- "phf_shared 0.10.0",
-]
-
-[[package]]
-name = "phf_generator"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
-dependencies = [
- "phf_shared 0.10.0",
- "rand",
+ "phf_shared",
 ]
 
 [[package]]
@@ -1045,16 +976,6 @@ dependencies = [
  "siphasher",
 ]
 
-[[package]]
-name = "phf_shared"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
-dependencies = [
- "siphasher",
- "uncased",
-]
-
 [[package]]
 name = "pin-project-lite"
 version = "0.2.6"
@@ -1385,7 +1306,7 @@ version = "1.0.59"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95"
 dependencies = [
- "itoa",
+ "itoa 0.4.6",
  "ryu",
  "serde 1.0.117",
 ]
@@ -1562,8 +1483,6 @@ version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d3cac831b615c25bcef632d1cabf864fa05813baad3d526829db18eb70e8b58d"
 dependencies = [
- "chrono",
- "chrono-tz",
  "globwalk",
  "humansize",
  "lazy_static",
@@ -1619,24 +1538,31 @@ dependencies = [
 
 [[package]]
 name = "thread_local"
-version = "1.0.1"
+version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
 dependencies = [
- "lazy_static",
+ "once_cell",
 ]
 
 [[package]]
 name = "time"
-version = "0.1.44"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
+checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d"
 dependencies = [
+ "itoa 1.0.1",
  "libc",
- "wasi",
- "winapi",
+ "num_threads",
+ "time-macros",
 ]
 
+[[package]]
+name = "time-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6"
+
 [[package]]
 name = "tinyvec"
 version = "0.3.4"
@@ -1645,11 +1571,10 @@ checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117"
 
 [[package]]
 name = "tokio"
-version = "1.11.0"
+version = "1.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce"
+checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
 dependencies = [
- "autocfg",
  "bytes",
  "libc",
  "memchr",
@@ -1682,7 +1607,7 @@ dependencies = [
  "log",
  "parking_lot",
  "percent-encoding",
- "phf 0.8.0",
+ "phf",
  "pin-project-lite",
  "postgres-protocol",
  "postgres-types",
@@ -1726,15 +1651,6 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
 
-[[package]]
-name = "uncased"
-version = "0.9.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0"
-dependencies = [
- "version_check",
-]
-
 [[package]]
 name = "unic-char-property"
 version = "0.9.0"
diff --git a/datanymizer_engine/Cargo.toml b/datanymizer_engine/Cargo.toml
index 7846f3f8..fe5ee6fe 100644
--- a/datanymizer_engine/Cargo.toml
+++ b/datanymizer_engine/Cargo.toml
@@ -8,16 +8,16 @@ edition = "2021"
 
 [dependencies]
 anyhow = "1.0"
-chrono = "0.4"
 config = "0.10"
 csv = "1.1"
-fake = { version = "2.4.1", features = ["chrono"] }
+fake = { version = "2.4.1" }
 once_cell = "1.5.2"
 rand = "0.8.4"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 serde_yaml = "0.8.14"
-tera = "1.15.0"
+tera = { version = "1.15.0", default-features = false, features = ["slug", "percent-encoding", "humansize", "rand"] }
 thiserror = "1.0"
+time = { version = "0.3.7", features = ["formatting", "macros", "parsing"] }
 unicode-segmentation = "1.7.0"
 wildmatch = "2.1.0"
diff --git a/datanymizer_engine/src/transformer/mod.rs b/datanymizer_engine/src/transformer/mod.rs
index c83eca7a..37bdd796 100644
--- a/datanymizer_engine/src/transformer/mod.rs
+++ b/datanymizer_engine/src/transformer/mod.rs
@@ -97,8 +97,8 @@ impl error::Error for TransformError {
     }
 }
 
-impl From<tera::Error> for TransformError {
-    fn from(err: tera::Error) -> Self {
+impl TransformError {
+    fn from_error<E: error::Error>(err: E) -> Self {
         Self {
             field_name: "".to_string(),
             field_value: "".to_string(),
@@ -107,12 +107,20 @@ impl From<tera::Error> for TransformError {
     }
 }
 
-impl From<chrono::ParseError> for TransformError {
-    fn from(pe: chrono::ParseError) -> Self {
-        Self {
-            field_name: "".to_string(),
-            field_value: "".to_string(),
-            reason: pe.to_string(),
-        }
+impl From<tera::Error> for TransformError {
+    fn from(err: tera::Error) -> Self {
+        Self::from_error(err)
+    }
+}
+
+impl From<time::error::Format> for TransformError {
+    fn from(err: time::error::Format) -> Self {
+        Self::from_error(err)
+    }
+}
+
+impl From<time::error::InvalidFormatDescription> for TransformError {
+    fn from(err: time::error::InvalidFormatDescription) -> Self {
+        Self::from_error(err)
     }
 }
diff --git a/datanymizer_engine/src/transformers/datetime.rs b/datanymizer_engine/src/transformers/datetime.rs
deleted file mode 100644
index 83bb845c..00000000
--- a/datanymizer_engine/src/transformers/datetime.rs
+++ /dev/null
@@ -1,130 +0,0 @@
-use crate::transformer::{TransformContext, TransformResult, TransformResultHelper, Transformer};
-use chrono::prelude::*;
-use chrono::DateTime;
-use fake::{faker::chrono::raw::*, locales::EN, Fake};
-use serde::{Deserialize, Serialize};
-
-#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Hash, Clone, Debug)]
-pub struct FromValue(String);
-#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Hash, Clone, Debug)]
-pub struct ToValue(String);
-#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Hash, Clone, Debug)]
-pub struct Format(String);
-
-const BOUNDS_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.f%:z";
-
-/// Generates random dates.
-///
-/// # Example:
-///
-/// With default values:
-///
-/// ```yaml
-/// #...
-/// rules:
-///   birth_date:
-///     datetime: {}
-/// ```
-///
-/// or custom date ranges:
-///
-/// ```yaml
-/// #...
-/// rules:
-///   birth_date:
-///     datetime:
-///       from: 1990-01-01T00:00:00+00:00
-///       to: 2010-12-31T00:00:00+00:00
-/// ```
-///
-/// Also, you can specify datetime format for the output.
-/// For the bounds (from/to) you should use the RFC 3339 format.
-/// The default output format is also RFC 3339 (%Y-%m-%dT%H:%M:%S%.f%:z).
-/// You don't need to change the format when using this transformer with datetime SQL fields.
-///
-/// ```yaml
-/// #...
-/// rules:
-///   birth_date:
-///     datetime:
-///       from: 1990-01-01T00:00:00+00:00
-///       to: 2010-12-31T00:00:00+00:00
-///       format: %Y-%m-%d
-/// ```
-#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Debug, Default)]
-pub struct RandomDateTimeTransformer {
-    #[serde(default)]
-    pub from: FromValue,
-    #[serde(default)]
-    pub to: ToValue,
-    #[serde(default)]
-    pub format: Format,
-}
-
-impl Default for FromValue {
-    fn default() -> Self {
-        Self(Utc.ymd(1970, 1, 1).and_hms_micro(0, 0, 0, 0).to_rfc3339())
-    }
-}
-
-impl Default for ToValue {
-    fn default() -> Self {
-        Self(Utc.ymd(9999, 1, 1).and_hms_micro(0, 0, 0, 0).to_rfc3339())
-    }
-}
-
-impl Default for Format {
-    fn default() -> Self {
-        Self(String::from(BOUNDS_FORMAT))
-    }
-}
-
-impl Transformer for RandomDateTimeTransformer {
-    fn transform(
-        &self,
-        _field_name: &str,
-        _field_value: &str,
-        _ctx: &Option<TransformContext>,
-    ) -> TransformResult {
-        let from_dt = DateTime::parse_from_str(&self.from.0, BOUNDS_FORMAT)?.with_timezone(&Utc);
-        let to_dt = DateTime::parse_from_str(&self.to.0, BOUNDS_FORMAT)?.with_timezone(&Utc);
-        let between: chrono::DateTime<Utc> = DateTimeBetween(EN, from_dt, to_dt).fake();
-        let res: String = between.format(&self.format.0).to_string();
-
-        TransformResult::present(res)
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    fn transformed_value(cfg: &str) -> String {
-        let transformer: RandomDateTimeTransformer = serde_yaml::from_str(cfg).unwrap();
-        transformer
-            .transform("datetime", "", &None)
-            .unwrap()
-            .unwrap()
-    }
-
-    #[test]
-    fn default_format() {
-        let cfg = r#"
-                          from: 2010-10-01T00:00:00+00:00
-                          to: 2010-10-31T00:00:00+00:00
-                       "#;
-        let result = transformed_value(cfg);
-        assert!(result.starts_with("2010-10-"));
-    }
-
-    #[test]
-    fn custom_format() {
-        let cfg = r#"
-                          format: "%m"
-                          from: 2000-12-01T00:00:00+00:00
-                          to: 2000-12-31T00:00:00+00:00
-                       "#;
-        let result = transformed_value(cfg);
-        assert_eq!(result, "12");
-    }
-}
diff --git a/datanymizer_engine/src/transformers/datetime/format.rs b/datanymizer_engine/src/transformers/datetime/format.rs
new file mode 100644
index 00000000..6d756955
--- /dev/null
+++ b/datanymizer_engine/src/transformers/datetime/format.rs
@@ -0,0 +1,431 @@
+use std::fmt::{Display, Formatter};
+use time::format_description::{self, Component, FormatItem};
+
+const PATTERN_REPLACEMENTS: [(&str, &str); 46] = [
+    ("Y", "[year]"),
+    ("y", "[year repr:last_two]"),
+    ("m", "[month]"),
+    ("b", "[month repr:short]"),
+    ("B", "[month repr:long]"),
+    ("h", "[month repr:short]"),
+    ("d", "[day]"),
+    ("e", "[day padding:space]"),
+    ("a", "[weekday repr:short]"),
+    ("A", "[weekday]"),
+    ("w", "[weekday repr:sunday]"),
+    ("u", "[weekday repr:monday one_indexed:true]"),
+    ("U", "[week_number repr:sunday]"),
+    ("W", "[week_number repr:monday]"),
+    ("G", "[year base:iso_week]"),
+    ("g", "[year repr:last_two base:iso_week]"),
+    ("V", "[week_number]"),
+    ("j", "[ordinal]"),
+    ("D", "[month]/[day]/[year repr:last_two]"),
+    ("x", "[month]/[day]/[year repr:last_two]"),
+    ("F", "[year]-[month]-[day]"),
+    ("v", "[day padding:space]-[month repr:short]-[year]"),
+    ("H", "[hour]"),
+    ("k", "[hour padding:space]"),
+    ("I", "[hour repr:12]"),
+    ("l", "[hour repr:12 padding:space]"),
+    ("P", "[period case:lower]"),
+    ("p", "[period]"),
+    ("M", "[minute]"),
+    ("S", "[second]"),
+    ("f", "[subsecond digits:9]"),
+    (".f", ".[subsecond digits:9]"),
+    (".3f", ".[subsecond digits:3]"),
+    (".6f", ".[subsecond digits:6]"),
+    (".9f", ".[subsecond digits:9]"),
+    ("R", "[hour]:[minute]"),
+    ("T", "[hour]:[minute]:[second]"),
+    ("X", "[hour]:[minute]:[second]"),
+    ("r", "[hour repr:12]:[minute]:[second] [period case:upper]"),
+    ("z", "[offset_hour sign:mandatory][offset_minute]"),
+    (":z", "[offset_hour sign:mandatory]:[offset_minute]"),
+    ("c", "[weekday repr:short] [month repr:short] [day padding:space] [hour]:[minute]:[second] [year]"),
+    ("+", "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:9][offset_hour sign:mandatory]:[offset_minute]"),
+    ("t", "\t"),
+    ("n", "\n"),
+    ("%", "%"),
+];
+
+/// Converts formats between Chrono crate/strftime
+/// (https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html)
+/// and Time crate (https://time-rs.github.io/book/api/format-description.html).
+///
+/// Notes:
+/// %C, %Z and %s are not supported (missing in the Time's format).
+/// %.f works like %.9f (always 9 digits). The behaviour of the %+ pattern is the same in this regard.
+/// Patterns (e.g. %x, %X, %c) are not localized (no locale support in the Time crate).
+/// Modifiers "_", "-", "0" are not supported yet (you can make a feature request).
+fn convert(s: &str) -> Result<String, ConvertError> {
+    // 4 is just assumption
+    let mut new_s = String::with_capacity(s.len() * 4);
+    let mut skip_count = 0;
+
+    for (i, c) in s.char_indices() {
+        if skip_count > 0 {
+            skip_count -= 1;
+            continue;
+        }
+
+        if c != '%' {
+            new_s.push(c);
+            continue;
+        }
+
+        let substr = &s[i + 1..];
+        if let Some((from, to)) = PATTERN_REPLACEMENTS
+            .iter()
+            .find(|(from, _)| substr.starts_with(from))
+        {
+            new_s.push_str(to);
+            // there are only ASCII chars in the patterns, so we can use `len()` as chars' count
+            skip_count = from.len();
+        } else {
+            return Err(ConvertError(s.to_string(), i));
+        }
+    }
+
+    Ok(new_s)
+}
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+enum CompiledItem {
+    Literal(Vec<u8>),
+    Component(Component),
+}
+
+impl CompiledItem {
+    fn from_format_item(item: FormatItem) -> Vec<Self> {
+        match item {
+            FormatItem::Literal(bs) => vec![Self::Literal(bs.to_owned())],
+            FormatItem::Component(c) => vec![Self::Component(c)],
+            FormatItem::Optional(i) => Self::from_format_item(i.to_owned()),
+            FormatItem::Compound(nested) => Self::from_format_items(nested.to_owned()),
+            FormatItem::First(nested) => nested
+                .get(0)
+                .map(|i| Self::from_format_item(i.to_owned()))
+                .unwrap_or_default(),
+            _ => vec![],
+        }
+    }
+
+    fn from_format_items(items: Vec<FormatItem>) -> Vec<Self> {
+        items
+            .to_owned()
+            .into_iter()
+            .flat_map(Self::from_format_item)
+            .collect()
+    }
+
+    fn format_item(&self) -> FormatItem {
+        match self {
+            Self::Literal(bs) => FormatItem::Literal(bs.as_slice()),
+            Self::Component(c) => FormatItem::Component(*c),
+        }
+    }
+}
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub struct Compiled(Vec<CompiledItem>);
+
+impl Compiled {
+    pub fn compile(f: &str) -> Result<Self, CompileError> {
+        let converted_format = convert(f)?;
+        let format_items = format_description::parse(converted_format.as_str())?;
+
+        Ok(Self::from_format_items(format_items))
+    }
+
+    pub fn format_items(&self) -> Vec<FormatItem> {
+        self.0.iter().map(CompiledItem::format_item).collect()
+    }
+
+    fn from_format_items(fi: Vec<FormatItem>) -> Self {
+        Self(CompiledItem::from_format_items(fi))
+    }
+}
+
+#[derive(Debug)]
+pub struct ConvertError(String, usize);
+
+impl Display for ConvertError {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "unknown pattern in the format string `{}` at {}",
+            self.0, self.1
+        )
+    }
+}
+
+impl std::error::Error for ConvertError {}
+
+#[derive(Debug)]
+pub enum CompileError {
+    Convert(ConvertError),
+    Format(time::error::InvalidFormatDescription),
+}
+
+impl Display for CompileError {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self)
+    }
+}
+
+impl std::error::Error for CompileError {}
+
+impl From<ConvertError> for CompileError {
+    fn from(err: ConvertError) -> Self {
+        Self::Convert(err)
+    }
+}
+
+impl From<time::error::InvalidFormatDescription> for CompileError {
+    fn from(err: time::error::InvalidFormatDescription) -> Self {
+        Self::Format(err)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use time::{macros::datetime, OffsetDateTime};
+
+    mod convert {
+        use super::*;
+
+        #[test]
+        fn replacements() {
+            let all = PATTERN_REPLACEMENTS
+                .map(|(src, _)| format!("%{}", src))
+                .join(" ");
+            let all = convert(all.as_str()).unwrap();
+
+            assert_eq!(all.find("%"), Some(all.len() - 1));
+        }
+    }
+
+    mod strftime {
+        use super::*;
+
+        fn strftime(dt: &OffsetDateTime, f: &str) -> String {
+            let f = Compiled::compile(f).unwrap();
+            dt.format(&f.format_items()).unwrap()
+        }
+
+        #[test]
+        fn ymd_patterns() {
+            let f = "%Y-%m-%d %y %B %b %h %e %D %x %F %v %j";
+            let dt = datetime!(2010-02-04 00:00:00 UTC);
+            assert_eq!(
+                strftime(&dt, f),
+                "2010-02-04 10 February Feb Feb  4 02/04/10 02/04/10 2010-02-04  4-Feb-2010 035"
+            );
+        }
+
+        #[test]
+        fn week_day_patterns() {
+            let f = "%a %A %w %u";
+            let dt = datetime!(2010-02-04 00:00:00 UTC);
+            assert_eq!(strftime(&dt, f), "Thu Thursday 5 4");
+        }
+
+        #[test]
+        fn week_number_patterns() {
+            let f = "%U %W %V %G %g";
+
+            let dt = datetime!(2018-01-06 00:00:00 UTC);
+            assert_eq!(strftime(&dt, f), "00 01 01 2018 18");
+
+            let dt = datetime!(2018-01-07 00:00:00 UTC);
+            assert_eq!(strftime(&dt, f), "01 01 01 2018 18");
+
+            let dt = datetime!(2018-01-08 00:00:00 UTC);
+            assert_eq!(strftime(&dt, f), "01 02 02 2018 18");
+
+            let dt = datetime!(2017-01-01 00:00:00 UTC);
+            assert_eq!(strftime(&dt, f), "01 00 52 2016 16");
+
+            let dt = datetime!(2017-01-02 00:00:00 UTC);
+            assert_eq!(strftime(&dt, f), "01 01 01 2017 17");
+        }
+
+        #[test]
+        fn hms_patterns() {
+            let f = "%H %k %I %l %P %p %M %S %R %T %X %r";
+
+            let dt = datetime!(2018-01-06 01:02:04 UTC);
+            assert_eq!(
+                strftime(&dt, f),
+                "01  1 01  1 am AM 02 04 01:02 01:02:04 01:02:04 01:02:04 AM"
+            );
+
+            let dt = datetime!(2018-01-06 13:32:34 UTC);
+            assert_eq!(
+                strftime(&dt, f),
+                "13 13 01  1 pm PM 32 34 13:32 13:32:34 13:32:34 01:32:34 PM"
+            );
+        }
+
+        #[test]
+        fn tz_patterns() {
+            let f = "%z %:z";
+
+            let dt = datetime!(2018-01-06 01:02:04 +5);
+            assert_eq!(strftime(&dt, f), "+0500 +05:00");
+
+            let dt = datetime!(2018-01-06 01:02:04 -1:30);
+            assert_eq!(strftime(&dt, f), "-0130 -01:30");
+        }
+
+        #[test]
+        fn subsec_patterns() {
+            let f = "%f %.f %.3f %.6f %.9f";
+            let dt = datetime!(2018-01-06 01:02:04.01234567 UTC);
+            assert_eq!(
+                strftime(&dt, f),
+                "012345670 .012345670 .012 .012345 .012345670"
+            );
+        }
+
+        #[test]
+        fn full_patterns() {
+            let f = "%c %+";
+            let dt = datetime!(2018-01-06 01:02:04.5 -2:00);
+            assert_eq!(
+                strftime(&dt, f),
+                "Sat Jan  6 01:02:04 2018 2018-01-06T01:02:04.500000000-02:00"
+            );
+        }
+
+        #[test]
+        fn escape_symbols() {
+            let f = "%t%n%%d";
+            let dt = OffsetDateTime::now_utc();
+            assert_eq!(strftime(&dt, f), "\t\n%d");
+        }
+
+        #[test]
+        fn unicode() {
+            let dt = datetime!(1995-12-22 00:00:00 +5);
+            assert_eq!(
+                strftime(&dt, "Год: %Y, месяц: %m, день: %d"),
+                "Год: 1995, месяц: 12, день: 22"
+            );
+        }
+
+        #[test]
+        fn last_percent() {
+            let err = convert("%Y-%m-%").err().unwrap();
+            assert_eq!(
+                err.to_string(),
+                "unknown pattern in the format string `%Y-%m-%` at 6"
+            );
+        }
+
+        #[test]
+        fn unknown_pattern() {
+            let err = convert("%y-%@-%d").err().unwrap();
+            assert_eq!(
+                err.to_string(),
+                "unknown pattern in the format string `%y-%@-%d` at 3"
+            );
+        }
+    }
+
+    mod from_format_item {
+        use super::*;
+        use time::format_description::modifier::{Day, Month, Year};
+
+        #[test]
+        fn literal() {
+            let s = "abc";
+            let fi = FormatItem::Literal(s.as_bytes());
+
+            let dt = datetime!(2018-05-06 00:00:00 UTC);
+            assert_eq!(dt.format(&fi).unwrap(), "abc");
+
+            assert_eq!(
+                CompiledItem::from_format_item(fi),
+                vec![CompiledItem::Literal(s.as_bytes().to_vec())]
+            );
+        }
+
+        #[test]
+        fn component() {
+            let year = Component::Year(Year::default());
+            let fi = FormatItem::Component(year);
+
+            let dt = datetime!(2018-05-06 00:00:00 UTC);
+            assert_eq!(dt.format(&fi).unwrap(), "2018");
+
+            assert_eq!(
+                CompiledItem::from_format_item(fi),
+                vec![CompiledItem::Component(year)]
+            )
+        }
+
+        #[test]
+        fn optional() {
+            let s = "abc";
+            let child = FormatItem::Literal(s.as_bytes());
+            let fi = FormatItem::Optional(&child);
+
+            let dt = datetime!(2018-05-06 00:00:00 UTC);
+            assert_eq!(dt.format(&fi).unwrap(), "abc");
+
+            assert_eq!(
+                CompiledItem::from_format_item(fi),
+                vec![CompiledItem::Literal(s.as_bytes().to_vec())]
+            )
+        }
+
+        #[test]
+        fn compound() {
+            let s = "abc";
+            let month = Component::Month(Month::default());
+
+            let children = vec![
+                FormatItem::Literal(s.as_bytes()),
+                FormatItem::Component(month),
+            ];
+
+            let fi = FormatItem::Compound(children.as_slice());
+
+            let dt = datetime!(2018-05-06 00:00:00 UTC);
+            assert_eq!(dt.format(&fi).unwrap(), "abc05");
+
+            assert_eq!(
+                CompiledItem::from_format_item(fi),
+                vec![
+                    CompiledItem::Literal(s.as_bytes().to_vec()),
+                    CompiledItem::Component(month),
+                ]
+            )
+        }
+
+        #[test]
+        fn first() {
+            let s = "abc";
+            let day = Component::Day(Day::default());
+
+            let children = vec![
+                FormatItem::Component(day),
+                FormatItem::Literal(s.as_bytes()),
+            ];
+
+            let fi = FormatItem::First(children.as_slice());
+
+            let dt = datetime!(2018-01-06 00:00:00 UTC);
+            assert_eq!(dt.format(&fi).unwrap(), "06");
+
+            assert_eq!(
+                CompiledItem::from_format_item(fi),
+                vec![CompiledItem::Component(day)]
+            );
+        }
+    }
+}
diff --git a/datanymizer_engine/src/transformers/datetime/mod.rs b/datanymizer_engine/src/transformers/datetime/mod.rs
new file mode 100644
index 00000000..6a3cf024
--- /dev/null
+++ b/datanymizer_engine/src/transformers/datetime/mod.rs
@@ -0,0 +1,224 @@
+use crate::transformer::{TransformContext, TransformResult, TransformResultHelper, Transformer};
+use format::Compiled;
+use rand::distributions::{Distribution, Uniform};
+use serde::{Deserialize, Serialize};
+use std::fmt::{Debug, Display, Formatter};
+use std::hash::{Hash, Hasher};
+use time::{format_description::well_known::Rfc3339, Duration, OffsetDateTime};
+
+mod format;
+
+/// Generates random dates in the specified interval (granularity is a second).
+///
+/// # Example:
+///
+/// With default values:
+///
+/// ```yaml
+/// #...
+/// rules:
+///   birth_date:
+///     datetime: {}
+/// ```
+///
+/// or custom date ranges:
+///
+/// ```yaml
+/// #...
+/// rules:
+///   birth_date:
+///     datetime:
+///       from: 1990-01-01T00:00:00+00:00
+///       to: 2010-12-31T00:00:00+00:00
+/// ```
+///
+/// Also, you can specify datetime format for the output.
+/// For the bounds (from/to) you should use the RFC 3339 format.
+/// The default output format is also RFC 3339 (%Y-%m-%dT%H:%M:%S%.f%:z).
+/// You don't need to change the format when using this transformer with datetime SQL fields.
+/// More information about formatting is in the format.rs file.
+///
+/// ```yaml
+/// #...
+/// rules:
+///   birth_date:
+///     datetime:
+///       from: 1990-01-01T00:00:00+00:00
+///       to: 2010-12-31T00:00:00+00:00
+///       format: %Y-%m-%d
+/// ```
+#[derive(Serialize, Deserialize, Eq, Clone, Debug)]
+#[serde(try_from = "Config")]
+pub struct RandomDateTimeTransformer {
+    from: String,
+    to: String,
+    format: String,
+
+    #[serde(skip)]
+    parsed_from: OffsetDateTime,
+    #[serde(skip)]
+    parsed_to: OffsetDateTime,
+    #[serde(skip)]
+    compiled: Compiled,
+}
+
+impl RandomDateTimeTransformer {
+    fn parse_date(s: &str) -> Result<OffsetDateTime, time::error::Parse> {
+        OffsetDateTime::parse(s, &Rfc3339)
+    }
+}
+
+impl Hash for RandomDateTimeTransformer {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.from.hash(state);
+        self.to.hash(state);
+        self.format.hash(state);
+    }
+}
+
+impl PartialEq for RandomDateTimeTransformer {
+    fn eq(&self, other: &Self) -> bool {
+        self.from == other.from && self.to == other.to && self.format == other.format
+    }
+}
+
+impl TryFrom<Config> for RandomDateTimeTransformer {
+    type Error = ParseError;
+
+    fn try_from(c: Config) -> Result<Self, Self::Error> {
+        let from = c.from;
+        let to = c.to;
+        let format = c.format;
+
+        let parsed_from = Self::parse_date(from.as_str())?;
+        let parsed_to = Self::parse_date(to.as_str())?;
+        let compiled = Compiled::compile(format.as_str())?;
+
+        Ok(Self {
+            from,
+            to,
+            format,
+            parsed_from,
+            parsed_to,
+            compiled,
+        })
+    }
+}
+
+impl Transformer for RandomDateTimeTransformer {
+    fn transform(
+        &self,
+        _field_name: &str,
+        _field_value: &str,
+        _ctx: &Option<TransformContext>,
+    ) -> TransformResult {
+        let duration = (self.parsed_to - self.parsed_from).whole_seconds();
+        let mut rng = rand::thread_rng();
+        let rnd_duration = Uniform::new_inclusive(0, duration).sample(&mut rng);
+
+        let res = (self.parsed_from + Duration::seconds(rnd_duration))
+            .format(&self.compiled.format_items())?;
+
+        TransformResult::present(res)
+    }
+}
+
+#[derive(Deserialize)]
+#[serde(default)]
+struct Config {
+    from: String,
+    to: String,
+    format: String,
+}
+
+impl Default for Config {
+    fn default() -> Self {
+        Self {
+            from: String::from("1970-01-01T00:00:00+00:00"),
+            to: String::from("9999-01-01T00:00:00+00:00"),
+            format: String::from("%Y-%m-%dT%H:%M:%S%.f%:z"),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub enum ParseError {
+    Parse(time::error::Parse),
+    Compile(format::CompileError),
+}
+
+impl Display for ParseError {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self)
+    }
+}
+
+impl std::error::Error for ParseError {}
+
+impl From<time::error::Parse> for ParseError {
+    fn from(err: time::error::Parse) -> Self {
+        Self::Parse(err)
+    }
+}
+
+impl From<format::CompileError> for ParseError {
+    fn from(err: format::CompileError) -> Self {
+        Self::Compile(err)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn transformed_value(cfg: &str) -> String {
+        let transformer: RandomDateTimeTransformer = serde_yaml::from_str(cfg).unwrap();
+        transformer
+            .transform("datetime", "", &None)
+            .unwrap()
+            .unwrap()
+    }
+
+    #[test]
+    fn default_format() {
+        let cfg = r#"
+                          from: 2010-10-01T00:00:00+00:00
+                          to: 2010-10-31T00:00:00.99+00:00
+                       "#;
+        let result = transformed_value(cfg);
+        assert!(result.starts_with("2010-10-"));
+    }
+
+    #[test]
+    fn custom_format_m() {
+        let cfg = r#"
+                          format: "%m"
+                          from: 2000-12-01T00:00:00+00:00
+                          to: 2000-12-31T00:00:00+00:00
+                       "#;
+        let result = transformed_value(cfg);
+        assert_eq!(result, "12");
+    }
+
+    #[test]
+    fn custom_format_h() {
+        let cfg = r#"
+                          format: "%H"
+                          from: 2000-12-31T02:00:00+00:00
+                          to: 2000-12-31T02:59:00+00:00
+                       "#;
+        let result = transformed_value(cfg);
+        assert_eq!(result, "02");
+    }
+
+    #[test]
+    fn custom_format_ymd() {
+        let cfg = r#"
+                          format: "%Y-%m-%d"
+                          from: 2000-12-31T02:00:00+00:00
+                          to: 2000-12-31T02:59:00+00:00
+                       "#;
+        let result = transformed_value(cfg);
+        assert_eq!(result, "2000-12-31");
+    }
+}
diff --git a/datanymizer_engine/src/transformers/fk/mod.rs b/datanymizer_engine/src/transformers/fk/mod.rs
index aa3d65d2..19566066 100644
--- a/datanymizer_engine/src/transformers/fk/mod.rs
+++ b/datanymizer_engine/src/transformers/fk/mod.rs
@@ -15,7 +15,6 @@ use fake::{
     faker::{
         address::raw::*,
         boolean::raw::*,
-        chrono::raw::*,
         company::raw::*,
         currency::raw::*,
         filesystem::raw::*,
@@ -304,10 +303,6 @@ define_fk_transformers![
     ("longitude", LongitudeTransformer, Longitude, GenericFloat, Empty),
     "Gets a boolean value (TRUE/FALSE), with a given probability.",
     ("boolean", BooleanTransformer, Boolean, bool, Ratio),
-    "Gets a random date (without formatting).",
-    ("raw_date", RawDateTransformer, Date, GenericDate, Empty),
-    "Gets a random datetime (without formatting).",
-    ("raw_datetime", RawDateTimeTransformer, DateTime, GenericDateTime, Empty),
     "Gets a company name suffix (e.g., `Inc.` or `LLC`).",
     ("company_suffix", CompanySuffixTransformer, CompanySuffix, String, Empty),
     "Gets a company name.",
@@ -586,14 +581,6 @@ mod tests {
         assert!(value < 360.0 && value > -360.0);
     }
 
-    #[test]
-    fn datetime() {
-        let t = RawDateTimeTransformer::default();
-        let value = t.transform("table.field", "t", &None).unwrap().unwrap();
-        // yyyy-mm-dd hh:mm:ss
-        assert_eq!(value.len(), 19);
-    }
-
     #[test]
     fn words() {
         let t = WordsTransformer {
diff --git a/datanymizer_engine/src/transformers/fk/sql_value.rs b/datanymizer_engine/src/transformers/fk/sql_value.rs
index cf2e5e28..171198f6 100644
--- a/datanymizer_engine/src/transformers/fk/sql_value.rs
+++ b/datanymizer_engine/src/transformers/fk/sql_value.rs
@@ -1,7 +1,5 @@
 pub type GenericFloat = f64;
 pub type GenericInt = isize;
-pub type GenericDate = chrono::naive::NaiveDate;
-pub type GenericDateTime = chrono::naive::NaiveDateTime;
 
 pub trait AsSqlValue {
     fn sql_value(v: Self) -> String;
@@ -37,18 +35,6 @@ impl AsSqlValue for GenericFloat {
     }
 }
 
-impl AsSqlValue for GenericDate {
-    fn sql_value(v: Self) -> String {
-        v.format("%Y-%m-%d").to_string()
-    }
-}
-
-impl AsSqlValue for GenericDateTime {
-    fn sql_value(v: Self) -> String {
-        v.format("%Y-%m-%d %H:%M:%S").to_string()
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -81,21 +67,4 @@ mod tests {
     fn generic_float() {
         assert_eq!(GenericFloat::sql_value(17.01), "17.01");
     }
-
-    #[test]
-    fn generic_date() {
-        assert_eq!(
-            GenericDate::sql_value(GenericDate::from_ymd(2012, 3, 4)),
-            "2012-03-04"
-        );
-    }
-
-    #[test]
-    fn generic_datetime() {
-        let dt = GenericDateTime::new(
-            GenericDate::from_ymd(2020, 5, 20),
-            chrono::naive::NaiveTime::from_hms(9, 12, 1),
-        );
-        assert_eq!(GenericDateTime::sql_value(dt), "2020-05-20 09:12:01");
-    }
 }
diff --git a/datanymizer_engine/src/transformers/mod.rs b/datanymizer_engine/src/transformers/mod.rs
index 2a070f37..b690b512 100644
--- a/datanymizer_engine/src/transformers/mod.rs
+++ b/datanymizer_engine/src/transformers/mod.rs
@@ -103,9 +103,6 @@ define_transformers_enum![
 
     ("boolean", Boolean, BooleanTransformer),
 
-    ("raw_date", RawDate, RawDateTransformer),
-    ("raw_datetime", RawDateTime, RawDateTimeTransformer),
-
     ("company_suffix", CompanySuffix, CompanySuffixTransformer),
     ("company_name", CompanyName, CompanyNameTransformer),
     ("company_name_alt", CompanyNameAlt, CompanyNameAltTransformer),
diff --git a/docs/transformers.md b/docs/transformers.md
index 9e5323e5..55f0024b 100644
--- a/docs/transformers.md
+++ b/docs/transformers.md
@@ -102,7 +102,7 @@ boolean:
 
 #### datetime
 
-Generates random dates.
+Generates random dates in the specified interval (granularity is a second).
 
 Examples:
 
@@ -116,19 +116,36 @@ You can specify a range:
 
 ```yaml
 datetime:
-  from: 1990-01-01T00:00:00+00:00
-  to: 2010-12-31T00:00:00+00:00
+  from: "1990-01-01T00:00:00+00:00"
+  to: "2010-12-31T00:00:00+00:00"
 ```
 
-Also, you can specify datetime format 
-([available specifiers](https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html)).
-Be careful with this option
+Also, you can specify the datetime format.
 
 ```yaml
 datetime:
-  format: %Y-%m-%dT%H:%M:%S%.f%:z
+  format: "%Y-%m-%d"
 ```
 
+For the bounds (`from`/`to`) you should use the RFC 3339 format (`%Y-%m-%dT%H:%M:%S%.f%:z`).
+
+The default output format is also RFC 3339. 
+You don't need to change the format when using this transformer with datetime SQL fields.
+
+[Here](https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html) you can look at the available formatting 
+patterns.
+
+Notes:
+
+* `%C`, `%Z` and `%s` are not supported.
+* `%.f` works like `%.9f` (always 9 digits). The behaviour of the `%+` pattern is the same in this regard.
+* Patterns (e.g. `%x`, `%X`, `%c`) are not localized.
+* Modifiers `_`, `-`, `0` are not supported yet (you can make a feature request).
+
+These are due to the fact that we removed the dependency on the [chrono](https://crates.io/crates/chrono) crate and use 
+the [time](https://crates.io/crates/time) crate directly (because of 
+[security issue](https://github.com/chronotope/chrono/pull/578) in `chrono`).
+
 #### random_num
 
 Gets a random number.
@@ -169,15 +186,6 @@ random_num:
     try_count: 5
 ```
 
-#### raw_date
-
-Gets a random date (without formatting).
-
-#### raw_datetime
-
-Gets a random datetime (without formatting).
-
-
 ## Special
 
 #### capitalize


Test patch: