5UNA2DEALCSRBINR27KSA6OMD6GQAXHYZ35ICQ7NB62G2XP4FT5QC
{% extends "base" %}
{% block body %}
<div style="width:30%;" class="container-fluid">
<form action="./sign_in" method="post">
<div class="mb-3 row">
<label for="user_name" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="user_name" required>
</div>
</div>
<div class="mb-3 row">
<label for="password" class="col-sm-2 col-form-label">Password </label>
<div class="col-sm-10">
<input type="password" class="form-control" name="password" required>
</div>
</div>
<button class="btn btn-primary" type="submit">Sign up</button>
</form>
</div>
{% endblock body %}
{% extends "base" %}
{% block body %}
<div style="width:30%;" class="container-fluid">
<form action="/users" method="post">
<div class="mb-3 row">
<label for="user_name" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="user_name" required>
</div>
</div>
<div class="mb-3 row">
<label for="email" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="email" class="form-control" name="email" required>
</div>
</div>
<div class="mb-3 row">
<label for="password" class="col-sm-2 col-form-label">Password </label>
<div class="col-sm-10">
<input type="password" class="form-control" name="password" required>
</div>
</div>
<button class="btn btn-primary" type="submit">Sign up</button>
</form>
</div>
{% endblock body %}
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nidobyte</title>
</head>
<body>
{% if flash %}
<p>{{flash}}</p>
{% endif %}
{% block body %}
{% endblock body %}
</body>
</html>
use std::env;
use rocket::{Rocket, Build};
use rocket::fairing::{self, AdHoc};
async fn init_db(rocket: Rocket<Build>) -> fairing::Result {
let database_url = match env::var("DATABASE_URL") {
Ok(val) => val,
Err(e) => {
error!("Failed to read DATABASE_URL environment variable: {}", e);
return Err(rocket)
}
};
let pool = sqlx::PgPool::connect(&database_url)
.await
.expect("Failed to connect to the database");
Ok(rocket.manage(pool))
}
pub fn stage() -> AdHoc {
AdHoc::on_ignite("SQL Stage - PostgreSQL", |rocket| async {
rocket
.attach(AdHoc::try_on_ignite("Postgres Database", init_db))
})
}
if let Err(e) = sqlx::migrate!("./migrations").run(&pool).await {
error!("Failed to migrate PG database: {}", e);
return Err(rocket);
}
use rocket::{
form::{Context, Form},
http::{Cookie, CookieJar},
response::{Flash, Redirect},
State,
};
use rocket_dyn_templates::Template;
use crate::database::Database;
use crate::models;
#[get("/new")]
pub fn new_user() -> Template {
Template::render("new_user", &Context::default())
}
#[derive(FromForm)]
pub struct NewUser {
// TODO validate against regexp: [a-z_][a-z0-9_-]*[$] - Needs a valid linux username
#[field(validate = len(..20))]
pub user_name: String,
#[field(validate = len(3..))]
pub email: String,
#[field(validate = len(6..64))]
pub password: String,
}
#[post("/", data = "<user>")]
pub async fn create(
db: &State<Database>,
cookie: &CookieJar<'_>,
user: Form<NewUser>,
) -> Result<Flash<Redirect>, Flash<Redirect>> {
// TODO When the form parsing fails, the users gets no feedback
let mut new_user = models::User {
id: -1,
name: user.user_name.clone(),
email: user.email.clone(),
password: user.password.clone(),
};
match new_user.create(db).await {
Ok(id) => {
new_user.id = id;
set_user_cookie(cookie, new_user);
return Ok(Flash::success(Redirect::to("/"), "Signed up succesfully"));
}
Err(_e) => Err(Flash::error(
Redirect::to("/users/new"),
"Something went wrong",
)), //TODO Show the error to the user in a flash message,
}
}
#[derive(FromForm)]
pub struct SignIn {
#[field(validate = len(..20))]
pub user_name: String,
#[field(validate = len(6..64))]
pub password: String,
}
#[get("/sign_in")]
pub async fn get_sign_in() -> Template {
Template::render("sign_in", &Context::default())
}
#[post("/sign_in", data = "<user>")]
pub async fn sign_in(
db: &State<Database>,
jar: &CookieJar<'_>,
user: Form<SignIn>,
) -> Result<Flash<Redirect>, Flash<Redirect>> {
// TODO figure out Rust and rewrite this without the nested matching
match models::User::authenticate(db, user.user_name.clone(), user.password.clone()).await {
Ok(u) => match u {
Some(u2) => {
set_user_cookie(jar, u2);
return Ok(Flash::success(Redirect::to("/"), "Signed in!"));
}
None => return Err(Flash::error(Redirect::to("./sign_in"), "Error signing in")),
},
Err(_e) => {
return Err(Flash::error(
Redirect::to("./sign_in"),
"SQL Error signing in",
))
}
}
}
static COOKIE_USER_ID_KEY: &str = "user_id";
#[delete("/sign_out")]
pub fn sign_out(jar: &CookieJar<'_>) -> Flash<Redirect> {
jar.remove_private(Cookie::named(COOKIE_USER_ID_KEY));
Flash::success(Redirect::to("/"), "Signed out succesfully")
}
fn set_user_cookie(jar: &CookieJar<'_>, user: models::User) {
jar.add_private(Cookie::new(COOKIE_USER_ID_KEY, user.id.to_string()))
}
use std::*;
use rocket::{form::*, get, post, response::Redirect, routes, State};
use rocket_auth::{prelude::Error, *};
use rocket_dyn_templates::Template;
use serde_json::json;
use sqlx::*;
use std::result::Result;
use tokio::sync::*;
#[get("/")]
async fn index(user: Option<User>) -> Template {
Template::render("index", json!({ "user": user }))
}
#[get("/login")]
fn get_login() -> Template {
Template::render("login", json!({}))
}
#[post("/login", data = "<form>")]
async fn post_login(mut auth: Auth<'_>, form: Form<Login>) -> Result<Redirect, Error> {
auth.login(&form).await?;
Ok(Redirect::to("/"))
}
#[get("/signup")]
async fn get_signup() -> Template {
Template::render("signup", json!({}))
}
#[post("/signup", data = "<form>")]
async fn post_signup(mut auth: Auth<'_>, form: Form<Signup>) -> Result<Redirect, Error> {
auth.signup(&form).await?;
auth.login(&form.into()).await?;
Ok(Redirect::to("/"))
}
#[get("/logout")]
fn logout(mut auth: Auth<'_>) -> Result<Template, Error> {
auth.logout()?;
Ok(Template::render("logout", json!({})))
}
fn routes() -> bool {
routes![
index,
get_login,
post_signup,
get_signup,
post_login,
logout,
]
}
use crate::database::Database;
use rocket::State;
use sqlx::{query, query_as};
use rocket::serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
#[serde(skip_deserializing)]
pub password: String,
}
impl User {
pub async fn create(&self, db: &State<Database>) -> Result<i32, sqlx::Error> {
let result = query!(
"INSERT INTO users (name, email, password) VALUES ($1, $2, crypt($3, gen_salt('bf'))) RETURNING id",
&self.name,
&self.email.to_lowercase(),
&self.password
)
.fetch_one(&**db)
.await?;
// TODO when I figure out Rust, update self with this id
Ok(result.id)
}
/// Validates a user and password combination, returns a User struct when
/// valid.
pub async fn authenticate(
db: &State<Database>,
name: String,
password: String,
) -> Result<Option<User>, sqlx::Error> {
let result = query_as!(
User,
"SELECT * FROM users WHERE name = $1 AND password = crypt($2, password)",
name,
password
)
.fetch_optional(&**db)
.await?;
Ok(result)
}
}
use std::env;
use rocket::{
fairing::{self, AdHoc},
Build, Rocket,
};
pub(crate) type Database = sqlx::PgPool;
async fn init_db(rocket: Rocket<Build>) -> fairing::Result {
let database_url = match env::var("DATABASE_URL") {
Ok(val) => val,
Err(e) => {
error!("Failed to read DATABASE_URL environment variable: {}", e);
return Err(rocket);
}
};
let pool = sqlx::PgPool::connect(&database_url)
.await
.expect("Failed to connect to the database");
if let Err(e) = sqlx::migrate!("./migrations").run(&pool).await {
error!("Failed to migrate PG database: {}", e);
return Err(rocket);
}
Ok(rocket.manage(pool))
}
pub fn stage() -> AdHoc {
AdHoc::on_ignite("SQL Stage - PostgreSQL", |rocket| async {
rocket.attach(AdHoc::try_on_ignite("Postgres Database", init_db))
})
}
-- Enable PG crypto helpers we need to store passwords
CREATE EXTENSION pgcrypto;
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL,
email TEXT NOT NULL,
password TEXT NOT NULL
);
CREATE UNIQUE INDEX unique_user_name ON users(name);
CREATE UNIQUE INDEX unique_user_email ON users(email);
- Test the sign up/sign in/sign out flow
- Record changes
features = [ "runtime-tokio-rustls", "postgres", "macros", "migrate" ]
features = [ "runtime-tokio-rustls", "postgres", "macros", "migrate", "time", "chrono"]
[dependencies.rocket_sync_db_pools]
version = "0.1.0-rc.1"
default-features = false
features = ["postgres_pool"]
[dependencies.rocket_dyn_templates]
version = "0.1.0-rc.1"
features = ["tera"]
name = "aead"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331"
dependencies = [
"generic-array 0.14.4",
]
[[package]]
name = "aes"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561"
dependencies = [
"aes-soft",
"aesni",
"cipher",
]
[[package]]
name = "aes-gcm"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "aes-soft"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072"
dependencies = [
"cipher",
"opaque-debug 0.3.0",
]
[[package]]
name = "aesni"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce"
dependencies = [
"cipher",
"opaque-debug 0.3.0",
]
[[package]]
"generic-array",
"generic-array 0.14.4",
]
[[package]]
name = "block-padding"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
dependencies = [
"byte-tools",
]
[[package]]
name = "bstr"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
dependencies = [
"memchr",
[[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",
"winapi 0.3.9",
]
[[package]]
name = "chrono-tz"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120"
dependencies = [
"chrono",
"parse-zoneinfo",
]
name = "fake-simd"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
]
[[package]]
name = "fsevent"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
dependencies = [
"bitflags",
"fsevent-sys",
]
[[package]]
name = "fsevent-sys"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
dependencies = [
"libc",
name = "fuchsia-zircon"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
dependencies = [
"bitflags",
"fuchsia-zircon-sys",
]
[[package]]
name = "fuchsia-zircon-sys"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
[[package]]
name = "globset"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
dependencies = [
"aho-corasick",
"bstr",
"fnv",
"log",
"regex",
]
[[package]]
name = "globwalk"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
"bitflags",
"ignore",
"walkdir",
]
name = "ignore"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
dependencies = [
"crossbeam-utils",
"globset",
"lazy_static",
"log",
"memchr",
"regex",
"same-file",
"thread_local",
"walkdir",
"winapi-util",
]
[[package]]
version = "0.6.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
dependencies = [
"cfg-if 0.1.10",
"fuchsia-zircon",
"fuchsia-zircon-sys",
"iovec",
"kernel32-sys",
"libc",
"log",
"miow 0.2.2",
"net2",
"slab",
"winapi 0.2.8",
]
[[package]]
name = "mio"
"winapi",
"winapi 0.3.9",
]
[[package]]
name = "mio-extras"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
dependencies = [
"lazycell",
"log",
"mio 0.6.23",
"slab",
]
[[package]]
name = "miow"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
dependencies = [
"kernel32-sys",
"net2",
"winapi 0.2.8",
"ws2_32-sys",
]
[[package]]
name = "normpath"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27e6e8f70e9fbbe3752d330d769e3424f24b9458ce266df93a3b456902fd696a"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "notify"
version = "4.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
dependencies = [
"bitflags",
"filetime",
"fsevent",
"fsevent-sys",
"inotify",
"libc",
"mio 0.6.23",
"mio-extras",
"walkdir",
"winapi 0.3.9",
"winapi",
"winapi 0.3.9",
]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg",
"num-traits",
name = "pest_generator"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
dependencies = [
"maplit",
"pest",
"sha-1 0.8.2",
]
[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
"siphasher",
]
[[package]]
[[package]]
name = "polyval"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd"
dependencies = [
"cpuid-bool",
"opaque-debug 0.3.0",
"universal-hash",
]
[[package]]
name = "postgres"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7871ee579860d8183f542e387b176a25f2656b9fb5211e045397f745a68d1c2"
dependencies = [
"bytes",
"fallible-iterator",
"futures",
"log",
"tokio",
"tokio-postgres",
]
name = "postgres-protocol"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff3e0f70d32e20923cabf2df02913be7c1842d4c772db8065c00fcfdd1d1bff3"
dependencies = [
"base64",
"byteorder",
"bytes",
"fallible-iterator",
"hmac",
"md-5",
"memchr",
"rand",
"sha2",
"stringprep",
]
[[package]]
name = "postgres-types"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "430f4131e1b7657b0cd9a2b0c3408d77c9a43a042d300b8c77f981dffcc43a2f"
dependencies = [
"bytes",
"fallible-iterator",
"postgres-protocol",
]
[[package]]
]
[[package]]
name = "r2d2"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f"
dependencies = [
"log",
"parking_lot",
"scheduled-thread-pool",
]
[[package]]
name = "r2d2_postgres"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7029c56be658cb54f321e0bee597810ee16796b735fa2559d7056bf06b12230b"
dependencies = [
"postgres",
"r2d2",
name = "rocket_dyn_templates"
version = "0.1.0-rc.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c83f1287ad8fa034410928297a91db37518d5c46d7cc7e1e1b4a77aec0cd8807"
dependencies = [
"glob",
"normpath",
"notify",
"rocket",
"serde",
"serde_json",
"tera",
]
[[package]]
name = "rocket_sync_db_pools"
version = "0.1.0-rc.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38cfdfebd552d075c368e641c88a5cd6ce1c58c5c710548aeb777abb48830f4b"
dependencies = [
"postgres",
"r2d2",
"r2d2_postgres",
"rocket",
"rocket_sync_db_pools_codegen",
"serde",
"tokio",
]
[[package]]
name = "rocket_sync_db_pools_codegen"
version = "0.1.0-rc.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267808c094db5366e1d8925aaf9f2ce05ff9b3bd92cb18c7040a1fe219c2e25"
dependencies = [
"devise",
"quote",
]
[[package]]
"winapi",
"winapi 0.3.9",
]
[[package]]
name = "tera"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf95b0d8a46da5fe3ea119394a6c7f1e745f9de359081641c99946e2bf55d4f2"
dependencies = [
"chrono",
"chrono-tz",
"globwalk",
"humansize",
"lazy_static",
"percent-encoding",
"pest",
"pest_derive",
"rand",
"regex",
"serde",
"serde_json",
"slug",
"unic-segment",
name = "tokio-postgres"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d2b1383c7e4fb9a09e292c7c6afb7da54418d53b045f1c1fac7a911411a2b8b"
dependencies = [
"async-trait",
"byteorder",
"bytes",
"fallible-iterator",
"futures",
"log",
"parking_lot",
"percent-encoding",
"phf",
"pin-project-lite",
"postgres-protocol",
"postgres-types",
"socket2",
"tokio",
"tokio-util",
]
[[package]]
name = "unic-char-property"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
dependencies = [
"unic-char-range",
]
[[package]]
name = "unic-char-range"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
[[package]]
name = "unic-common"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
[[package]]
name = "unic-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
dependencies = [
"unic-ucd-segment",
]
[[package]]
name = "unic-ucd-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
dependencies = [
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-version"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
dependencies = [
"unic-common",
]
[[package]]