use std::{
collections::HashMap,
fs::{self, File},
io::{self, BufRead, BufReader},
path::{Path, PathBuf},
str::FromStr,
};
use unity_version::RevisionHash;
use unity_version::Version;
use unity_version::CompleteVersion;
#[derive(Default)]
pub struct DetectOptions {
pub recursive: bool,
pub max_depth: u32,
pub case_sensitive: bool,
}
impl DetectOptions {
pub fn new() -> Self {
Self {
recursive: false,
max_depth: u32::MAX,
case_sensitive: true,
}
}
pub fn recursive(&mut self, recursive: bool) -> &mut Self {
self.recursive = recursive;
self
}
pub fn max_depth(&mut self, max_depth: u32) -> &mut Self {
self.max_depth = max_depth;
self
}
pub fn case_sensitive(&mut self, case_sensitive: bool) -> &mut Self {
self.case_sensitive = case_sensitive;
self
}
pub fn detect_project_version(&self, dir: &Path) -> io::Result<Version> {
let v = self.detect_project_version_str(dir)?;
Version::from_str(&v)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Can't parse Unity version"))
}
pub fn detect_project_version_str(&self, dir: &Path) -> io::Result<String> {
let project_version = self.detect_unity_project_dir(dir).and_then(|p| {
self.try_get_project_version(p).ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "ProjectVersion.txt not found")
})
})?;
let file = File::open(project_version)?;
let lines = BufReader::new(file).lines();
let mut editor_versions: HashMap<&'static str, String> = HashMap::with_capacity(2);
for line in lines {
if let Ok(line) = line {
if line.starts_with("m_EditorVersion: ") {
let value = line.replace("m_EditorVersion: ", "");
editor_versions.insert("EditorVersion", value.to_owned());
}
if line.starts_with("m_EditorVersionWithRevision: ") {
let value = line.replace("m_EditorVersionWithRevision: ", "");
editor_versions.insert("EditorVersionWithRevision", value.to_owned());
}
}
}
let v = editor_versions
.get("EditorVersionWithRevision")
.or_else(|| editor_versions.get("EditorVersion"))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Can't parse Unity version")
})?;
Ok(v.to_owned())
}
pub fn detect_project_version_revision_hash(&self, dir: &Path) -> io::Result<RevisionHash> {
let complete_version = self.detect_project_complete_version(dir)?;
Ok(complete_version.revision().clone())
}
pub fn detect_project_complete_version(&self, dir: &Path) -> io::Result<CompleteVersion> {
let version_str = self.detect_project_version_str(dir)?;
CompleteVersion::from_str(&version_str)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))
}
pub fn detect_unity_project_dir(&self, dir: &Path) -> io::Result<PathBuf> {
self.detect_unity_project_dir_with_depth(dir, 0)
}
fn detect_unity_project_dir_with_depth(&self, dir: &Path, current_depth: u32) -> io::Result<PathBuf> {
let error = Err(io::Error::new(
io::ErrorKind::NotFound,
"Unable to find a Unity project",
));
if !dir.is_dir() {
return error;
}
if self.try_get_project_version(dir).is_some() {
return Ok(dir.to_path_buf());
}
if !self.recursive || current_depth >= self.max_depth {
return error;
}
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let result = self.detect_unity_project_dir_with_depth(&path, current_depth + 1);
if result.is_ok() {
return result;
}
}
}
error
}
fn try_get_project_version<P: AsRef<Path>>(&self, base_dir: P) -> Option<PathBuf> {
let project_version = base_dir
.as_ref()
.join("ProjectSettings")
.join("ProjectVersion.txt");
if project_version.exists() {
Some(project_version)
} else {
None
}
}
}
pub fn detect_unity_project_dir(dir: &Path) -> io::Result<PathBuf> {
DetectOptions::new().detect_unity_project_dir(dir)
}
pub fn detect_project_version(project_path: &Path) -> io::Result<Version> {
DetectOptions::new().detect_project_version(project_path)
}
pub fn detect_project_version_revision_hash(project_path: &Path) -> io::Result<RevisionHash> {
DetectOptions::new().detect_project_version_revision_hash(project_path)
}
pub fn detect_project_complete_version(project_path: &Path) -> io::Result<CompleteVersion> {
DetectOptions::new().detect_project_complete_version(project_path)
}
pub fn try_get_project_version<P: AsRef<Path>>(base_dir: P) -> Option<PathBuf> {
DetectOptions::new().try_get_project_version(base_dir)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn create_unity_project(base_dir: &Path, version_content: &str) -> std::io::Result<()> {
let project_settings = base_dir.join("ProjectSettings");
fs::create_dir_all(&project_settings)?;
let version_file = project_settings.join("ProjectVersion.txt");
fs::write(version_file, version_content)?;
Ok(())
}
#[test]
fn test_try_get_project_version_valid_project() {
let temp_dir = TempDir::new().unwrap();
create_unity_project(temp_dir.path(), "m_EditorVersion: 2021.3.16f1").unwrap();
let result = try_get_project_version(temp_dir.path());
assert!(result.is_some());
let path = result.unwrap();
assert!(path.ends_with("ProjectSettings/ProjectVersion.txt"));
assert!(path.exists());
}
#[test]
fn test_try_get_project_version_no_project() {
let temp_dir = TempDir::new().unwrap();
let result = try_get_project_version(temp_dir.path());
assert!(result.is_none());
}
#[test]
fn test_try_get_project_version_missing_project_settings() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir(temp_dir.path().join("Assets")).unwrap();
let result = try_get_project_version(temp_dir.path());
assert!(result.is_none());
}
#[test]
fn test_detect_unity_project_dir_current_directory() {
let temp_dir = TempDir::new().unwrap();
create_unity_project(temp_dir.path(), "m_EditorVersion: 2021.3.16f1").unwrap();
let result = detect_unity_project_dir(temp_dir.path());
assert!(result.is_ok());
assert_eq!(result.unwrap(), temp_dir.path());
}
#[test]
fn test_detect_unity_project_dir_not_found_no_recursion() {
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("subproject");
fs::create_dir(&subdir).unwrap();
create_unity_project(&subdir, "m_EditorVersion: 2021.3.16f1").unwrap();
let result = detect_unity_project_dir(temp_dir.path());
assert!(result.is_err());
}
#[test]
fn test_detect_unity_project_dir_recursive_search() {
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("subproject");
fs::create_dir(&subdir).unwrap();
create_unity_project(&subdir, "m_EditorVersion: 2021.3.16f1").unwrap();
let result = DetectOptions::new().recursive(true).detect_unity_project_dir(temp_dir.path());
assert!(result.is_ok());
assert_eq!(result.unwrap(), subdir);
}
#[test]
fn test_detect_unity_project_dir_nested_recursive() {
let temp_dir = TempDir::new().unwrap();
let nested_path = temp_dir
.path()
.join("level1")
.join("level2")
.join("unity_project");
fs::create_dir_all(&nested_path).unwrap();
create_unity_project(&nested_path, "m_EditorVersion: 2021.3.16f1").unwrap();
let result = DetectOptions::new().recursive(true).detect_unity_project_dir(temp_dir.path());
assert!(result.is_ok());
assert_eq!(result.unwrap(), nested_path);
}
#[test]
fn test_detect_project_version_with_editor_version() {
let temp_dir = TempDir::new().unwrap();
let version_content =
"m_EditorVersion: 2021.3.16f1\nm_EditorVersionWithRevision: 2021.3.16f1 (4016570cf34f)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = detect_project_version(temp_dir.path());
assert!(result.is_ok());
let version = result.unwrap();
assert_eq!(version.to_string(), "2021.3.16f1");
}
#[test]
fn test_detect_project_version_with_revision() {
let temp_dir = TempDir::new().unwrap();
let version_content =
"m_EditorVersion: 2020.3.1f1\nm_EditorVersionWithRevision: 2021.3.16f1 (4016570cf34f)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = detect_project_version(temp_dir.path());
assert!(result.is_ok());
let version = result.unwrap();
assert_eq!(version.to_string(), "2021.3.16f1");
}
#[test]
fn test_detect_project_version_only_editor_version() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersion: 2019.4.31f1\nSomeOtherField: value";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = detect_project_version(temp_dir.path());
assert!(result.is_ok());
let version = result.unwrap();
assert_eq!(version.to_string(), "2019.4.31f1");
}
#[test]
fn test_detect_project_version_no_version_info() {
let temp_dir = TempDir::new().unwrap();
let version_content = "SomeField: value\nAnotherField: another_value";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = detect_project_version(temp_dir.path());
assert!(result.is_err());
}
#[test]
fn test_detect_project_version_malformed_version() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersion: not_a_valid_version";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = detect_project_version(temp_dir.path());
assert!(result.is_err());
}
#[test]
fn test_detect_project_version_recursive() {
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("my_project");
fs::create_dir(&subdir).unwrap();
create_unity_project(&subdir, "m_EditorVersion: 2022.1.5f1").unwrap();
let result = DetectOptions::new().recursive(true).detect_project_version(temp_dir.path());
assert!(result.is_ok());
let version = result.unwrap();
assert_eq!(version.to_string(), "2022.1.5f1");
}
#[test]
fn test_detect_project_version_no_project_found() {
let temp_dir = TempDir::new().unwrap();
let result = DetectOptions::new().recursive(true).detect_project_version(temp_dir.path());
assert!(result.is_err());
}
#[test]
fn test_detect_options_builder_pattern() {
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("nested").join("project");
fs::create_dir_all(&subdir).unwrap();
create_unity_project(&subdir, "m_EditorVersion: 2023.1.0f1").unwrap();
let result = DetectOptions::new()
.recursive(true)
.max_depth(5)
.case_sensitive(true)
.detect_unity_project_dir(temp_dir.path());
assert!(result.is_ok());
assert_eq!(result.unwrap(), subdir);
}
#[test]
fn test_detect_options_convenience_functions() {
let temp_dir = TempDir::new().unwrap();
create_unity_project(temp_dir.path(), "m_EditorVersion: 2022.3.5f1").unwrap();
let result = detect_unity_project_dir(temp_dir.path());
assert!(result.is_ok());
let version_result = detect_project_version(temp_dir.path());
assert!(result.is_ok());
assert_eq!(version_result.unwrap().to_string(), "2022.3.5f1");
}
#[test]
fn test_detect_options_default_vs_custom() {
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("deep").join("project");
fs::create_dir_all(&subdir).unwrap();
create_unity_project(&subdir, "m_EditorVersion: 2021.2.8f1").unwrap();
let default_result = DetectOptions::new().detect_unity_project_dir(temp_dir.path());
assert!(default_result.is_err());
let recursive_result = DetectOptions::new()
.recursive(true)
.detect_unity_project_dir(temp_dir.path());
assert!(recursive_result.is_ok());
assert_eq!(recursive_result.unwrap(), subdir);
}
#[test]
fn test_max_depth_limiting() {
let temp_dir = TempDir::new().unwrap();
let deep_project = temp_dir.path()
.join("level1")
.join("level2")
.join("level3")
.join("project");
fs::create_dir_all(&deep_project).unwrap();
create_unity_project(&deep_project, "m_EditorVersion: 2023.2.1f1").unwrap();
let limited_result = DetectOptions::new()
.recursive(true)
.max_depth(2)
.detect_unity_project_dir(temp_dir.path());
assert!(limited_result.is_err());
let deep_result = DetectOptions::new()
.recursive(true)
.max_depth(5)
.detect_unity_project_dir(temp_dir.path());
assert!(deep_result.is_ok());
assert_eq!(deep_result.unwrap(), deep_project);
let unlimited_result = DetectOptions::new()
.recursive(true)
.detect_unity_project_dir(temp_dir.path());
assert!(unlimited_result.is_ok());
assert_eq!(unlimited_result.unwrap(), deep_project);
}
#[test]
fn test_max_depth_zero_means_current_only() {
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("subproject");
fs::create_dir(&subdir).unwrap();
create_unity_project(&subdir, "m_EditorVersion: 2022.1.0f1").unwrap();
let depth_zero_result = DetectOptions::new()
.recursive(true)
.max_depth(0)
.detect_unity_project_dir(temp_dir.path());
assert!(depth_zero_result.is_err());
let current_dir_result = DetectOptions::new()
.recursive(true)
.max_depth(0)
.detect_unity_project_dir(&subdir);
assert!(current_dir_result.is_ok());
assert_eq!(current_dir_result.unwrap(), subdir);
}
#[test]
fn test_combined_options() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path().join("level1").join("level2").join("project");
fs::create_dir_all(&project_path).unwrap();
create_unity_project(&project_path, "m_EditorVersion: 2023.1.15f1").unwrap();
let result = DetectOptions::new()
.recursive(true)
.max_depth(3)
.case_sensitive(true)
.detect_unity_project_dir(temp_dir.path());
assert!(result.is_ok());
assert_eq!(result.unwrap(), project_path);
}
#[test]
fn test_detect_project_version_revision_hash_with_revision() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersion: 2021.3.55f1\nm_EditorVersionWithRevision: 2021.3.55f1 (f87d5274e360)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = DetectOptions::new().detect_project_version_revision_hash(temp_dir.path());
assert!(result.is_ok());
let revision_hash = result.unwrap();
assert_eq!(revision_hash.as_str(), "f87d5274e360");
}
#[test]
fn test_detect_project_version_revision_hash_without_revision() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersion: 2021.3.55f1";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = DetectOptions::new().detect_project_version_revision_hash(temp_dir.path());
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("Failed to parse unity version string"));
}
#[test]
fn test_detect_project_version_revision_hash_convenience_function_with_revision() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersionWithRevision: 2022.1.5f1 (abc123def456)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = detect_project_version_revision_hash(temp_dir.path());
assert!(result.is_ok());
let revision_hash = result.unwrap();
assert_eq!(revision_hash.as_str(), "abc123def456");
}
#[test]
fn test_detect_project_version_revision_hash_convenience_function_without_revision() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersion: 2022.1.5f1";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = detect_project_version_revision_hash(temp_dir.path());
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("Failed to parse unity version string"));
}
#[test]
fn test_detect_project_version_revision_hash_different_unity_versions() {
let test_cases = vec![
("2019.4.31f1 (be6d8d9ca5f4)", Some("be6d8d9ca5f4")),
("2020.3.48f1 (b805b124c6b2)", Some("b805b124c6b2")),
("2021.3.16f1 (4016570cf34f)", Some("4016570cf34f")),
("2022.3.5f1 (9674261d40ee)", Some("9674261d40ee")),
("2023.1.0a1 (123456789abc)", Some("123456789abc")),
("2023.2.0b5 (fedcba098765)", Some("fedcba098765")),
("2024.1.0p1 (111222333444)", Some("111222333444")),
("2019.4.31f1", None),
("2020.3.48f1", None),
("2021.3.16f1", None),
("2022.3.5f1", None),
];
for (version_with_revision, expected_hash) in test_cases {
let temp_dir = TempDir::new().unwrap();
let version_content = format!("m_EditorVersionWithRevision: {}", version_with_revision);
create_unity_project(temp_dir.path(), &version_content).unwrap();
let result = DetectOptions::new().detect_project_version_revision_hash(temp_dir.path());
match expected_hash {
Some(expected) => {
assert!(result.is_ok(), "Expected success for version with revision: {}", version_with_revision);
let revision_hash = result.unwrap();
assert_eq!(revision_hash.as_str(), expected, "Hash mismatch for: {}", version_with_revision);
}
None => {
assert!(result.is_err(), "Expected error for version without revision: {}", version_with_revision);
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("Failed to parse unity version string"));
}
}
}
}
#[test]
fn test_detect_project_version_revision_hash_invalid_hash_length() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersionWithRevision: 2021.3.55f1 (f87d527)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = DetectOptions::new().detect_project_version_revision_hash(temp_dir.path());
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("Failed to parse unity version string"));
assert!(error.to_string().contains("f87d527"));
}
#[test]
fn test_detect_project_version_revision_hash_invalid_hash_characters() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersionWithRevision: 2021.3.55f1 (f87d5274e36z)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = DetectOptions::new().detect_project_version_revision_hash(temp_dir.path());
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("Failed to parse unity version string"));
assert!(error.to_string().contains("f87d5274e36z"));
}
#[test]
fn test_detect_project_version_revision_hash_malformed_version() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersionWithRevision: not_a_valid_version (f87d5274e360)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = DetectOptions::new().detect_project_version_revision_hash(temp_dir.path());
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("Failed to parse unity version string"));
assert!(error.to_string().contains("not_a_valid_version"));
}
#[test]
fn test_detect_project_version_revision_hash_no_project() {
let temp_dir = TempDir::new().unwrap();
let result = DetectOptions::new().detect_project_version_revision_hash(temp_dir.path());
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
}
#[test]
fn test_detect_project_version_revision_hash_recursive_search() {
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("my_project");
fs::create_dir(&subdir).unwrap();
let version_content = "m_EditorVersionWithRevision: 2023.1.10f1 (deadbeefcafe)";
create_unity_project(&subdir, version_content).unwrap();
let result = DetectOptions::new()
.recursive(true)
.detect_project_version_revision_hash(temp_dir.path());
assert!(result.is_ok());
let revision_hash = result.unwrap();
assert_eq!(revision_hash.as_str(), "deadbeefcafe");
}
#[test]
fn test_detect_project_version_revision_hash_prefers_with_revision() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersion: 2021.3.55f1\nm_EditorVersionWithRevision: 2021.3.55f1 (f87d5274e360)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = DetectOptions::new().detect_project_version_revision_hash(temp_dir.path());
assert!(result.is_ok());
let revision_hash = result.unwrap();
assert_eq!(revision_hash.as_str(), "f87d5274e360");
}
#[test]
fn test_detect_project_complete_version_with_revision() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersion: 2021.3.55f1\nm_EditorVersionWithRevision: 2021.3.55f1 (f87d5274e360)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = DetectOptions::new().detect_project_complete_version(temp_dir.path());
assert!(result.is_ok());
let complete_version = result.unwrap();
assert_eq!(complete_version.version().to_string(), "2021.3.55f1");
assert_eq!(complete_version.revision().as_str(), "f87d5274e360");
assert_eq!(complete_version.to_string(), "2021.3.55f1 (f87d5274e360)");
}
#[test]
fn test_detect_project_complete_version_without_revision() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersion: 2021.3.55f1";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = DetectOptions::new().detect_project_complete_version(temp_dir.path());
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("Failed to parse unity version string"));
}
#[test]
fn test_detect_project_complete_version_convenience_function() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersionWithRevision: 2022.3.10f1 (abc123def456)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = detect_project_complete_version(temp_dir.path());
assert!(result.is_ok());
let complete_version = result.unwrap();
assert_eq!(complete_version.version().to_string(), "2022.3.10f1");
assert_eq!(complete_version.revision().as_str(), "abc123def456");
assert_eq!(complete_version.to_string(), "2022.3.10f1 (abc123def456)");
}
#[test]
fn test_detect_project_complete_version_invalid_hash() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersionWithRevision: 2021.3.55f1 (invalid)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let result = DetectOptions::new().detect_project_complete_version(temp_dir.path());
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("Failed to parse unity version string"));
}
#[test]
fn test_detect_project_complete_version_backwards_compatibility() {
let temp_dir = TempDir::new().unwrap();
let version_content = "m_EditorVersionWithRevision: 2021.3.55f1 (f87d5274e360)";
create_unity_project(temp_dir.path(), version_content).unwrap();
let version_result = DetectOptions::new().detect_project_version(temp_dir.path());
assert!(version_result.is_ok());
assert_eq!(version_result.unwrap().to_string(), "2021.3.55f1");
let hash_result = DetectOptions::new().detect_project_version_revision_hash(temp_dir.path());
assert!(hash_result.is_ok());
assert_eq!(hash_result.unwrap().as_str(), "f87d5274e360");
let complete_result = DetectOptions::new().detect_project_complete_version(temp_dir.path());
assert!(complete_result.is_ok());
let complete_version = complete_result.unwrap();
assert_eq!(complete_version.version().to_string(), "2021.3.55f1");
assert_eq!(complete_version.revision().as_str(), "f87d5274e360");
}
}