feat(storage-server): implement ext4 read-only filesystem with superblock, inode, extent, and directory parsing
This commit is contained in:
343
crates/servers/storage-server/src/fs/ext4.rs
Normal file
343
crates/servers/storage-server/src/fs/ext4.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
use crate::block_device::{BlockDevice, SectorId};
|
||||
use crate::filesystem::{DirEntry, FileHandle, FileName, FileSystem};
|
||||
|
||||
const SECTOR_SIZE: usize = 512;
|
||||
const SUPERBLOCK_OFFSET: usize = 1024;
|
||||
const EXT4_MAGIC: u16 = 0xEF53;
|
||||
const ROOT_INODE: u32 = 2;
|
||||
const MAX_OPEN_FILES: usize = 8;
|
||||
const EXTENT_MAGIC: u16 = 0xF30A;
|
||||
|
||||
struct Superblock {
|
||||
inodes_count: u32,
|
||||
blocks_count: u32,
|
||||
block_size: usize,
|
||||
blocks_per_group: u32,
|
||||
inodes_per_group: u32,
|
||||
inode_size: u16,
|
||||
first_data_block: u32,
|
||||
}
|
||||
|
||||
impl Superblock {
|
||||
fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < 256 { return None; }
|
||||
let magic = u16::from_le_bytes([data[0x38], data[0x39]]);
|
||||
if magic != EXT4_MAGIC { return None; }
|
||||
|
||||
let log_block_size = u32::from_le_bytes([data[0x18], data[0x19], data[0x1A], data[0x1B]]);
|
||||
Some(Self {
|
||||
inodes_count: read_u32(data, 0x00),
|
||||
blocks_count: read_u32(data, 0x04),
|
||||
block_size: 1024 << log_block_size,
|
||||
blocks_per_group: read_u32(data, 0x20),
|
||||
inodes_per_group: read_u32(data, 0x28),
|
||||
inode_size: u16::from_le_bytes([data[0x58], data[0x59]]),
|
||||
first_data_block: read_u32(data, 0x14),
|
||||
})
|
||||
}
|
||||
|
||||
fn sectors_per_block(&self) -> usize {
|
||||
self.block_size / SECTOR_SIZE
|
||||
}
|
||||
|
||||
fn inode_block_group(&self, inode: u32) -> u32 {
|
||||
(inode - 1) / self.inodes_per_group
|
||||
}
|
||||
|
||||
fn inode_index_in_group(&self, inode: u32) -> u32 {
|
||||
(inode - 1) % self.inodes_per_group
|
||||
}
|
||||
}
|
||||
|
||||
struct Inode {
|
||||
mode: u16,
|
||||
size: u64,
|
||||
flags: u32,
|
||||
block_data: [u8; 60],
|
||||
}
|
||||
|
||||
impl Inode {
|
||||
fn is_dir(&self) -> bool { self.mode & 0xF000 == 0x4000 }
|
||||
fn is_file(&self) -> bool { self.mode & 0xF000 == 0x8000 }
|
||||
fn uses_extents(&self) -> bool { self.flags & 0x80000 != 0 }
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Extent {
|
||||
logical_block: u32,
|
||||
length: u16,
|
||||
physical_block: u64,
|
||||
}
|
||||
|
||||
impl Extent {
|
||||
fn parse(data: &[u8]) -> Self {
|
||||
let start_hi = u16::from_le_bytes([data[6], data[7]]) as u64;
|
||||
let start_lo = read_u32(data, 8) as u64;
|
||||
Self {
|
||||
logical_block: read_u32(data, 0),
|
||||
length: u16::from_le_bytes([data[4], data[5]]),
|
||||
physical_block: (start_hi << 32) | start_lo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct OpenFile {
|
||||
inode_num: u32,
|
||||
size: usize,
|
||||
offset: usize,
|
||||
name: FileName,
|
||||
first_extent: Extent,
|
||||
}
|
||||
|
||||
pub struct Ext4<B: BlockDevice> {
|
||||
device: B,
|
||||
sb: Superblock,
|
||||
gdt_block: u32,
|
||||
open_files: [Option<OpenFile>; MAX_OPEN_FILES],
|
||||
}
|
||||
|
||||
impl<B: BlockDevice> Ext4<B> {
|
||||
pub fn mount(mut device: B) -> Option<Self> {
|
||||
let mut buf = [0u8; 1024];
|
||||
// Superblock starts at byte 1024 = sector 2
|
||||
if !device.read_sector(SectorId::new(2), &mut buf[..512]) { return None; }
|
||||
if !device.read_sector(SectorId::new(3), &mut buf[512..]) { return None; }
|
||||
|
||||
let sb = Superblock::parse(&buf)?;
|
||||
let gdt_block = sb.first_data_block + 1;
|
||||
|
||||
Some(Self {
|
||||
device,
|
||||
sb,
|
||||
gdt_block,
|
||||
open_files: [const { None }; MAX_OPEN_FILES],
|
||||
})
|
||||
}
|
||||
|
||||
fn read_block(&mut self, block: u64, buf: &mut [u8]) -> bool {
|
||||
let start_sector = (block as usize) * self.sb.sectors_per_block();
|
||||
let spb = self.sb.sectors_per_block();
|
||||
for i in 0..spb {
|
||||
let offset = i * SECTOR_SIZE;
|
||||
if offset + SECTOR_SIZE > buf.len() { break; }
|
||||
if !self.device.read_sector(
|
||||
SectorId::new((start_sector + i) as u32),
|
||||
&mut buf[offset..offset + SECTOR_SIZE],
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn read_inode(&mut self, inode_num: u32) -> Option<Inode> {
|
||||
let bg = self.sb.inode_block_group(inode_num);
|
||||
let index = self.sb.inode_index_in_group(inode_num);
|
||||
|
||||
// Read block group descriptor
|
||||
let mut gdt_buf = [0u8; 4096];
|
||||
self.read_block(self.gdt_block as u64, &mut gdt_buf);
|
||||
let gd_offset = (bg as usize) * 32;
|
||||
let inode_table_block = read_u32(&gdt_buf, gd_offset + 8);
|
||||
|
||||
// Calculate inode position
|
||||
let inode_offset = (index as usize) * (self.sb.inode_size as usize);
|
||||
let block_offset = inode_offset / self.sb.block_size;
|
||||
let offset_in_block = inode_offset % self.sb.block_size;
|
||||
|
||||
let mut block_buf = [0u8; 4096];
|
||||
self.read_block((inode_table_block + block_offset as u32) as u64, &mut block_buf);
|
||||
|
||||
let d = &block_buf[offset_in_block..];
|
||||
if d.len() < 128 { return None; }
|
||||
|
||||
let size_lo = read_u32(d, 0x04) as u64;
|
||||
let size_hi = read_u32(d, 0x6C) as u64;
|
||||
let size = size_lo | (size_hi << 32);
|
||||
|
||||
let mut block_data = [0u8; 60];
|
||||
block_data.copy_from_slice(&d[0x28..0x64]);
|
||||
|
||||
Some(Inode {
|
||||
mode: u16::from_le_bytes([d[0x00], d[0x01]]),
|
||||
size,
|
||||
flags: read_u32(d, 0x20),
|
||||
block_data,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_extents(&self, inode: &Inode) -> Option<Extent> {
|
||||
if !inode.uses_extents() { return None; }
|
||||
let hdr = &inode.block_data;
|
||||
let magic = u16::from_le_bytes([hdr[0], hdr[1]]);
|
||||
if magic != EXTENT_MAGIC { return None; }
|
||||
let entries = u16::from_le_bytes([hdr[2], hdr[3]]);
|
||||
let depth = u16::from_le_bytes([hdr[6], hdr[7]]);
|
||||
if depth != 0 || entries == 0 { return None; }
|
||||
Some(Extent::parse(&hdr[12..24]))
|
||||
}
|
||||
|
||||
fn find_in_dir(&mut self, dir_inode: u32, name: &[u8]) -> Option<u32> {
|
||||
let inode = self.read_inode(dir_inode)?;
|
||||
if !inode.is_dir() { return None; }
|
||||
|
||||
let extent = self.read_extents(&inode)?;
|
||||
let mut block_buf = [0u8; 4096];
|
||||
|
||||
for blk in 0..extent.length as u64 {
|
||||
self.read_block(extent.physical_block + blk, &mut block_buf);
|
||||
let mut pos = 0;
|
||||
while pos + 8 <= self.sb.block_size {
|
||||
let entry_inode = read_u32(&block_buf, pos);
|
||||
let rec_len = u16::from_le_bytes([block_buf[pos + 4], block_buf[pos + 5]]) as usize;
|
||||
let name_len = block_buf[pos + 6] as usize;
|
||||
|
||||
if rec_len == 0 { break; }
|
||||
if entry_inode != 0 && name_len == name.len() {
|
||||
if &block_buf[pos + 8..pos + 8 + name_len] == name {
|
||||
return Some(entry_inode);
|
||||
}
|
||||
}
|
||||
pos += rec_len;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn resolve_path(&mut self, path: &[u8]) -> Option<u32> {
|
||||
let mut current = ROOT_INODE;
|
||||
for component in path.split(|&b| b == b'/') {
|
||||
if component.is_empty() { continue; }
|
||||
current = self.find_in_dir(current, component)?;
|
||||
}
|
||||
Some(current)
|
||||
}
|
||||
|
||||
fn alloc_handle(&self) -> Option<FileHandle> {
|
||||
for i in 0..MAX_OPEN_FILES {
|
||||
if self.open_files[i].is_none() {
|
||||
return Some(FileHandle::new(i as u16));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockDevice> FileSystem for Ext4<B> {
|
||||
fn open(&mut self, path: &[u8]) -> Option<FileHandle> {
|
||||
let inode_num = self.resolve_path(path)?;
|
||||
let inode = self.read_inode(inode_num)?;
|
||||
if !inode.is_file() { return None; }
|
||||
|
||||
let extent = self.read_extents(&inode)?;
|
||||
let handle = self.alloc_handle()?;
|
||||
self.open_files[handle.raw() as usize] = Some(OpenFile {
|
||||
inode_num,
|
||||
size: inode.size as usize,
|
||||
offset: 0,
|
||||
name: FileName::from_bytes(path),
|
||||
first_extent: extent,
|
||||
});
|
||||
Some(handle)
|
||||
}
|
||||
|
||||
fn read(&mut self, handle: FileHandle, buf: &mut [u8]) -> Option<usize> {
|
||||
let file = *self.open_files[handle.raw() as usize].as_ref()?;
|
||||
if file.offset >= file.size { return Some(0); }
|
||||
|
||||
let remaining = file.size - file.offset;
|
||||
let to_read = buf.len().min(remaining);
|
||||
let bs = self.sb.block_size;
|
||||
|
||||
let block_in_extent = file.offset / bs;
|
||||
let byte_in_block = file.offset % bs;
|
||||
|
||||
let mut block_buf = [0u8; 4096];
|
||||
let phys_block = file.first_extent.physical_block + block_in_extent as u64;
|
||||
self.read_block(phys_block, &mut block_buf);
|
||||
|
||||
let available = bs - byte_in_block;
|
||||
let chunk = to_read.min(available);
|
||||
buf[..chunk].copy_from_slice(&block_buf[byte_in_block..byte_in_block + chunk]);
|
||||
|
||||
self.open_files[handle.raw() as usize].as_mut().map(|f| f.offset += chunk);
|
||||
Some(chunk)
|
||||
}
|
||||
|
||||
fn write(&mut self, _handle: FileHandle, _buf: &[u8]) -> Option<usize> {
|
||||
// Write requires block allocation + inode update + extent modification
|
||||
// Deferred: needs block bitmap management
|
||||
None
|
||||
}
|
||||
|
||||
fn close(&mut self, handle: FileHandle) {
|
||||
self.open_files[handle.raw() as usize] = None;
|
||||
}
|
||||
|
||||
fn list_dir(&mut self, path: &[u8], entries: &mut [DirEntry]) -> Option<usize> {
|
||||
let dir_inode_num = if path.is_empty() || path == b"/" {
|
||||
ROOT_INODE
|
||||
} else {
|
||||
self.resolve_path(path)?
|
||||
};
|
||||
|
||||
let inode = self.read_inode(dir_inode_num)?;
|
||||
if !inode.is_dir() { return None; }
|
||||
|
||||
let extent = self.read_extents(&inode)?;
|
||||
let mut block_buf = [0u8; 4096];
|
||||
let mut count = 0;
|
||||
|
||||
for blk in 0..extent.length as u64 {
|
||||
self.read_block(extent.physical_block + blk, &mut block_buf);
|
||||
let mut pos = 0;
|
||||
while pos + 8 <= self.sb.block_size && count < entries.len() {
|
||||
let entry_inode = read_u32(&block_buf, pos);
|
||||
let rec_len = u16::from_le_bytes([block_buf[pos + 4], block_buf[pos + 5]]) as usize;
|
||||
let name_len = block_buf[pos + 6] as usize;
|
||||
let file_type = block_buf[pos + 7];
|
||||
|
||||
if rec_len == 0 { break; }
|
||||
if entry_inode != 0 && name_len > 0 {
|
||||
let name = &block_buf[pos + 8..pos + 8 + name_len];
|
||||
if name != b"." && name != b".." {
|
||||
let child_size = if file_type == 1 {
|
||||
self.read_inode(entry_inode)
|
||||
.map(|i| i.size as usize)
|
||||
.unwrap_or(0)
|
||||
} else { 0 };
|
||||
|
||||
entries[count] = DirEntry {
|
||||
name: FileName::from_bytes(name),
|
||||
size: child_size,
|
||||
is_dir: file_type == 2,
|
||||
};
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
pos += rec_len;
|
||||
}
|
||||
}
|
||||
Some(count)
|
||||
}
|
||||
|
||||
fn create(&mut self, _path: &[u8]) -> Option<FileHandle> {
|
||||
// Create requires inode allocation + directory entry insertion
|
||||
// Deferred: needs inode bitmap management
|
||||
None
|
||||
}
|
||||
|
||||
fn delete(&mut self, _path: &[u8]) -> bool {
|
||||
// Delete requires inode deallocation + directory entry removal
|
||||
false
|
||||
}
|
||||
|
||||
fn file_size(&mut self, handle: FileHandle) -> Option<usize> {
|
||||
self.open_files[handle.raw() as usize].as_ref().map(|f| f.size)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_u32(data: &[u8], offset: usize) -> u32 {
|
||||
u32::from_le_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]])
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod ext4;
|
||||
pub mod fat16;
|
||||
|
||||
166
crates/servers/storage-server/src/tests/ext4.rs
Normal file
166
crates/servers/storage-server/src/tests/ext4.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use crate::adapters::mock_block::MockBlockDevice;
|
||||
use crate::block_device::{BlockDevice, SectorId};
|
||||
use crate::filesystem::{DirEntry, FileSystem};
|
||||
use crate::fs::ext4::Ext4;
|
||||
|
||||
fn write_u32(buf: &mut [u8], offset: usize, val: u32) {
|
||||
buf[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
|
||||
}
|
||||
|
||||
fn write_u16(buf: &mut [u8], offset: usize, val: u16) {
|
||||
buf[offset..offset + 2].copy_from_slice(&val.to_le_bytes());
|
||||
}
|
||||
|
||||
fn format_ext4_disk() -> MockBlockDevice {
|
||||
let mut dev = MockBlockDevice::new(64);
|
||||
let mut buf = [0u8; 512];
|
||||
|
||||
// Superblock at sectors 2-3 (byte offset 1024)
|
||||
// Sector 2: first 512 bytes of superblock
|
||||
write_u32(&mut buf, 0x00, 32); // s_inodes_count
|
||||
write_u32(&mut buf, 0x04, 64); // s_blocks_count
|
||||
write_u32(&mut buf, 0x14, 1); // s_first_data_block = 1 (1K blocks would, but we use 0 for 4K)
|
||||
write_u32(&mut buf, 0x18, 2); // s_log_block_size = 2 → 1024 << 2 = 4096
|
||||
write_u32(&mut buf, 0x20, 64); // s_blocks_per_group
|
||||
write_u32(&mut buf, 0x28, 32); // s_inodes_per_group
|
||||
write_u16(&mut buf, 0x38, 0xEF53); // s_magic
|
||||
write_u16(&mut buf, 0x58, 128); // s_inode_size
|
||||
write_u32(&mut buf, 0x14, 0); // s_first_data_block = 0 for 4K blocks
|
||||
dev.write_sector(SectorId::new(2), &buf);
|
||||
|
||||
// Block group descriptor at block 1 (sectors 8-15 for 4K blocks)
|
||||
let mut gdt = [0u8; 512];
|
||||
write_u32(&mut gdt, 8, 3); // bg_inode_table = block 3
|
||||
dev.write_sector(SectorId::new(8), &gdt);
|
||||
|
||||
// Inode table at block 3 (sectors 24-31)
|
||||
// Root inode (inode 2) at offset 128 in the table
|
||||
let mut inode_sector = [0u8; 512];
|
||||
|
||||
// Inode 2 (root dir) at byte offset 128 = still in first sector
|
||||
let ioff = 128; // inode 2 index = 1, * 128 bytes = 128
|
||||
write_u16(&mut inode_sector, ioff + 0x00, 0x4000 | 0o755); // mode = directory
|
||||
write_u32(&mut inode_sector, ioff + 0x04, 4096); // size_lo
|
||||
write_u32(&mut inode_sector, ioff + 0x20, 0x80000); // flags = uses extents
|
||||
|
||||
// Extent header at inode+0x28
|
||||
let eh = ioff + 0x28;
|
||||
write_u16(&mut inode_sector, eh + 0, 0xF30A); // magic
|
||||
write_u16(&mut inode_sector, eh + 2, 1); // entries
|
||||
write_u16(&mut inode_sector, eh + 4, 4); // max
|
||||
write_u16(&mut inode_sector, eh + 6, 0); // depth
|
||||
|
||||
// Extent leaf at inode+0x28+12
|
||||
let ext = eh + 12;
|
||||
write_u32(&mut inode_sector, ext + 0, 0); // logical block
|
||||
write_u16(&mut inode_sector, ext + 4, 1); // length
|
||||
write_u16(&mut inode_sector, ext + 6, 0); // physical hi
|
||||
write_u32(&mut inode_sector, ext + 8, 4); // physical lo → block 4
|
||||
|
||||
// Inode 3 (a regular file "hello") at byte offset 256
|
||||
let ioff2 = 256;
|
||||
write_u16(&mut inode_sector, ioff2 + 0x00, 0x8000 | 0o644); // mode = regular file
|
||||
write_u32(&mut inode_sector, ioff2 + 0x04, 13); // size = 13 bytes
|
||||
write_u32(&mut inode_sector, ioff2 + 0x20, 0x80000); // flags = extents
|
||||
|
||||
let eh2 = ioff2 + 0x28;
|
||||
write_u16(&mut inode_sector, eh2 + 0, 0xF30A);
|
||||
write_u16(&mut inode_sector, eh2 + 2, 1);
|
||||
write_u16(&mut inode_sector, eh2 + 4, 4);
|
||||
write_u16(&mut inode_sector, eh2 + 6, 0);
|
||||
let ext2 = eh2 + 12;
|
||||
write_u32(&mut inode_sector, ext2 + 0, 0);
|
||||
write_u16(&mut inode_sector, ext2 + 4, 1);
|
||||
write_u16(&mut inode_sector, ext2 + 6, 0);
|
||||
write_u32(&mut inode_sector, ext2 + 8, 5); // data at block 5
|
||||
|
||||
dev.write_sector(SectorId::new(24), &inode_sector);
|
||||
|
||||
// Root directory at block 4 (sectors 32-39)
|
||||
let mut dir = [0u8; 512];
|
||||
let mut pos = 0;
|
||||
|
||||
// Entry: "." → inode 2
|
||||
write_u32(&mut dir, pos + 0, 2);
|
||||
write_u16(&mut dir, pos + 4, 12); // rec_len
|
||||
dir[pos + 6] = 1; // name_len
|
||||
dir[pos + 7] = 2; // file_type = dir
|
||||
dir[pos + 8] = b'.';
|
||||
pos += 12;
|
||||
|
||||
// Entry: ".." → inode 2
|
||||
write_u32(&mut dir, pos + 0, 2);
|
||||
write_u16(&mut dir, pos + 4, 12);
|
||||
dir[pos + 6] = 2;
|
||||
dir[pos + 7] = 2;
|
||||
dir[pos + 8] = b'.';
|
||||
dir[pos + 9] = b'.';
|
||||
pos += 12;
|
||||
|
||||
// Entry: "hello" → inode 3
|
||||
write_u32(&mut dir, pos + 0, 3);
|
||||
write_u16(&mut dir, pos + 4, 488); // rec_len = rest of block
|
||||
dir[pos + 6] = 5; // name_len
|
||||
dir[pos + 7] = 1; // file_type = regular
|
||||
dir[pos + 8..pos + 13].copy_from_slice(b"hello");
|
||||
|
||||
dev.write_sector(SectorId::new(32), &dir);
|
||||
|
||||
// File data at block 5 (sectors 40-47)
|
||||
let mut data = [0u8; 512];
|
||||
data[..13].copy_from_slice(b"Hello, ext4!\n");
|
||||
dev.write_sector(SectorId::new(40), &data);
|
||||
|
||||
dev
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mount_valid_ext4() {
|
||||
let dev = format_ext4_disk();
|
||||
assert!(Ext4::mount(dev).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mount_invalid_disk() {
|
||||
let dev = MockBlockDevice::new(64);
|
||||
assert!(Ext4::mount(dev).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_root_directory() {
|
||||
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
|
||||
let mut entries = [DirEntry::empty(); 8];
|
||||
let count = fs.list_dir(b"/", &mut entries).unwrap();
|
||||
assert_eq!(count, 1); // "hello" (. and .. filtered)
|
||||
assert_eq!(entries[0].name.as_bytes(), b"hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_and_read_file() {
|
||||
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
|
||||
let h = fs.open(b"hello").unwrap();
|
||||
assert_eq!(fs.file_size(h), Some(13));
|
||||
|
||||
let mut buf = [0u8; 32];
|
||||
let read = fs.read(h, &mut buf).unwrap();
|
||||
assert_eq!(read, 13);
|
||||
assert_eq!(&buf[..13], b"Hello, ext4!\n");
|
||||
fs.close(h);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_nonexistent_returns_none() {
|
||||
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
|
||||
assert!(fs.open(b"nope").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_past_eof_returns_zero() {
|
||||
let mut fs = Ext4::mount(format_ext4_disk()).unwrap();
|
||||
let h = fs.open(b"hello").unwrap();
|
||||
let mut buf = [0u8; 32];
|
||||
fs.read(h, &mut buf).unwrap(); // read all 13 bytes
|
||||
let read = fs.read(h, &mut buf).unwrap();
|
||||
assert_eq!(read, 0);
|
||||
fs.close(h);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod ext4;
|
||||
mod fat16;
|
||||
mod protocol;
|
||||
mod server;
|
||||
|
||||
Reference in New Issue
Block a user