initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+143
@@ -0,0 +1,143 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "dsa-emu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"fxhash",
|
||||
"ron",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"typeid",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "dsa-emu"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
arc-swap = "1.8.2"
|
||||
fxhash = "0.2.1"
|
||||
ron = "0.12.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
@@ -0,0 +1,7 @@
|
||||
LoadConfig(
|
||||
// defined as the bootloader in dsa.mmap.ron
|
||||
start_address: 0x1000,
|
||||
|
||||
// maximum allowed bootloader size (as to not overlap MMIO)
|
||||
max_size: 0x4000,
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
MemoryMap(
|
||||
table_addr: 0x1000,
|
||||
regions: [
|
||||
// 0000-1000 = reserved
|
||||
Region(
|
||||
base: 0x0,
|
||||
size: 0x1000,
|
||||
region_type: Reserved
|
||||
),
|
||||
// 1000-4000 = bootloader
|
||||
Region(
|
||||
base: 0x1000,
|
||||
size: 0x3000,
|
||||
region_type: Bootloader
|
||||
),
|
||||
// 5000-0FFFFFFF = MMIO
|
||||
Region(
|
||||
base: 0x5000,
|
||||
size: 0x0FFF_FFFF,
|
||||
region_type: MMIO
|
||||
),
|
||||
// 10000000-BFFFFFFF = userspace
|
||||
Region(
|
||||
base: 0x1000_0000,
|
||||
size: 0xBFFF_FFFF,
|
||||
region_type: Usable
|
||||
),
|
||||
// C0000000-C0FFFFFF = kernel page tables
|
||||
Region(
|
||||
base: 0xC000_0000,
|
||||
size: 0x100_0000,
|
||||
region_type: KernelTables
|
||||
),
|
||||
// C1000000-FFFFFFFF = kernel
|
||||
Region(
|
||||
base: 0xC100_0000,
|
||||
size: 0x3EFF_FFFF,
|
||||
region_type: Kernel
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicU8},
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::processor::{processor::Emulator, state::SharedState};
|
||||
|
||||
mod memory;
|
||||
mod processor;
|
||||
|
||||
fn main() {
|
||||
let mut emulator = Emulator::new();
|
||||
let state = emulator.state_handle();
|
||||
let runner = thread::spawn(move || emulator.run());
|
||||
let observer = thread::spawn(|| observe(state));
|
||||
|
||||
runner.join().unwrap();
|
||||
observer.join().unwrap();
|
||||
}
|
||||
|
||||
fn observe(state: Arc<SharedState>) {
|
||||
loop {
|
||||
thread::sleep(std::time::Duration::from_millis(100));
|
||||
let state = state.proc.load();
|
||||
println!("GP Registers: {:?}", state.gp_registers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use fxhash::FxHashMap;
|
||||
|
||||
use crate::memory::PhysAddr;
|
||||
|
||||
type Page = [u8; 4096];
|
||||
|
||||
pub struct MainStore {
|
||||
pages: FxHashMap<PhysAddr, Page>,
|
||||
}
|
||||
|
||||
impl MainStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pages: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
const fn segment_addr(addr: PhysAddr) -> (PhysAddr, usize) {
|
||||
(addr & !(0xFFF), (addr & 0xFFF) as usize)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn read_byte(&self, addr: PhysAddr) -> u8 {
|
||||
let (page, offset) = Self::segment_addr(addr);
|
||||
self.pages.get(&page).map(|p| p[offset]).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn read_word(&self, addr: PhysAddr) -> u32 {
|
||||
let (page, offset) = Self::segment_addr(addr);
|
||||
let page = self.pages.get(&page).unwrap_or(&[0; 4096]);
|
||||
u32::from_be_bytes(page[offset..=offset + 3].try_into().unwrap())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn read_page(&self, addr: PhysAddr) -> &[u8] {
|
||||
self.pages.get(&addr).unwrap_or(&[0; 4096])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_byte(&mut self, addr: PhysAddr, value: u8) {
|
||||
let (page, offset) = Self::segment_addr(addr);
|
||||
self.pages.entry(page).or_insert_with(|| [0; 4096])[offset] = value;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_word(&mut self, addr: PhysAddr, value: u32) {
|
||||
let (page, offset) = Self::segment_addr(addr);
|
||||
let page = self.pages.entry(page).or_insert_with(|| [0; 4096]);
|
||||
page[offset..=offset + 3].copy_from_slice(&value.to_be_bytes());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_page(&mut self, addr: PhysAddr, value: &[u8; 4096]) {
|
||||
let (page, offset) = Self::segment_addr(addr);
|
||||
let page = self.pages.entry(page).or_insert_with(|| [0; 4096]);
|
||||
page[offset..offset + value.len()].copy_from_slice(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
use crate::memory::{FaultInfo, PhysAddr, VirtAddr, tlb::TLB};
|
||||
|
||||
pub struct MMU {
|
||||
buffer: TLB,
|
||||
enable_paging: bool,
|
||||
|
||||
// Memory Management Register (MMR) Tells MMU where to find page tables
|
||||
mmr_value: u32,
|
||||
|
||||
page_table: Box<[Option<PageTable>; 1024]>,
|
||||
}
|
||||
|
||||
impl MMU {
|
||||
pub fn new() -> Self {
|
||||
MMU {
|
||||
buffer: TLB::new(),
|
||||
enable_paging: false,
|
||||
mmr_value: 0,
|
||||
page_table: Box::new([const { None }; 1024]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paging_enabled(&mut self, enable: bool) {
|
||||
self.enable_paging = enable;
|
||||
}
|
||||
|
||||
pub fn tlb_insert(&mut self, virtual_address: VirtAddr, physical_address: PhysAddr) {
|
||||
self.buffer.insert(virtual_address, physical_address);
|
||||
}
|
||||
|
||||
pub fn create_mapping(&mut self, virtual_address: VirtAddr, physical_address: PhysAddr) {
|
||||
let table = (virtual_address >> 22) as usize;
|
||||
let page = ((virtual_address >> 12) & 0x3FF) as usize;
|
||||
|
||||
self.page_table[table].get_or_insert_default().entries[page] =
|
||||
Some(PageTableEntry::new(physical_address, true, true, false));
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn lookup(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
|
||||
if !self.enable_paging {
|
||||
return Ok(virtual_address);
|
||||
}
|
||||
|
||||
if let Some(addr) = self.buffer.lookup(virtual_address) {
|
||||
return Ok(addr);
|
||||
}
|
||||
|
||||
self.walk(virtual_address)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn walk(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
|
||||
let phys_addr = self.walk_page_table(virtual_address)?;
|
||||
self.buffer.insert(virtual_address, phys_addr);
|
||||
Ok(phys_addr)
|
||||
}
|
||||
|
||||
fn walk_page_table(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
|
||||
let table = (virtual_address >> 22) as usize;
|
||||
let page = ((virtual_address >> 12) & 0x3FF) as usize;
|
||||
|
||||
if let Some(table) = &self.page_table[table] {
|
||||
return table.entries[page]
|
||||
.map(|entry| entry.physical_addr)
|
||||
.ok_or(FaultInfo::PageFault);
|
||||
} else {
|
||||
Err(FaultInfo::PageFault)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PageDir {
|
||||
entries: Box<[PageTable; 1024]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PageTable {
|
||||
entries: Box<[Option<PageTableEntry>; 1024]>,
|
||||
}
|
||||
|
||||
impl Default for PageTable {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
entries: Box::new([None; 1024]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PageTableEntry {
|
||||
physical_addr: PhysAddr,
|
||||
writable: bool,
|
||||
present: bool,
|
||||
user: bool,
|
||||
}
|
||||
|
||||
impl PageTableEntry {
|
||||
fn new(physical_addr: PhysAddr, writable: bool, present: bool, user: bool) -> Self {
|
||||
Self {
|
||||
physical_addr,
|
||||
writable,
|
||||
present,
|
||||
user,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::memory::mmu::MMU;
|
||||
|
||||
mod cache;
|
||||
pub mod mainstore;
|
||||
pub mod mmu;
|
||||
mod tlb;
|
||||
|
||||
pub type VirtAddr = u32;
|
||||
pub type PhysAddr = u32;
|
||||
|
||||
pub enum FaultInfo {
|
||||
PageFault,
|
||||
}
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
enum RegionType {
|
||||
Reserved,
|
||||
Bootloader,
|
||||
Kernel,
|
||||
KernelTables,
|
||||
Usable,
|
||||
MMIO,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct Region {
|
||||
base: PhysAddr,
|
||||
size: usize,
|
||||
region_type: RegionType,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MemoryMap {
|
||||
table_addr: PhysAddr,
|
||||
pub regions: Vec<Region>,
|
||||
}
|
||||
|
||||
impl MemoryMap {
|
||||
pub const DEFAULT_MEMORY_MAP_ADDR: PhysAddr = 0x1000;
|
||||
|
||||
pub fn new() -> Self {
|
||||
MemoryMap {
|
||||
table_addr: Self::DEFAULT_MEMORY_MAP_ADDR,
|
||||
regions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn identity_map(mmu: &mut MMU, region: &Region) {
|
||||
let first_page = region.base >> 12;
|
||||
let page_count = (region.size >> 12) as u32;
|
||||
|
||||
for page in (first_page..first_page + page_count).map(|p| p << 12) {
|
||||
mmu.create_mapping(page, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MemoryMap {
|
||||
fn default() -> Self {
|
||||
ron::from_str(include_str!("../../config/dsa.mmap.ron")).unwrap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::memory::{PhysAddr, VirtAddr};
|
||||
|
||||
const TLB_SIZE: usize = 256;
|
||||
const TLB_MASK: u32 = (TLB_SIZE - 1) as u32;
|
||||
|
||||
pub struct TLB {
|
||||
entries: [TlbEntry; TLB_SIZE],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct TlbEntry {
|
||||
virtual_address: VirtAddr,
|
||||
physical_address: PhysAddr,
|
||||
valid: bool,
|
||||
}
|
||||
|
||||
impl TLB {
|
||||
pub fn new() -> Self {
|
||||
TLB {
|
||||
entries: [TlbEntry {
|
||||
virtual_address: 0,
|
||||
physical_address: 0,
|
||||
valid: false,
|
||||
}; TLB_SIZE],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn index(page: u32) -> usize {
|
||||
let hash = page ^ (page >> 12);
|
||||
(hash & TLB_MASK) as usize
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn lookup(&self, virtual_address: VirtAddr) -> Option<PhysAddr> {
|
||||
let page = virtual_address >> 12; // for 4KB pages
|
||||
let entry = self.entries[Self::index(page)];
|
||||
if entry.valid && entry.virtual_address == virtual_address {
|
||||
Some(entry.physical_address)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn insert(&mut self, virt: VirtAddr, phys: PhysAddr) {
|
||||
let idx = Self::index(virt) as usize;
|
||||
self.entries[idx] = TlbEntry {
|
||||
virtual_address: virt,
|
||||
physical_address: phys,
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub enum Interrupt {
|
||||
PageFault,
|
||||
ProtectionFault,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod interrupts;
|
||||
pub mod processor;
|
||||
pub mod state;
|
||||
@@ -0,0 +1,67 @@
|
||||
use std::{sync::Arc, thread, time::Duration};
|
||||
|
||||
use crate::{
|
||||
memory::{FaultInfo, MemoryMap, mainstore::MainStore, mmu::MMU},
|
||||
processor::{
|
||||
interrupts::Interrupt,
|
||||
state::{ProcessorSnapshot, SharedState},
|
||||
},
|
||||
};
|
||||
|
||||
pub struct Emulator {
|
||||
internal_state: ProcessorSnapshot,
|
||||
shared_state: Arc<SharedState>,
|
||||
|
||||
mmu: MMU,
|
||||
mainstore: MainStore,
|
||||
}
|
||||
|
||||
impl Emulator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
internal_state: ProcessorSnapshot::default(),
|
||||
shared_state: Arc::new(SharedState::new()),
|
||||
mmu: MMU::new(),
|
||||
mainstore: MainStore::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state_handle(&self) -> Arc<SharedState> {
|
||||
self.shared_state.clone()
|
||||
}
|
||||
|
||||
pub fn run(&mut self) {
|
||||
let mut clock = 0;
|
||||
|
||||
let regions = MemoryMap::default();
|
||||
|
||||
self.mmu.paging_enabled(true);
|
||||
MemoryMap::identity_map(&mut self.mmu, regions.regions.get(0).unwrap());
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_micros(1000));
|
||||
|
||||
match self.mmu.lookup(clock) {
|
||||
Ok(addr) => {
|
||||
let x = self.mainstore.read_word(addr);
|
||||
println!("{}", x);
|
||||
self.mainstore.write_word(addr, x + 1);
|
||||
}
|
||||
Err(FaultInfo::PageFault) => self.interrupt(Interrupt::PageFault),
|
||||
}
|
||||
|
||||
self.internal_state.gp_registers[0] += 10;
|
||||
|
||||
if clock % 1000 == 0 {
|
||||
self.shared_state
|
||||
.proc
|
||||
.store(Arc::new(self.internal_state.clone()));
|
||||
}
|
||||
clock += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn interrupt(&mut self, int: Interrupt) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicU8},
|
||||
};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
pub struct SharedState {
|
||||
pub proc: ArcSwap<ProcessorSnapshot>,
|
||||
|
||||
pub running: AtomicBool,
|
||||
pub interrupt: AtomicU8,
|
||||
}
|
||||
|
||||
impl SharedState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
proc: ArcSwap::new(Arc::new(ProcessorSnapshot::default())),
|
||||
|
||||
running: AtomicBool::new(true),
|
||||
interrupt: AtomicU8::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProcessorSnapshot {
|
||||
pub gp_registers: [u32; 16],
|
||||
}
|
||||
|
||||
impl Default for ProcessorSnapshot {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
gp_registers: [0; 16],
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user