Compare commits
6 Commits
1c0b5a23d4
...
60f81f6d4e
| Author | SHA1 | Date | |
|---|---|---|---|
|
60f81f6d4e
|
|||
|
7a4876b0e8
|
|||
|
70f77f3501
|
|||
|
8a65bb2c92
|
|||
|
af3d1a567b
|
|||
|
4449607449
|
60
Cargo.lock
generated
60
Cargo.lock
generated
@@ -457,6 +457,23 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-macros"
|
||||
version = "0.1.2"
|
||||
source = "git+https://git.javalsai.tuxcord.net/rust/const-macros.git#6f91e1e1a6ce2e5cdb5866a1e1d7841ed5e3cd6f"
|
||||
dependencies = [
|
||||
"const-macros-proc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-macros-proc"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.javalsai.tuxcord.net/rust/const-macros.git#6f91e1e1a6ce2e5cdb5866a1e1d7841ed5e3cd6f"
|
||||
dependencies = [
|
||||
"magic",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-str"
|
||||
version = "1.1.0"
|
||||
@@ -1075,7 +1092,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
[[package]]
|
||||
name = "magic"
|
||||
version = "0.16.7"
|
||||
source = "git+https://github.com/javalsai/rust-magic.git?branch=dbpaths-clone#c5a357bd482b7625e407c39f5f5af5f2d425a2fb"
|
||||
source = "git+https://github.com/javalsai/rust-magic.git?branch=dbpaths-clone#211e458a706c728aebffb1a48175b5c6edef3493"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"magic-sys",
|
||||
@@ -1083,9 +1100,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "magic-sys"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b275c342ee5dd5b19e51905dcc092b8fd1bb1187abbc600ac8910bbb15840e"
|
||||
checksum = "9a7813c89073ddea0d1979d29786cfb5bd5c8bfe19fdede15a9e6e27fe919b23"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
@@ -1175,6 +1192,7 @@ dependencies = [
|
||||
"anstyle",
|
||||
"anyhow",
|
||||
"clap",
|
||||
"const-macros",
|
||||
"const-str",
|
||||
"futures-util",
|
||||
"libc",
|
||||
@@ -1182,6 +1200,7 @@ dependencies = [
|
||||
"moka",
|
||||
"pamsock",
|
||||
"serde",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml",
|
||||
@@ -1519,9 +1538,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.4"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -1549,6 +1568,17 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -1758,9 +1788,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.0.7+spec-1.1.0"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96"
|
||||
checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
@@ -1773,27 +1803,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.0.1+spec-1.1.0"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
|
||||
checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.10+spec-1.1.0"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
|
||||
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.7+spec-1.1.0"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d"
|
||||
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
@@ -1841,9 +1871,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
checksum = "da36089a805484bcccfffe0739803392c8298778a2d2f09febf76fac5ad9025b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
|
||||
22
Cargo.toml
22
Cargo.toml
@@ -10,8 +10,19 @@ keywords = ["oauth", "oauth2", "oidc", "OpenID"]
|
||||
categories = ["web-programming::http-server", "authentication"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
strip = "symbols" # almost half of binary size smh
|
||||
|
||||
# so I don't need "full" just yet and the debug builds are HUGE
|
||||
[profile.dev]
|
||||
debug = 1
|
||||
[profile.dev.package.actix-web]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
[profile.dev.package.tokio]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
|
||||
[features]
|
||||
default = ["pamsock"]
|
||||
pamsock = ["dep:pamsock"]
|
||||
@@ -25,24 +36,31 @@ actix-web = "4.13"
|
||||
anstyle = "1.0"
|
||||
anyhow = "1.0"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
const-macros = { git = "https://git.javalsai.tuxcord.net/rust/const-macros.git", version = "0.1.2" }
|
||||
const-str = { version = "1.1", features = ["proc"] }
|
||||
futures-util = "0.3"
|
||||
libc = "0.2"
|
||||
# https://github.com/robo9k/rust-magic/issues/434
|
||||
magic = { version = "0.16", git = "https://github.com/javalsai/rust-magic.git", branch = "dbpaths-clone" }
|
||||
magic = "0.16"
|
||||
moka = { version = "0.12", features = ["async-lock", "future"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha2 = "0.10"
|
||||
thiserror = "2.0"
|
||||
tokio = { version = "1.49", features = ["full"] }
|
||||
toml = "1.0"
|
||||
users = "0.11"
|
||||
|
||||
[patch.crates-io]
|
||||
# https://github.com/robo9k/rust-magic/issues/434
|
||||
magic = { git = "https://github.com/javalsai/rust-magic.git", branch = "dbpaths-clone" }
|
||||
|
||||
[lints.clippy]
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
multiple_crate_versions = { level = "allow" } # otherwise there's too much
|
||||
|
||||
correctness = { level = "deny", priority = -1 }
|
||||
nursery = { level = "deny", priority = -1 }
|
||||
option_if_let_else = { level = "allow" } # I personally sometimes prefer what this prevents
|
||||
|
||||
pedantic = { level = "deny", priority = -1 }
|
||||
perf = { level = "deny", priority = -1 }
|
||||
style = { level = "deny", priority = -1 }
|
||||
|
||||
@@ -11,3 +11,7 @@ This is a reimplementation of the oauth2 server I was making and forgot where I
|
||||
# License
|
||||
|
||||
All code licensed under the GPL-2.0-only.
|
||||
|
||||
# Known Issues
|
||||
|
||||
HTTP `HEAD` requests are not responded to properly, I'm exploring solutions but each one has its drawbacks to consider.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use const_macros::{file::FileAsset, include_asset};
|
||||
|
||||
/// Max [`moka`] pfp cache capacity
|
||||
pub const MAX_PFP_CACHE_CAPACITY: u64 = 1024;
|
||||
|
||||
@@ -9,8 +11,7 @@ pub const MAX_PFP_CACHE_CAPACITY: u64 = 1024;
|
||||
pub const MAX_PFP_SIZE: u64 = 8 * 1024 * 1024; // 8 MiB; TODO: might lower, high for prototyping
|
||||
|
||||
/// Default user image to use if users have none
|
||||
pub const DEFAULT_USER_PFP: &[u8] = include_bytes!("../assets/default-pfp.png");
|
||||
pub const DEFAULT_USER_PFP_MIME: &str = "image/png; charset=binary";
|
||||
pub const DEFAULT_USER_PFP: FileAsset = include_asset!("assets/default-pfp.png");
|
||||
/// Basically [`USER_CACHES_HEADER`] but much longer for the [`DEFAULT_USER_PFP`]
|
||||
pub const DEFAULT_USER_PFP_CACHES_HEADER: &str =
|
||||
crate::utils::web::make_static_cache_header!(Duration::from_hours(2), Duration::from_days(1));
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::{ffi::OsStr, io, os::unix::fs::MetadataExt, path::Path, sync::Arc};
|
||||
|
||||
use ::users::os::unix::UserExt;
|
||||
use ::users::os::unix::UserExt as _;
|
||||
use moka::future::{Cache, CacheBuilder};
|
||||
use sha2::Digest as _;
|
||||
use tokio::{fs::File, io::AsyncReadExt};
|
||||
|
||||
use crate::{
|
||||
@@ -14,12 +15,13 @@ pub mod users;
|
||||
|
||||
const MIME_IMAGE_PREFIX: &str = "image/";
|
||||
|
||||
pub struct MimeWithBytes {
|
||||
pub struct ImageInfo {
|
||||
pub mime: Box<str>,
|
||||
pub bytes: Box<[u8]>,
|
||||
pub shasum: Box<str>,
|
||||
}
|
||||
|
||||
pub type Image = Arc<MimeWithBytes>;
|
||||
pub type Image = Arc<ImageInfo>;
|
||||
|
||||
// TODO: most of the cache methods here block the executor, if we wanna commit to async we'd have
|
||||
// to consider that
|
||||
@@ -115,15 +117,23 @@ impl<'a> AppCache<'a> {
|
||||
for subpath in consts::USER_PFP_PATHS {
|
||||
let path = home.join(subpath);
|
||||
|
||||
// I'm relying too much on this condition
|
||||
if let Ok(img_buf) = read_limited_path::<{ consts::MAX_PFP_SIZE }>(&path).await
|
||||
&& let Ok(Ok(mime)) = self.magic_mime_cookie.buffer(&img_buf) // TODO: first layer
|
||||
// error is actually
|
||||
// relevant
|
||||
&& mime.starts_with(MIME_IMAGE_PREFIX)
|
||||
{
|
||||
return Some(Arc::new(MimeWithBytes {
|
||||
let shasum = sha2::Sha256::digest(&img_buf);
|
||||
let shasum = format!(
|
||||
"\"{}\"",
|
||||
crate::utils::shasum::sha256sum_to_hex_string(&shasum)
|
||||
);
|
||||
|
||||
return Some(Arc::new(ImageInfo {
|
||||
mime: mime.into_boxed_str(),
|
||||
bytes: img_buf.into_boxed_slice(),
|
||||
shasum: shasum.into_boxed_str(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use actix_web::{App, HttpServer, web};
|
||||
|
||||
pub mod caches;
|
||||
pub mod services;
|
||||
pub mod static_app_data;
|
||||
|
||||
pub struct AppState {
|
||||
pub args: crate::args::Args,
|
||||
@@ -11,6 +12,9 @@ pub struct AppState {
|
||||
pub cache: caches::AppCache<'static>,
|
||||
}
|
||||
|
||||
/// Type alias to be used just as `data: AppData,` extractor in [`actix_web`] request handlers.
|
||||
pub type AppData = static_app_data::StaticAppDataExtractor<&'static AppState>;
|
||||
|
||||
/// Leaks memory for the sake of not atomic'ing all over.
|
||||
#[expect(clippy::missing_errors_doc)]
|
||||
pub async fn start_app(args: crate::args::Args, config: crate::conf::Config) -> io::Result<()> {
|
||||
@@ -18,10 +22,7 @@ pub async fn start_app(args: crate::args::Args, config: crate::conf::Config) ->
|
||||
|
||||
let config = Box::leak(Box::new(config));
|
||||
|
||||
let cache = caches::AppCache::new(
|
||||
&config.unix.magic_paths,
|
||||
&config.unix.groups,
|
||||
);
|
||||
let cache = caches::AppCache::new(&config.unix.magic_paths, &config.unix.groups);
|
||||
|
||||
let app: &AppState = Box::leak(Box::new(AppState {
|
||||
args,
|
||||
@@ -36,7 +37,7 @@ pub async fn start_app(args: crate::args::Args, config: crate::conf::Config) ->
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(web::Data::new(app))
|
||||
.app_data(app)
|
||||
.service(services::images::make_scope(ws::IMAGES))
|
||||
.default_service(web::to(services::not_found::not_found))
|
||||
})
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
//! Scope for the image get backend.
|
||||
//! - `/`: Gives the default image.
|
||||
//! - `/{username}`: Gives username's pfp or redirects to the default image's path (for better
|
||||
//! - `/default`: Gives the default image.
|
||||
//! - `/user/{username}`: Gives username's pfp or redirects to the default image's path (for better
|
||||
//! cache control) if there's no image.
|
||||
//!
|
||||
//! Must be scoped at [`ws::IMAGES`]
|
||||
|
||||
// TODO: etags
|
||||
|
||||
use actix_web::{
|
||||
HttpResponse, get,
|
||||
http::header,
|
||||
@@ -15,7 +13,7 @@ use actix_web::{
|
||||
|
||||
use crate::{
|
||||
consts::{self, web_scopes as ws},
|
||||
server,
|
||||
server::AppData,
|
||||
};
|
||||
|
||||
#[must_use]
|
||||
@@ -25,33 +23,39 @@ pub fn make_scope(path: &str) -> actix_web::Scope {
|
||||
.service(get_image)
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn get_default_image(
|
||||
_data: web::Data<&server::AppState>,
|
||||
_username: web::Path<String>,
|
||||
) -> HttpResponse {
|
||||
#[get("/default")]
|
||||
async fn get_default_image() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.insert_header((
|
||||
header::CACHE_CONTROL,
|
||||
consts::DEFAULT_USER_PFP_CACHES_HEADER,
|
||||
))
|
||||
.content_type(consts::DEFAULT_USER_PFP_MIME)
|
||||
.body(web::Bytes::from_static(consts::DEFAULT_USER_PFP))
|
||||
.insert_header((
|
||||
header::ETAG,
|
||||
const_str::concat!('"', consts::DEFAULT_USER_PFP.shasum_str, '"'),
|
||||
))
|
||||
.content_type(consts::DEFAULT_USER_PFP.mime)
|
||||
.body(web::Bytes::from_static(consts::DEFAULT_USER_PFP.bytes))
|
||||
}
|
||||
|
||||
#[get("/{username}")]
|
||||
#[get("/user/{username}")]
|
||||
async fn get_image(
|
||||
data: web::Data<&server::AppState>,
|
||||
data: AppData,
|
||||
username: web::Path<String>,
|
||||
) -> web::Either<Redirect, HttpResponse> {
|
||||
let cached_pfp = data.cache.get_pfp(username.to_string()).await;
|
||||
|
||||
cached_pfp.as_ref().map_or_else(
|
||||
|| web::Either::Left(web::Redirect::to(ws::IMAGES).temporary()),
|
||||
|| {
|
||||
web::Either::Left(
|
||||
web::Redirect::to(const_str::concat!(ws::IMAGES, "/default")).temporary(),
|
||||
)
|
||||
},
|
||||
|img| {
|
||||
web::Either::Right(
|
||||
HttpResponse::Ok()
|
||||
.insert_header((header::CACHE_CONTROL, consts::USER_CACHES_HEADER))
|
||||
.insert_header((header::ETAG, img.shasum.as_ref()))
|
||||
.content_type(img.mime.as_ref())
|
||||
.body(web::Bytes::copy_from_slice(img.bytes.as_ref())),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use actix_web::{
|
||||
HttpRequest, HttpResponse,
|
||||
web::{self, Bytes},
|
||||
};
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use futures_util::stream;
|
||||
|
||||
use crate::consts;
|
||||
@@ -11,9 +8,10 @@ fn mix_u32s(seed: impl Iterator<Item = u32>) -> u32 {
|
||||
const U32_BIT_MIXING_CONSTANT: u32 = 0x7feb_352d;
|
||||
const U32_SELF_BIT_MIXING: u32 = 0x846c_a68b;
|
||||
|
||||
let mut random = seed.fold(0_u32, |acc, e| {
|
||||
let mut random = seed.fold(0_u32, |mut acc, e| {
|
||||
// mix bits
|
||||
acc.wrapping_add(e.wrapping_mul(U32_BIT_MIXING_CONSTANT))
|
||||
acc ^= e.wrapping_mul(e);
|
||||
acc.wrapping_mul(U32_BIT_MIXING_CONSTANT).wrapping_add(e)
|
||||
});
|
||||
|
||||
// GPT's suggestion for a nicer distribution, like 8 cpu instructions so whatever
|
||||
@@ -25,6 +23,16 @@ fn mix_u32s(seed: impl Iterator<Item = u32>) -> u32 {
|
||||
}
|
||||
|
||||
pub async fn not_found(req: HttpRequest) -> HttpResponse {
|
||||
if req.method().as_str().eq_ignore_ascii_case("HEAD") {
|
||||
return HttpResponse::NotImplemented()
|
||||
.insert_header((
|
||||
"Unimplemented",
|
||||
"The HEAD method is not yet implemented \
|
||||
and this response is not valid for this endpoint",
|
||||
))
|
||||
.body(());
|
||||
}
|
||||
|
||||
let seed = req.path().as_bytes();
|
||||
let random = mix_u32s(seed.iter().copied().map(u32::from));
|
||||
|
||||
@@ -39,7 +47,7 @@ pub async fn not_found(req: HttpRequest) -> HttpResponse {
|
||||
Ok::<_, !>(web::Bytes::from(format!(
|
||||
"> {method} '{url}'\n404 NOT FOUND\nLet's all love lain\n\n"
|
||||
))),
|
||||
Ok(Bytes::from_static(res.as_bytes())),
|
||||
Ok(web::Bytes::from_static(res.as_bytes())),
|
||||
]))
|
||||
// .body(format!(
|
||||
// "> {method} '{url}'\n404 NOT FOUND\nLet's all love lain\n\n{res}"
|
||||
|
||||
38
src/server/static_app_data.rs
Normal file
38
src/server/static_app_data.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::{future::Ready, ops::Deref};
|
||||
|
||||
use actix_web::{error::InternalError, http::StatusCode};
|
||||
|
||||
/// App data extractor without any [`Arc`] inbetween, perefct if the app data is a simple
|
||||
/// `&'static` reference that can be copied, no atomic anywhere.
|
||||
pub struct StaticAppDataExtractor<T> {
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
impl<T> Deref for StaticAppDataExtractor<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy + 'static> actix_web::FromRequest for StaticAppDataExtractor<T> {
|
||||
type Error = InternalError<&'static str>;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(
|
||||
req: &actix_web::HttpRequest,
|
||||
_payload: &mut actix_web::dev::Payload,
|
||||
) -> Self::Future {
|
||||
let val = match req.app_data::<T>() {
|
||||
Some(&data) => Ok(Self { data }),
|
||||
None => Err(InternalError::new(
|
||||
"Requested application data is not configured correctly. \
|
||||
View/enable debug logs for more details.",
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)),
|
||||
};
|
||||
|
||||
std::future::ready(val)
|
||||
}
|
||||
}
|
||||
26
src/utils.rs
26
src/utils.rs
@@ -37,3 +37,29 @@ pub mod web {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod shasum {
|
||||
#[must_use]
|
||||
pub fn sha256sum_to_hex_string(sha256sum: &[u8]) -> String {
|
||||
let mut out = String::with_capacity(sha256sum.len() * 2);
|
||||
|
||||
for &b in sha256sum {
|
||||
/// Only correct as long as `n` is ranged within a nibble
|
||||
#[inline]
|
||||
const fn nibble_to_hex(n: u8) -> char {
|
||||
(match n {
|
||||
0..=9 => b'0' + n,
|
||||
_ => b'a' + (n - 10),
|
||||
}) as char
|
||||
}
|
||||
|
||||
let hi = (b >> 4) & 0x0f;
|
||||
let lo = b & 0x0f;
|
||||
|
||||
out.push(nibble_to_hex(hi));
|
||||
out.push(nibble_to_hex(lo));
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user