mirror of
https://github.com/uutils/diffutils.git
synced 2026-06-10 15:48:59 -07:00
Merge pull request #33 from TanmayPatil105/context-diff-modification-time
Display modification times of input files in context and unified diff
This commit is contained in:
Generated
+157
-2
@@ -11,6 +11,21 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.6"
|
||||
@@ -55,12 +70,44 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@@ -78,6 +125,7 @@ name = "diffutils"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"assert_cmd",
|
||||
"chrono",
|
||||
"diff",
|
||||
"predicates",
|
||||
"pretty_assertions",
|
||||
@@ -118,6 +166,38 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.153"
|
||||
@@ -130,6 +210,12 @@ version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.1"
|
||||
@@ -151,6 +237,12 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "3.1.0"
|
||||
@@ -211,9 +303,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.3"
|
||||
version = "1.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
|
||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -330,6 +422,60 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -361,6 +507,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
|
||||
@@ -15,6 +15,7 @@ name = "diffutils"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.35"
|
||||
diff = "0.1.10"
|
||||
regex = "1.10.3"
|
||||
same-file = "1.0.6"
|
||||
|
||||
+16
-7
@@ -8,6 +8,7 @@ use std::io::Write;
|
||||
|
||||
use crate::params::Params;
|
||||
use crate::utils::do_write_line;
|
||||
use crate::utils::get_modification_time;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DiffLine {
|
||||
@@ -267,10 +268,14 @@ fn make_diff(
|
||||
|
||||
#[must_use]
|
||||
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
|
||||
let from_modified_time = get_modification_time(¶ms.from.to_string_lossy());
|
||||
let to_modified_time = get_modification_time(¶ms.to.to_string_lossy());
|
||||
let mut output = format!(
|
||||
"*** {0}\t\n--- {1}\t\n",
|
||||
"*** {0}\t{1}\n--- {2}\t{3}\n",
|
||||
params.from.to_string_lossy(),
|
||||
params.to.to_string_lossy()
|
||||
from_modified_time,
|
||||
params.to.to_string_lossy(),
|
||||
to_modified_time
|
||||
)
|
||||
.into_bytes();
|
||||
let diff_results = make_diff(expected, actual, params.context_count, params.brief);
|
||||
@@ -717,6 +722,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_stop_early() {
|
||||
use crate::assert_diff_eq;
|
||||
|
||||
let from_filename = "foo";
|
||||
let from = ["a", "b", "c", ""].join("\n");
|
||||
let to_filename = "bar";
|
||||
@@ -731,9 +738,10 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let expected_full = [
|
||||
"*** foo\t",
|
||||
"--- bar\t",
|
||||
"*** foo\tTIMESTAMP",
|
||||
"--- bar\tTIMESTAMP",
|
||||
"***************",
|
||||
"*** 1,3 ****",
|
||||
" a",
|
||||
@@ -746,7 +754,7 @@ mod tests {
|
||||
"",
|
||||
]
|
||||
.join("\n");
|
||||
assert_eq!(diff_full, expected_full.as_bytes());
|
||||
assert_diff_eq!(diff_full, expected_full);
|
||||
|
||||
let diff_brief = diff(
|
||||
from.as_bytes(),
|
||||
@@ -758,8 +766,9 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let expected_brief = ["*** foo\t", "--- bar\t", ""].join("\n");
|
||||
assert_eq!(diff_brief, expected_brief.as_bytes());
|
||||
|
||||
let expected_brief = ["*** foo\tTIMESTAMP", "--- bar\tTIMESTAMP", ""].join("\n");
|
||||
assert_diff_eq!(diff_brief, expected_brief);
|
||||
|
||||
let nodiff_full = diff(
|
||||
from.as_bytes(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod context_diff;
|
||||
pub mod ed_diff;
|
||||
pub mod macros;
|
||||
pub mod normal_diff;
|
||||
pub mod params;
|
||||
pub mod unified_diff;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// asserts equality of the actual diff and expected diff
|
||||
// considering datetime varitations
|
||||
//
|
||||
// It replaces the modification time in the actual diff
|
||||
// with placeholer "TIMESTAMP" and then asserts the equality
|
||||
//
|
||||
// For eg.
|
||||
// let brief = "*** fruits_old.txt\t2024-03-24 23:43:05.189597645 +0530\n
|
||||
// --- fruits_new.txt\t2024-03-24 23:35:08.922581904 +0530\n";
|
||||
//
|
||||
// replaced = "*** fruits_old.txt\tTIMESTAMP\n
|
||||
// --- fruits_new.txt\tTIMESTAMP\n";
|
||||
#[macro_export]
|
||||
macro_rules! assert_diff_eq {
|
||||
($actual:expr, $expected:expr) => {{
|
||||
use regex::Regex;
|
||||
use std::str;
|
||||
|
||||
let diff = str::from_utf8(&$actual).unwrap();
|
||||
let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap();
|
||||
let actual = re.replacen(diff, 2, "TIMESTAMP");
|
||||
|
||||
assert_eq!(actual, $expected);
|
||||
}};
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use std::process::{exit, ExitCode};
|
||||
|
||||
mod context_diff;
|
||||
mod ed_diff;
|
||||
mod macros;
|
||||
mod normal_diff;
|
||||
mod params;
|
||||
mod unified_diff;
|
||||
|
||||
+16
-7
@@ -8,6 +8,7 @@ use std::io::Write;
|
||||
|
||||
use crate::params::Params;
|
||||
use crate::utils::do_write_line;
|
||||
use crate::utils::get_modification_time;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DiffLine {
|
||||
@@ -238,10 +239,14 @@ fn make_diff(
|
||||
|
||||
#[must_use]
|
||||
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
|
||||
let from_modified_time = get_modification_time(¶ms.from.to_string_lossy());
|
||||
let to_modified_time = get_modification_time(¶ms.to.to_string_lossy());
|
||||
let mut output = format!(
|
||||
"--- {0}\t\n+++ {1}\t\n",
|
||||
"--- {0}\t{1}\n+++ {2}\t{3}\n",
|
||||
params.from.to_string_lossy(),
|
||||
params.to.to_string_lossy()
|
||||
from_modified_time,
|
||||
params.to.to_string_lossy(),
|
||||
to_modified_time
|
||||
)
|
||||
.into_bytes();
|
||||
let diff_results = make_diff(expected, actual, params.context_count, params.brief);
|
||||
@@ -870,6 +875,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_stop_early() {
|
||||
use crate::assert_diff_eq;
|
||||
|
||||
let from_filename = "foo";
|
||||
let from = ["a", "b", "c", ""].join("\n");
|
||||
let to_filename = "bar";
|
||||
@@ -884,9 +891,10 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let expected_full = [
|
||||
"--- foo\t",
|
||||
"+++ bar\t",
|
||||
"--- foo\tTIMESTAMP",
|
||||
"+++ bar\tTIMESTAMP",
|
||||
"@@ -1,3 +1,3 @@",
|
||||
" a",
|
||||
"-b",
|
||||
@@ -895,7 +903,7 @@ mod tests {
|
||||
"",
|
||||
]
|
||||
.join("\n");
|
||||
assert_eq!(diff_full, expected_full.as_bytes());
|
||||
assert_diff_eq!(diff_full, expected_full);
|
||||
|
||||
let diff_brief = diff(
|
||||
from.as_bytes(),
|
||||
@@ -907,8 +915,9 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let expected_brief = ["--- foo\t", "+++ bar\t", ""].join("\n");
|
||||
assert_eq!(diff_brief, expected_brief.as_bytes());
|
||||
|
||||
let expected_brief = ["--- foo\tTIMESTAMP", "+++ bar\tTIMESTAMP", ""].join("\n");
|
||||
assert_diff_eq!(diff_brief, expected_brief);
|
||||
|
||||
let nodiff_full = diff(
|
||||
from.as_bytes(),
|
||||
|
||||
@@ -52,6 +52,25 @@ pub fn do_write_line(
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the modification time of the input file specified by file path
|
||||
/// If an error occurs, it returns the current system time
|
||||
pub fn get_modification_time(file_path: &str) -> String {
|
||||
use chrono::{DateTime, Local};
|
||||
use std::fs;
|
||||
use std::time::SystemTime;
|
||||
|
||||
let modification_time: SystemTime = fs::metadata(file_path)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(SystemTime::now());
|
||||
|
||||
let modification_time: DateTime<Local> = modification_time.into();
|
||||
let modification_time: String = modification_time
|
||||
.format("%Y-%m-%d %H:%M:%S%.9f %z")
|
||||
.to_string();
|
||||
|
||||
modification_time
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -115,4 +134,46 @@ mod tests {
|
||||
assert_line_written("foo bar\tbaz", true, 8, "foo bar baz");
|
||||
}
|
||||
}
|
||||
|
||||
mod modification_time {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn set_time() {
|
||||
use chrono::{DateTime, Local};
|
||||
use std::time::SystemTime;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
let temp = NamedTempFile::new().unwrap();
|
||||
// set file modification time equal to current time
|
||||
let current = SystemTime::now();
|
||||
let _ = temp.as_file().set_modified(current);
|
||||
|
||||
// format current time
|
||||
let current: DateTime<Local> = current.into();
|
||||
let current: String = current.format("%Y-%m-%d %H:%M:%S%.9f %z").to_string();
|
||||
|
||||
// verify
|
||||
assert_eq!(
|
||||
current,
|
||||
get_modification_time(&temp.path().to_string_lossy())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_file() {
|
||||
use chrono::{DateTime, Local};
|
||||
use std::time::SystemTime;
|
||||
|
||||
let invalid_file = "target/utils/invalid-file";
|
||||
|
||||
// store current time before calling `get_modification_time`
|
||||
// Because the file is invalid, it will return SystemTime::now()
|
||||
// which will be greater than previously saved time
|
||||
let current_time: DateTime<Local> = SystemTime::now().into();
|
||||
let m_time: DateTime<Local> = get_modification_time(invalid_file).parse().unwrap();
|
||||
|
||||
assert!(m_time > current_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-18
@@ -4,6 +4,7 @@
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use assert_cmd::cmd::Command;
|
||||
use diffutilslib::assert_diff_eq;
|
||||
use predicates::prelude::*;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
@@ -178,26 +179,32 @@ fn read_from_stdin() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg(file1.path())
|
||||
.arg("-")
|
||||
.write_stdin("bar\n");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stdout(predicate::eq(format!(
|
||||
"--- {}\t\n+++ -\t\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||
cmd.assert().code(predicate::eq(1)).failure();
|
||||
|
||||
let output = cmd.output().unwrap().stdout;
|
||||
assert_diff_eq!(
|
||||
output,
|
||||
format!(
|
||||
"--- {}\tTIMESTAMP\n+++ -\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||
file1.path().to_string_lossy()
|
||||
)));
|
||||
)
|
||||
);
|
||||
|
||||
let mut cmd = Command::cargo_bin("diffutils")?;
|
||||
cmd.arg("-u")
|
||||
.arg("-")
|
||||
.arg(file2.path())
|
||||
.write_stdin("foo\n");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stdout(predicate::eq(format!(
|
||||
"--- -\t\n+++ {}\t\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||
cmd.assert().code(predicate::eq(1)).failure();
|
||||
|
||||
let output = cmd.output().unwrap().stdout;
|
||||
assert_diff_eq!(
|
||||
output,
|
||||
format!(
|
||||
"--- -\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||
file2.path().to_string_lossy()
|
||||
)));
|
||||
)
|
||||
);
|
||||
|
||||
let mut cmd = Command::cargo_bin("diffutils")?;
|
||||
cmd.arg("-u").arg("-").arg("-");
|
||||
@@ -213,13 +220,16 @@ fn read_from_stdin() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg(file1.path())
|
||||
.arg("/dev/stdin")
|
||||
.write_stdin("bar\n");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stdout(predicate::eq(format!(
|
||||
"--- {}\t\n+++ /dev/stdin\t\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||
cmd.assert().code(predicate::eq(1)).failure();
|
||||
|
||||
let output = cmd.output().unwrap().stdout;
|
||||
assert_diff_eq!(
|
||||
output,
|
||||
format!(
|
||||
"--- {}\tTIMESTAMP\n+++ /dev/stdin\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||
file1.path().to_string_lossy()
|
||||
)));
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user