xi_core_lib/
file.rs

1// Copyright 2018 The xi-editor Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Interactions with the file system.
16
17use std::collections::HashMap;
18use std::ffi::OsString;
19use std::fmt;
20use std::fs::{self, File};
21use std::io::{self, Read, Write};
22use std::path::{Path, PathBuf};
23use std::str;
24use std::time::SystemTime;
25
26use xi_rope::Rope;
27use xi_rpc::RemoteError;
28
29use crate::tabs::BufferId;
30
31#[cfg(feature = "notify")]
32use crate::tabs::OPEN_FILE_EVENT_TOKEN;
33#[cfg(feature = "notify")]
34use crate::watcher::FileWatcher;
35#[cfg(target_family = "unix")]
36use std::{fs::Permissions, os::unix::fs::PermissionsExt};
37
38const UTF8_BOM: &str = "\u{feff}";
39
40/// Tracks all state related to open files.
41pub struct FileManager {
42    open_files: HashMap<PathBuf, BufferId>,
43    file_info: HashMap<BufferId, FileInfo>,
44    /// A monitor of filesystem events, for things like reloading changed files.
45    #[cfg(feature = "notify")]
46    watcher: FileWatcher,
47}
48
49#[derive(Debug)]
50pub struct FileInfo {
51    pub encoding: CharacterEncoding,
52    pub path: PathBuf,
53    pub mod_time: Option<SystemTime>,
54    pub has_changed: bool,
55    #[cfg(target_family = "unix")]
56    pub permissions: Option<u32>,
57}
58
59pub enum FileError {
60    Io(io::Error, PathBuf),
61    UnknownEncoding(PathBuf),
62    HasChanged(PathBuf),
63}
64
65#[derive(Debug, Clone, Copy)]
66pub enum CharacterEncoding {
67    Utf8,
68    Utf8WithBom,
69}
70
71impl FileManager {
72    #[cfg(feature = "notify")]
73    pub fn new(watcher: FileWatcher) -> Self {
74        FileManager { open_files: HashMap::new(), file_info: HashMap::new(), watcher }
75    }
76
77    #[cfg(not(feature = "notify"))]
78    pub fn new() -> Self {
79        FileManager { open_files: HashMap::new(), file_info: HashMap::new() }
80    }
81
82    #[cfg(feature = "notify")]
83    pub fn watcher(&mut self) -> &mut FileWatcher {
84        &mut self.watcher
85    }
86
87    pub fn get_info(&self, id: BufferId) -> Option<&FileInfo> {
88        self.file_info.get(&id)
89    }
90
91    pub fn get_editor(&self, path: &Path) -> Option<BufferId> {
92        self.open_files.get(path).cloned()
93    }
94
95    /// Returns `true` if this file is open and has changed on disk.
96    /// This state is stashed.
97    pub fn check_file(&mut self, path: &Path, id: BufferId) -> bool {
98        if let Some(info) = self.file_info.get_mut(&id) {
99            let mod_t = get_mod_time(path);
100            if mod_t != info.mod_time {
101                info.has_changed = true
102            }
103            return info.has_changed;
104        }
105        false
106    }
107
108    pub fn open(&mut self, path: &Path, id: BufferId) -> Result<Rope, FileError> {
109        if !path.exists() {
110            let _ = File::create(path).map_err(|e| FileError::Io(e, path.to_owned()))?;
111        }
112
113        let (rope, info) = try_load_file(path)?;
114
115        self.open_files.insert(path.to_owned(), id);
116        if self.file_info.insert(id, info).is_none() {
117            #[cfg(feature = "notify")]
118            self.watcher.watch(path, false, OPEN_FILE_EVENT_TOKEN);
119        }
120        Ok(rope)
121    }
122
123    pub fn close(&mut self, id: BufferId) {
124        if let Some(info) = self.file_info.remove(&id) {
125            self.open_files.remove(&info.path);
126            #[cfg(feature = "notify")]
127            self.watcher.unwatch(&info.path, OPEN_FILE_EVENT_TOKEN);
128        }
129    }
130
131    pub fn save(&mut self, path: &Path, text: &Rope, id: BufferId) -> Result<(), FileError> {
132        let is_existing = self.file_info.contains_key(&id);
133        if is_existing {
134            self.save_existing(path, text, id)
135        } else {
136            self.save_new(path, text, id)
137        }
138    }
139
140    fn save_new(&mut self, path: &Path, text: &Rope, id: BufferId) -> Result<(), FileError> {
141        try_save(path, text, CharacterEncoding::Utf8, self.get_info(id))
142            .map_err(|e| FileError::Io(e, path.to_owned()))?;
143        let info = FileInfo {
144            encoding: CharacterEncoding::Utf8,
145            path: path.to_owned(),
146            mod_time: get_mod_time(path),
147            has_changed: false,
148            #[cfg(target_family = "unix")]
149            permissions: get_permissions(path),
150        };
151        self.open_files.insert(path.to_owned(), id);
152        self.file_info.insert(id, info);
153        #[cfg(feature = "notify")]
154        self.watcher.watch(path, false, OPEN_FILE_EVENT_TOKEN);
155        Ok(())
156    }
157
158    fn save_existing(&mut self, path: &Path, text: &Rope, id: BufferId) -> Result<(), FileError> {
159        let prev_path = self.file_info[&id].path.clone();
160        if prev_path != path {
161            self.save_new(path, text, id)?;
162            self.open_files.remove(&prev_path);
163            #[cfg(feature = "notify")]
164            self.watcher.unwatch(&prev_path, OPEN_FILE_EVENT_TOKEN);
165        } else if self.file_info[&id].has_changed {
166            return Err(FileError::HasChanged(path.to_owned()));
167        } else {
168            let encoding = self.file_info[&id].encoding;
169            try_save(path, text, encoding, self.get_info(id))
170                .map_err(|e| FileError::Io(e, path.to_owned()))?;
171            self.file_info.get_mut(&id).unwrap().mod_time = get_mod_time(path);
172        }
173        Ok(())
174    }
175}
176
177fn try_load_file<P>(path: P) -> Result<(Rope, FileInfo), FileError>
178where
179    P: AsRef<Path>,
180{
181    // TODO: support for non-utf8
182    // it's arguable that the rope crate should have file loading functionality
183    let mut f =
184        File::open(path.as_ref()).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?;
185    let mut bytes = Vec::new();
186    f.read_to_end(&mut bytes).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?;
187
188    let encoding = CharacterEncoding::guess(&bytes);
189    let rope = try_decode(bytes, encoding, path.as_ref())?;
190    let info = FileInfo {
191        encoding,
192        mod_time: get_mod_time(&path),
193        #[cfg(target_family = "unix")]
194        permissions: get_permissions(&path),
195        path: path.as_ref().to_owned(),
196        has_changed: false,
197    };
198    Ok((rope, info))
199}
200
201#[allow(unused)]
202fn try_save(
203    path: &Path,
204    text: &Rope,
205    encoding: CharacterEncoding,
206    file_info: Option<&FileInfo>,
207) -> io::Result<()> {
208    let tmp_extension = path.extension().map_or_else(
209        || OsString::from("swp"),
210        |ext| {
211            let mut ext = ext.to_os_string();
212            ext.push(".swp");
213            ext
214        },
215    );
216    let tmp_path = &path.with_extension(tmp_extension);
217
218    let mut f = File::create(tmp_path)?;
219    match encoding {
220        CharacterEncoding::Utf8WithBom => f.write_all(UTF8_BOM.as_bytes())?,
221        CharacterEncoding::Utf8 => (),
222    }
223
224    for chunk in text.iter_chunks(..text.len()) {
225        f.write_all(chunk.as_bytes())?;
226    }
227
228    fs::rename(tmp_path, path)?;
229
230    #[cfg(target_family = "unix")]
231    {
232        if let Some(info) = file_info {
233            fs::set_permissions(path, Permissions::from_mode(info.permissions.unwrap_or(0o644)))?;
234        }
235    }
236
237    Ok(())
238}
239
240fn try_decode(bytes: Vec<u8>, encoding: CharacterEncoding, path: &Path) -> Result<Rope, FileError> {
241    match encoding {
242        CharacterEncoding::Utf8 => Ok(Rope::from(
243            str::from_utf8(&bytes).map_err(|_e| FileError::UnknownEncoding(path.to_owned()))?,
244        )),
245        CharacterEncoding::Utf8WithBom => {
246            let s = String::from_utf8(bytes)
247                .map_err(|_e| FileError::UnknownEncoding(path.to_owned()))?;
248            Ok(Rope::from(&s[UTF8_BOM.len()..]))
249        }
250    }
251}
252
253impl CharacterEncoding {
254    fn guess(s: &[u8]) -> Self {
255        if s.starts_with(UTF8_BOM.as_bytes()) {
256            CharacterEncoding::Utf8WithBom
257        } else {
258            CharacterEncoding::Utf8
259        }
260    }
261}
262
263/// Returns the modification timestamp for the file at a given path,
264/// if present.
265fn get_mod_time<P: AsRef<Path>>(path: P) -> Option<SystemTime> {
266    File::open(path).and_then(|f| f.metadata()).and_then(|meta| meta.modified()).ok()
267}
268
269/// Returns the file permissions for the file at a given path on UNIXy systems,
270/// if present.
271#[cfg(target_family = "unix")]
272fn get_permissions<P: AsRef<Path>>(path: P) -> Option<u32> {
273    File::open(path).and_then(|f| f.metadata()).map(|meta| meta.permissions().mode()).ok()
274}
275
276impl From<FileError> for RemoteError {
277    fn from(src: FileError) -> RemoteError {
278        //TODO: when we migrate to using the failure crate for error handling,
279        // this should return a better message
280        let code = src.error_code();
281        let message = src.to_string();
282        RemoteError::custom(code, message, None)
283    }
284}
285
286impl FileError {
287    fn error_code(&self) -> i64 {
288        match self {
289            FileError::Io(_, _) => 5,
290            FileError::UnknownEncoding(_) => 6,
291            FileError::HasChanged(_) => 7,
292        }
293    }
294}
295
296impl fmt::Display for FileError {
297    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
298        match self {
299            FileError::Io(ref e, ref p) => write!(f, "{}. File path: {:?}", e, p),
300            FileError::UnknownEncoding(ref p) => write!(f, "Error decoding file: {:?}", p),
301            FileError::HasChanged(ref p) => write!(
302                f,
303                "File has changed on disk. \
304                 Please save elsewhere and reload the file. File path: {:?}",
305                p
306            ),
307        }
308    }
309}