Skip to content

Instantly share code, notes, and snippets.

@rrbutani
Last active January 31, 2026 06:35
Show Gist options
  • Select an option

  • Save rrbutani/5aa0d1d75c3db254568102e32452c024 to your computer and use it in GitHub Desktop.

Select an option

Save rrbutani/5aa0d1d75c3db254568102e32452c024 to your computer and use it in GitHub Desktop.
use nix -p rustc cargo rustfmt clippy rust-analyzer
[package]
name = "fxhash-collide"
version = "0.1.0"
edition = "2024"
[dependencies]
fxhash = "0.2.1"
rustc-hash = "2.1.1"
[[bin]]
name = "main"
path = "main.rs"
#[allow(unused)]
use std::{array, collections::HashMap, hash::{Hash, Hasher}};
use fxhash::FxHasher64;
struct StringGen<'s, const LEN: usize, ByteIt: Iterator<Item = u8> + Clone> {
iter: ByteIt,
iters: [ByteIt; LEN],
curr: &'s mut [u8; LEN],
done: bool,
}
impl<'s, const L: usize, I: Iterator<Item = u8> + Clone> StringGen<'s, L, I> {
pub fn new(iter: I, buf: &'s mut [u8; L]) -> Self {
let iters = array::from_fn(|i| {
let mut it = iter.clone();
let first = it.next().unwrap();
buf[i] = first;
it
});
StringGen {
iter: iter.clone(),
iters,
curr: buf,
done: false,
}
}
pub fn next(&mut self) -> Option<&[u8; L]> {
if self.done { return None; }
// iterate over output byte positions:
for (idx, iter) in self.iters.iter_mut().enumerate() {
// if we've got a new byte for the position great – swap it in and
// yield the resulting value:
if let Some(next) = iter.next() {
self.curr[idx] = next;
return Some(self.curr)
} else {
// if not, we need to restart the iterator for this position and
// then continue on upwards to the next position (left)
*iter = self.iter.clone();
self.curr[idx] = iter.next().unwrap();
}
}
// if the loop above completes we've exhausted the iterator for the
// first position meaning that we are done:
self.done = true;
None
}
}
#[cfg(false)]
fn main_sweep() {
let char_it = (b'a'..=b'z').chain(b'A'..=b'Z');
let mut buf = [0; 8];
let mut it = StringGen::new(char_it, &mut buf);
let mut hash_count = HashMap::<u64, u16>::new();
let mut highest_count = 1;
while let Some(str) = it.next() {
let hash = {
let mut hash = FxHasher64::default();
let s: &str = unsafe { str::from_utf8_unchecked(str) };
s.hash(&mut hash);
hash.finish()
};
let count = hash_count.entry(hash).or_default();
*count += 1;
if *count > highest_count {
highest_count = *count;
eprintln!("{highest_count:5} collisions; {} uniq hash vals", hash_count.len());
}
if hash_count.len() % 10_000_000 == 0 {
eprintln!(" uniq hash vals: {} M", hash_count.len() / 1_000_000);
}
}
}
fn main() {
if std::env::args().len() >= 1 {
// rustc_hash
let seed1 = 0x24_3f_6a_88__85_a3_08_d3u64;
let seed2 = 0x13_19_8a_2e__03_70_73_44u64;
// let seed3 = 0xa4_09_38_22__29_9f_31_d0u64;
eprintln!("{seed1:#18X}");
eprintln!("{seed2:#18X}");
// eprintln!("{seed3:#18X}");
eprintln!("{:#4X?}", seed1.to_le_bytes());
eprintln!("{:#4X?}", seed2.to_le_bytes());
// eprintln!("{:#4X?}", seed3.to_le_bytes());
eprintln!("{:?}", str::from_utf8(seed1.to_be_bytes().as_slice()));
eprintln!("{:?}", str::from_utf8(seed2.to_be_bytes().as_slice()));
// eprintln!("{:?}", str::from_utf8(seed3.to_be_bytes().as_slice()));
/*
rustc_hash
```
a, b = inp_qword1, inp_qword2
let [lo, hi] = (seed1 ^ a) * (seed2 ^ b);
lo ^ hi
```
ideal would be either `seed1 ^ a` or `seed2 ^ b` being 0 but unfortunately
the seed values just so happen to be selected such that it's impossible to
make this happen if a/b must be valid UTF-8 strings
so we have to engage with the multiply...
----------
one option is to rely on "negative interference" between lo and hi
- means that there's a wider search space available but... harder to
reason about?
----------
another is to target a specific product; i.e. a specific lo and hi
- and to then find many pairs of `A` and `B` that result in that product
- such that `A ^ seed1` and `B ^ seed2` result in ascii printable strings
should go at it the other way:
- build up a set of factors that we can produce by picking ASCII (really
unicode) `S` and xor'ing it with seed1 and seed2
- hope that we can find a number that is composed of enough pairs of
factors?
arbitrary: lo == hi == X
find A_u64 * B_u64
*/
// for now, just generate collisions that aren't valid UTF-8.
let mut outs = Vec::with_capacity(512);
let char_it = b'!'..=b'$';
let mut buf = [0; 8];
let mut it = StringGen::new(char_it, &mut buf);
let s1 = seed1.to_le_bytes();
for _ in 0..512 {
let second = it.next().unwrap();
let str: [u8; 16] = array::from_fn(|i| {
if i < 8 { s1[i] } else { second[i - 8] }
});
outs.push(str);
}
let mut hash = None;
for o in outs {
let str = unsafe { str::from_utf8_unchecked(o.as_slice()) };
let mut hasher = rustc_hash::FxHasher::default();
str.hash(&mut hasher);
if let Some(h_val) = hash {
assert_eq!(h_val, hasher.finish());
} else {
hash = Some(hasher.finish())
}
// assert no `\n` or `;` bytes:
for b in o {
assert_ne!(b, b'\n');
assert_ne!(b, b';');
}
println!("{str}");
}
return;
}
// let char_it = (b'a'..=b'z').chain(b'A'..=b'Z');
let char_it = b'a'..=b'z'; // let's do only ASCII lowercase, for fun
let mut buf = [0; 8];
let mut it = StringGen::new(char_it, &mut buf);
let filter = |byte: &u8| {
// byte.is_ascii()
// byte.is_ascii_alphanumeric()
// we're going to be (arbitrarily) even more picky and only accept
// alphabetical chars that are lowercase:
byte.is_ascii_alphabetic() && byte.is_ascii_lowercase()
};
let num_collisions = 512;
let mut collisions = Vec::with_capacity(num_collisions);
let mut expected_str_hash = None;
while let Some(str) = it.next() && collisions.len() < num_collisions {
let mut hash = FxHasher64::default();
hash.write(str);
let first_u64_hash = hash.finish();
// hash_out := hash_curr.rotate_left(5).bitxor(next_u64).wrapping_mul(...)
//
// we want to pick `next_u64` such that `hash_out` is `0 * ...`
let next_u64 = first_u64_hash.rotate_left(5);
// only keep `next_u64` values that decompose into printable (ASCII)
// bytes:
let next_bytes = next_u64.to_le_bytes();
if !next_bytes.iter().all(filter) {
continue
}
// construct the 16 byte input:
let inp: [u8; 16] = array::from_fn(|i| {
if i < 8 { str[i] } else { next_bytes[i - 8] }
});
// verify that its hash is 0:
let mut hash = FxHasher64::default();
hash.write(&inp);
assert_eq!(hash.finish(), 0);
// verify that when interpreted as a string, the hash of the bytes is
// consistent with past results
//
// (note that this hash will not be 0 because `write_str` hashes an
// extra `0xFFu8`)
let as_str = unsafe { str::from_utf8_unchecked(inp.as_slice()) };
let mut hash = FxHasher64::default();
as_str.hash(&mut hash);
let str_hash = hash.finish();
if let Some(h) = expected_str_hash {
assert_eq!(h, str_hash);
} else {
expected_str_hash = Some(str_hash);
}
collisions.push(inp);
eprint!(".");
}
eprintln!();
// print values
for c in collisions {
println!("{}", unsafe { str::from_utf8_unchecked(c.as_slice()) });
}
}
oafffhcawsadnliq
oaffngeegsadnaaw
oaffngucwsadnaaa
oaffnoifasadnavl
oaffnoqeysadnava
oaffnoyaysadnavv
oaffnwpdgsadnaky
oaffvcweusadnvac
oaffvkcarsadnvvy
oaffvsbcxsadnvkq
oaffvsjbpsadnvkf
oafnebhbisadcdzi
oafnebxevsadcdzs
oafnejgdosadcdoa
oafnernbusadcddn
oafnervamsadcddc
oafnezjdwsadcdyn
oafnezrcosadcdyc
oafnmfifmsadcyoc
oafnmfqbmsadcyox
oafnmfyaesadcyom
oafnmnpdssadcydp
oafnmnxcksadcyde
oafnmvlfusadcyyp
oafnmvtemsadcyye
oafnueccpsadcngt
oafnuekbhsadcngi
oafnuuffpsadcnqa
oafnuunbpsadcnqv
oafnuuvahsadcnqk
oafvalqbcsadxdqt
oafvatheqsadxdfw
oafvatpdisadxdfl
oafvatxcasadxdfa
oafvihcatsadxyql
oafvihsdasadxyqv
oafvipbczsadxyfd
oafvqgeedsadxnir
oafvqwpddsadxnst
oafvynccasadxcvx
oafvynkbysadxcvm
oafvynsaqsadxcvb
oafvyvbegsadxckp
oanbhbfbdsaynnrm
oanbhbveqsaynnrw
oanbhjedjsaynnge
oanbhzhdrsaynnqr
oanbhzpcjsaynnqg
oanbpahftsayncjs
oanbpapelsayncjh
oanbpqcbgsaynctk
oanbpqsetsaynctu
oanbpybdmsayncic
oanbxmedesaynxtm
oanbxudfksaynxie
oanbxulbksaynxiz
oanbxutacsaynxio
oanjgdbfdsaycfxh
oanjglidjsaycfmu
oanjglqcbsaycfmj
oanjgthfpsaycfbm
oanjgtpehsaycfbb
oanjgtxahsaycfbw
oanjwgefcsaycpeh
oanjwoybusaycpzg
oanjwwhfksaycpou
oanjwwpecsaycpoj
oanrcftcpsayxfzp
oanrcnsevsayxfoh
oanrcvbawsayxfdv
oanrsahfqsayxprn
oanrsapeisayxprc
oanrsaxaisayxprx
oanrsiwcosayxpgp
oanzjlidgsaymsup
oanzjthfmsaymsjh
oanzjtxaesaymsjr
oanzrcteisaymhxs
oanzrkcdbsaymhma
oanzrsbcpsaymhby
oanzrsjbhsaymhbn
oanzrszeusaymhbx
oavajfabcsanftbp
oavajfqepsanftbz
oavajfydhsanftbo
oavajvlacsanftlr
oavarehegsanfizs
oavarmwbesanfiou
oavarunessanfidx
oavaruvdksanfidm
oavqebheisanpluf
oavqejocosanpljs
oavqejwbgsanpljh
oavqezzbosanpltu
oavqmabblsanpamw
oavqmajadsanpaml
oavqmazdqsanpamv
oavqmiadrsanpabo
oavqmiicjsanpabd
oavqmqeelsanpawd
oavqmqmalsanpawy
oavqmylcrsanpalq
oavqmytbjsanpalf
oavquecfpsanpvbq
oavquekehsanpvbf
oavqumwbbsanpvwp
oavquunepsanpvls
oavquuvdhsanpvlh
oavydtbbhsanedeq
oavydtzdmsanedep
oavylhmaxsaneypp
oavylpddfsaneyes
oavytggassanenha
oavytgoehsanenhv
oavytwbbcsanenry
oavytwzdhsanenrx
oibhhrbcysvdpjrd
oibhxmfarsvdptjl
oibpgdccqsvdebng
oibpgljawsvdebct
oibpgtfcysvdebxt
oibpgtnbqsvdebxi
oibpohlcusvdewcv
oibpohtbmsvdewck
oibpophewsvdewxv
oibpoppdosvdewxk
oibpoxwbusvdewmx
oibpwobersvdelpg
oibpwwicxsvdelet
oibpwwqbpsvdelei
oibxcfeeesvdzbpe
oibxcfmaesvdzbpz
oibxcnlcksvdzber
oibxcntbcsvdzbeg
oibxcvhemsvdzbzr
oibxcvpdesvdzbzg
oibxkjneisvdzwet
oibxkjvdasvdzwei
oibxkzabdsvdzwow
oibxkzydisvdzwov
oibxsaadfsvdzlhx
oibxsaqbvsvdzlhb
oibxsqlcfsvdzlrz
oibxsykelsvdzlgr
oibxsysddsvdzlgg
oijdbegcqsvypwnz
oijdbeobisvypwno
oijdbewaasvypwnd
oijdbmfewsvypwcr
oijdbmndosvypwcg
oijdbujfqsvypwxg
oijdbuzaisvypwxq
oijdjdaclsvyplfk
oijdjdyeqsvyplfj
oijdjtdctsvyplpx
oijdjtlblsvyplpm
oijdjttadsvyplpb
oijdrkwcnsvypasp
oijdrsvetsvypahh
oijdzgacgsvypvss
oijdzgyelsvypvsr
oijlagkfisvyeotj
oijlaobabsvyeoim
oijlaordosvyeoiw
oijlaozcgsvyeoil
oijlifmbdsvyedlp
oijlindersvyedas
oijlinldjsvyedah
oijlivhflsvyedvh
oijlivxadsvyedvr
oijlqbodbsvyeylr
oijlqbwczsvyeylg
oijlqjnfhsvyeyaj
oijlqrzcbsvyeyvt
oijlqzaccsvyeykm
oijlqzyehsvyeykl
oijlyaaeesvyendn
oijlyiuawsvyenym
oijlyqldesvyennp
oijlyykfksvyench
oijtehodxsvyzdnn
oijtehwcpsvyzdnc
oijtexbassvyzdxq
oijtexzcxsvyzdxp
oijtmdacisvyzynf
oijtmdyensvyzyne
oijtmlhaosvyzycs
oijtmtdcqsvyzyxs
oijtmtlbisvyzyxh
oijtuckfqsvyznfa
oijtucsbqsvyznfv
oijtusnfysvyznpn
oijtusveqsvyznpc
oirclhqelsvnhrvu
oirclhyddsvnhrvj
oirctgcfosvnhgnq
oirctgkegsvnhgnf
oirctorcmsvnhgcs
oirctozbesvnhgch
oirctwneosvnhgxs
oirctwvdgsvnhgxh
oirsgdcfqsvnrjid
oirsgdkbqsvnrjiy
oirsgdsaisvnrjin
oirsgtffysvnrjsq
oirsgtneqsvnrjsf
oirswgcflsvnrtvl
oirswgkedsvnrtva
oirswgsadsvnrtvv
oirswojdrsvnrtky
oirsworcjsvnrtkn
oirswozbbsvnrtkc
oizjbjubssvcucsq
oizjbrleasvcucht
oizjbrtdysvcuchi
oizjjfgadsvcuxsi
oizjjfwdqsvcuxss
oizjjnfcjsvcuxha
oizjreietsvcumko
oizjreqdlsvcumkd
oizjrudagsvcumug
oizjrutdtsvcumuq
oizjzdkaosvcubcu
oizjzlgcqsvcubxu
oizjzlobisvcubxj
oizjztfewsvcubmm
oizjztndosvcubmb
oizrihcedsvcjpyd
oizrihkadsvcjpyy
oizripjcjsvcjpnq
oizriprbbsvcjpnf
oizrixafxsvcjpct
oizrixiepsvcjpci
oizrqgudlsvcjeqt
oizrqodcesvcjefb
oizrycobusvcjzqa
oizrykfecsvcjzfd
oizryknacsvcjzfy
oqiffhfduskqvgfq
oqiffhncmskqvgff
oqiffxqcuskqvgps
oqiffxybmskqvgph
oqifvkfdpskqvqsy
oqifvknchskqvqsn
oqifvsefvskqvqhq
oqifvsmenskqvqhf
oqinmflavskqktlc
oqinmftekskqktlx
oqinmncddskqktaf
oqinueffnskqkidt
oqinuenefskqkidi
oqinuuabaskqkinl
oqinuuiayskqkina
oqinuuqenskqkinv
oqinuuydfskqkink
oqqehdjafskfnmaq
oqqehlfchskfnmvq
oqqehteenskfnmki
oqqepcqdjskfnbyt
oqqepcycbskfnbyi
oqqepkxehskfnbna
oqqepsgaiskfnbco
oqqepswdvskfnbcy
oqqexgbeaskfnwnd
oqqexgjaaskfnwny
oqqexoicgskfnwcq
oqqexweeiskfnwxq
oqqexwmdaskfnwxf
oqqmgffefskfcegl
oqqmgvaayskfceqd
oqqmgvienskfceqy
oqqmgvqdfskfceqn
oqqmorccwskfczqf
oqqmwifeaskfcott
oqqmwindyskfcoti
oqquchxbrskfxeit
oqqucxccmskfxesb
oqqukdbbkskfxziw
oqqukdjacskfxzil
oqqukdzdpskfxziv
oqqukteekskfxzsd
oqquktmakskfxzsy
oqqusclesskfxoar
oqqusctdkskfxoag
oqqussgafskfxokj
oqqusswdsskfxokt
whdddhvclhnjkuvy
whdddpebehnjkukg
whdddpuerhnjkukq
whddlghdohnjkjnu
whddlgpcghnjkjnj
whddlogfuhnjkjcm
whddlooemhnjkjcb
whddlowamhnjkjcw
whddlwscohnjkjxw
whdtgcbathnjubay
whdtgczcyhnjubax
whdtgkfbnhnjubvn
whdtgknafhnjubvc
whdtgsedthnjubkf
whdtoghdlhnjuwvp
whdtogpcdhnjuwve
whdtoogfrhnjuwkh
whdtoowajhnjuwkr
whdtwfbdghnjulna
whdtwnacuhnjulcy
whdtwnibmhnjulcn
whdtwnqaehnjulcc
whdtwnyezhnjulcx
whdtwvedohnjulxn
whdtwvmcghnjulxc
whtglwccbhntxrsj
whtgtfjcjhntxgax
whtgtfrbbhntxgam
whtgtfzazhntxgab
whtgtnfelhntxgvx
whtgtnnddhntxgvm
whtgtvmfjhntxgke
whtgtvubjhntxgkz
whtocjrbwhntmuwq
whtocjzaohntmuwf
whtocrieehntmult
whtoczxbchntmuav
whtokaeathntmjzu
whtokidczhntmjom
whtokilbrhntmjob
whtokygcbhntmjyz
whtokyobzhntmjyo
whtokywarhntmjyd
whtwbdbahhntbmhb
whtwbdrduhntbmhl
whtwbdzcmhntbmha
whtwbteaphntbmro
whtwjkpbrhntbbur
whtwjkxajhntbbug
whtwjsodxhntbbjj
whtwrgbachntbwuj
whtwrgrdphntbwut
whtwrgzchhntbwui
whtwroacihntbwjb
whtwroyenhntbwja
whtwzfdeshntblmp
whtwzfldkhntblme
whtwznsbqhntblbr
whtwzvodshntblwr
whtwzvwckhntblwg
wpcfhcnfjhcbqeyc
wpcfhcvbjhcbqeyx
wpcfhkeachcbqenf
wpcfhkudphcbqenp
wpcfpggcahcbqznh
wpcfponaghcbqzcu
wpcfpwjcihcbqzxu
wpcfpwrbahcbqzxj
wpcfxfybihcbqofx
wpcfxvdcdhcbqopf
wpcnoikcahcbfrtx
wpcnoisbyhcbfrtm
wpcnoqjeghcbfrip
wpcnwpdebhcbfgaa
wpcnwplabhcbfgav
wpcnwxhcdhcbfgvv
wpkbbneechcwqrtt
wpkbbvlfahcwqria
wpkbbvtbahcwqriv
wpkbjehbphcwqgwb
wpkbjufcdhcwqgar
wpkbzhccwhcwqqdm
wpkbzhkbohcwqqdb
wpkbzpgdqhcwqqyb
wpkbzxnbwhcwqqno
wpkbzxvaohcwqqnd
wpkjahmfyhcwfjed
wpkjahubyhcwfjey
wpkjapaanhcwfjzo
wpkjapycshcwfjzn
wpkjaxxeyhcwfjof
wpkjqkmfthcwftrl
wpkjqkuelhcwftra
wpkjqsdamhcwftgo
wpkjqstdzhcwftgy
wpkjybhdihcwfiue
wpkjyjobohcwfijr
wpkjyjwaghcwfijg
wpkjyzjfwhcwfitj
wpkjyzzaohcwfitt
wpkzdhubvhcwpwmt
wpkzdpdaohcwpwbb
wpkzdpledhcwpwbw
wpkzdxxevhcwpwwa
wpkzlggcyhcwplep
wpkzlgobqhcwplee
wpkzlokdshcwplze
wpkzlwrbyhcwplor
wpkzlwzaqhcwplog
wpkztneavhcwparv
wpkztvlbthcwpagc
wpsadbbdnhclixzg
wpsadjibthclixot
wpsadjqalhclixoi
wpsadrhdzhclixdl
wpsadrpcrhclixda
wpsadzlethclixya
wpsadztathclixyv
wpsalatcvhclimrw
wpsalicbohclimge
wpsalikfdhclimgz
wpsalyfbwhclimqr
wpsalynaohclimqg
wpsatpqcyhclibtu
wpsatpybqhclibtj
wpsatxhfghclibix
wpsatxxdwhclibib
wpsqgerauhclsezp
wpsqgmidchclseos
wpsqguhfihclsedk
wpsqguxaahclsedu
wpsqoatcshclszzr
wpsqoikfahclszou
wpsqoiseyhclszoj
wpsqoqbazhclszdx
wpsqoyfbthclszym
wpsqoynalhclszyb
wpsqoyveahclszyw
wpsqwhuathclsogp
wpsqwxhfdhclsoqs
wpsynkgblhclhrup
wpsynkoadhclhrue
wpsynkweyhclhruz
wpsynsfdrhclhrjh
wpsyvbrcnhclhgxs
wpsyvbzbfhclhgxh
wpsyvjabghclhgma
wpsyvjqethclhgmk
wpsyvrxczhclhgbx
wpsyvzdbohclhgwn
wpsyvzlaghclhgwc
wxgddhadehxwspsz
wxgddhqbuhxwspsd
wxgddphechxwsphg
wxgdlwnbxhxwseub
wxgdtceevhxwszkb
wxgdtcmavhxwszkw
wxgdtspdvhxwszud
wxglcbafchxwhhdu
wxglcjubuhxwhhyt
wxglcrdanhxwhhnb
wxglcrlechxwhhnw
wxglseievhxwhrqr
wxglseqdnhxwhrqg
wxglsmxbthxwhrft
wxocfdeavhxlkvnz
wxocfllbthxlkvcg
wxocfthdvhxlkvxg
wxocncwcvhxlkkfj
wxocnsbayhxlkkpx
wxosahkfbhxlunvh
wxosaprdhhxlunku
wxosigecehxlucny
wxosiguauhxlucnc
wxosiodekhxluccq
wxosioldchxluccf
wxosiwhfehxlucxf
wxosqcwcshxluxne
wxosqknfahxluxch
wxosqsbavhxluxxs
wxosybidvhxlumfa
wxosyrdefhxlumpy
wxosyrtcvhxlumpc
wxosyzkfdhxlumef
wxwbpgocqhxacqvu
wxwbpgwbihxacqvj
wxwbponewhxacqkm
wxwbpovdohxacqkb
wxwbxfadthxacfnq
wxwbxficlhxacfnf
wxwbxnparhxacfcs
wxwbxvlcthxacfxs
wxwbxvtblhxacfxh
wxwjlaucchxaxqcs
wxwjliabxhxaxqxi
wxwjliqeehxaxqxs
wxwjlygbdhxaxqbn
wxwjlyweqhxaxqbx
wxwjthcfhhxaxfpo
wxwjtprcfhxaxfeq
wxwjtxnehhxaxfzq
wxwrcdocshxamtqh
wxwrclffahxamtfk
wxwrclvayhxamtfu
wxwrkcadvhxamiid
wxwrkslcvhxamisf
wxwzbfscshxablwx
wxwzbnbblhxabllf
wxwzbnreyhxabllp
wxwzbnzdqhxablle
wxwzjeedvhxabaot
wxwzjemcnhxabaoi
wxwzjmlethxabada
wxwzjmtathxabadv
wxwzjupcvhxabayv
wxwzjuxbnhxabayk
wxwzragfthxabvov
wxwzraoelhxabvok
wxwzrivcrhxabvdx
wxwzrqbbghxabvyn
wxwzrqrethxabvyx
wxwzrqzdlhxabvym
wxwzryadmhxabvnf
wxwzzpdfwhxabkqt
wxwzzpleohxabkqi
wxwzzxscuhxabkfv
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment