From 30d158546820becdf43140b1c601cb28c51c30ec Mon Sep 17 00:00:00 2001 From: Markus Triska Date: Fri, 19 Jun 2020 18:20:31 +0200 Subject: [PATCH 1/3] ADDED: library(files), for reasoning about files and directories. --- README.md | 3 ++ src/clause_types.rs | 15 +++++++ src/lib/files.pl | 88 +++++++++++++++++++++++++++++++++++++ src/machine/system_calls.rs | 53 +++++++++++++++++++++- 4 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 src/lib/files.pl diff --git a/README.md b/README.md index d8c4acd8..a04aecee 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,9 @@ The modules that ship with Scryer Prolog are also called `current_time/1` to obtain the current system time, the nonterminal `format_time//2` to describe strings with dates and times, and `sleep/1` to slow down a computation. +* [`files`](src/lib/files.pl) + Predicates for reasoning about files and directories, such as + `directory_files/2`, `file_exists/1` and `file_size/2`. * [`cont`](src/lib/cont.pl) Provides *delimited continuations* via `reset/3` and `shift/1`. * [`random`](src/lib/random.pl) diff --git a/src/clause_types.rs b/src/clause_types.rs index 7b7923c7..66222a6d 100644 --- a/src/clause_types.rs +++ b/src/clause_types.rs @@ -171,6 +171,11 @@ pub enum SystemClauseType { CurrentHostname, CurrentInput, CurrentOutput, + DirectoryFiles, + FileSize, + FileExists, + DirectoryExists, + MakeDirectory, DeleteAttribute, DeleteHeadAttribute, DynamicModuleResolution(usize), @@ -328,6 +333,11 @@ impl SystemClauseType { &SystemClauseType::CurrentInput => clause_name!("$current_input"), &SystemClauseType::CurrentHostname => clause_name!("$current_hostname"), &SystemClauseType::CurrentOutput => clause_name!("$current_output"), + &SystemClauseType::DirectoryFiles => clause_name!("$directory_files"), + &SystemClauseType::FileSize => clause_name!("$file_size"), + &SystemClauseType::FileExists => clause_name!("$file_exists"), + &SystemClauseType::DirectoryExists => clause_name!("$directory_exists"), + &SystemClauseType::MakeDirectory => clause_name!("$make_directory"), &SystemClauseType::REPL(REPLCodePtr::CompileBatch) => clause_name!("$compile_batch"), &SystemClauseType::REPL(REPLCodePtr::UseModule) => clause_name!("$use_module"), &SystemClauseType::REPL(REPLCodePtr::UseQualifiedModule) => { @@ -654,6 +664,11 @@ impl SystemClauseType { ("$unwind_environments", 0) => Some(SystemClauseType::UnwindEnvironments), ("$unwind_stack", 0) => Some(SystemClauseType::UnwindStack), ("$unify_with_occurs_check", 2) => Some(SystemClauseType::UnifyWithOccursCheck), + ("$directory_files", 2) => Some(SystemClauseType::DirectoryFiles), + ("$file_size", 2) => Some(SystemClauseType::FileSize), + ("$file_exists", 1) => Some(SystemClauseType::FileExists), + ("$directory_exists", 1) => Some(SystemClauseType::DirectoryExists), + ("$make_directory", 1) => Some(SystemClauseType::MakeDirectory), ("$use_module", 1) => Some(SystemClauseType::REPL(REPLCodePtr::UseModule)), ("$use_module_from_file", 1) => Some(SystemClauseType::REPL(REPLCodePtr::UseModuleFromFile)), diff --git a/src/lib/files.pl b/src/lib/files.pl new file mode 100644 index 00000000..4667fadd --- /dev/null +++ b/src/lib/files.pl @@ -0,0 +1,88 @@ +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Written June 2020 by Markus Triska (triska@metalevel.at) + Part of Scryer Prolog. + + Predicates for reasoning about files and directories. + + In this library, directories and files are represented as + *lists of characters*. This is an ideal representation: + + -) Lists of characters can be conveniently reasoned about with DCGs + and built-in Prolog predicates from library(lists). This alone + is already a very compelling argument to use them. + -) Other Scryer libraries such as library(http/http_open) also already + use lists of characters to represent paths. + -) File names are mostly ephemeral, so it is good for efficiency + that they can quickly allocated transiently on the heap, leaving the + atom table mostly unaffected. Indexing is almost never needed + for file names. + -) The previous point is also good for security, since the system + leaves little trace of which files were even accessed. + -) Scryer Prolog represents lists of characters extremely compactly. + + Some Prolog programmers will likely find this representation quite + unusual, because files are represented as *atoms* in many systems. + + However, the ISO standard only demands that sources and sinks be + *ground* terms, so lists of characters are completely admissible: + + A source/sink is specified as an implementation defined + ground term in a call of open/4 (8.11.5). All subsequent + references to the source/sink are made by referring to a + stream-term (7.10.2) or alias (7.10.2.2). + + I believe that with the advent of Scryer Prolog and its efficient + representation of strings as lists of characters, we should take + this opportunity for improvement, and in fact extend the use of + lists of characters also to other predicates like open/3. + + Please note that we *cannot* simply accept *both* representations, + because that would invalidate the type errors raised by this library, + and make future extensions for type checking impossible. + + So I ask you: Please try out this representation for a few months. + It simplifies working with files considerably. Once we have collected + more experience with this representations, please let us consider + how to proceed in such a way that we can call it an improvement. +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +:- module(files, [directory_files/2, + file_size/2, + file_exists/1, + directory_exists/1, + make_directory/1]). + +:- use_module(library(error)). +:- use_module(library(lists)). + +list_of_chars(Cs) :- + must_be(list, Cs), + ( ground(Cs) -> + ( member(C, Cs), \+ atom_length(C, 1) -> + type_error(char, C, files_and_directories) + ; true + ) + ; instantiation_error(s) + ). + +directory_files(Directory, Files) :- + list_of_chars(Directory), + can_be(list, Files), + '$directory_files'(Directory, Files). + +file_size(File, Size) :- + list_of_chars(File), + can_be(integer, Size), + '$file_size'(File, Size). + +file_exists(File) :- + list_of_chars(File), + '$file_exists'(File). + +directory_exists(Directory) :- + list_of_chars(Directory), + '$directory_exists'(Directory). + +make_directory(Directory) :- + list_of_chars(Directory), + '$make_directory'(Directory). diff --git a/src/machine/system_calls.rs b/src/machine/system_calls.rs index 97c16867..8e25941e 100644 --- a/src/machine/system_calls.rs +++ b/src/machine/system_calls.rs @@ -23,11 +23,11 @@ use crate::indexmap::IndexSet; use crate::ref_thread_local::RefThreadLocal; use std::cmp; +use std::fs; use std::collections::BTreeSet; use std::convert::TryFrom; use std::io::{ErrorKind, Read, Write}; use std::iter::{once, FromIterator}; -use std::fs::{OpenOptions}; use std::net::{TcpListener, TcpStream}; use std::ops::Sub; use std::rc::Rc; @@ -822,6 +822,55 @@ impl MachineState { } } } + &SystemClauseType::DirectoryFiles => { + let dir = self.heap_pstr_iter(self[temp_v!(1)]).to_string(); + let path = std::path::Path::new(&dir); + let mut files = Vec::new(); + + if let Ok(entries) = fs::read_dir(path) { + for entry in entries { + if let Ok(entry) = entry { + let name = entry.file_name().into_string().unwrap(); + files.push(self.heap.put_complete_string(&name)); + } + } + } + + let files_list = Addr::HeapCell(self.heap.to_list(files.into_iter())); + self.unify(self[temp_v!(2)], files_list); + } + &SystemClauseType::FileSize => { + let file = self.heap_pstr_iter(self[temp_v!(1)]).to_string(); + let len = Integer::from(fs::metadata(&file).unwrap().len()); + + let len = self.heap.to_unifiable(HeapCellValue::Integer(Rc::new(len))); + + self.unify(self[temp_v!(2)], len); + } + &SystemClauseType::FileExists => { + let file = self.heap_pstr_iter(self[temp_v!(1)]).to_string(); + if !std::path::Path::new(&file).exists() || !fs::metadata(&file).unwrap().is_file() { + self.fail = true; + return Ok(()); + } + } + &SystemClauseType::DirectoryExists => { + let directory = self.heap_pstr_iter(self[temp_v!(1)]).to_string(); + if !std::path::Path::new(&directory).exists() || !fs::metadata(&directory).unwrap().is_dir() { + self.fail = true; + return Ok(()); + } + } + &SystemClauseType::MakeDirectory => { + let directory = self.heap_pstr_iter(self[temp_v!(1)]).to_string(); + + match fs::create_dir(directory) { + Ok(_) => { } + _ => { self.fail = true; + return Ok(()); + } + } + } &SystemClauseType::AtEndOfExpansion => { if self.cp == LocalCodePtr::TopLevel(0, 0) { self.at_end_of_expansion = true; @@ -3256,7 +3305,7 @@ impl MachineState { let mode = atom_from!(self, indices, self.store(self.deref(self[temp_v!(2)]))); - let mut open_options = OpenOptions::new(); + let mut open_options = fs::OpenOptions::new(); let (is_input_file, in_append_mode) = match mode.as_str() { From aa9432e24fc18005c6d093d23f89432b6822b4c0 Mon Sep 17 00:00:00 2001 From: Markus Triska Date: Wed, 24 Jun 2020 22:29:22 +0200 Subject: [PATCH 2/3] comment on indexing, addressing a comment by @pmoura in #606 If indexing is needed for lists of characters, it should be added to the engine. --- src/lib/files.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/files.pl b/src/lib/files.pl index 4667fadd..3d219173 100644 --- a/src/lib/files.pl +++ b/src/lib/files.pl @@ -15,7 +15,7 @@ -) File names are mostly ephemeral, so it is good for efficiency that they can quickly allocated transiently on the heap, leaving the atom table mostly unaffected. Indexing is almost never needed - for file names. + for file names. If needed, it should be added to the engine. -) The previous point is also good for security, since the system leaves little trace of which files were even accessed. -) Scryer Prolog represents lists of characters extremely compactly. From 65f200e02caddc551588686ba435b3b3ec639e58 Mon Sep 17 00:00:00 2001 From: Markus Triska Date: Sat, 20 Jun 2020 10:12:14 +0200 Subject: [PATCH 3/3] directory_files/2: throw representation error if into_string() fails Suggested by @notoria in #606. Many thanks! --- src/machine/machine_errors.rs | 4 ++-- src/machine/system_calls.rs | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/machine/machine_errors.rs b/src/machine/machine_errors.rs index 8db4c7cd..dddc58ac 100644 --- a/src/machine/machine_errors.rs +++ b/src/machine/machine_errors.rs @@ -575,7 +575,7 @@ impl DomainErrorType { // from 7.12.2 f) of 13211-1:1995 #[derive(Debug, Clone, Copy)] pub enum RepFlag { - // Character, + Character, CharacterCode, InCharacterCode, MaxArity, @@ -586,7 +586,7 @@ pub enum RepFlag { impl RepFlag { pub fn as_str(self) -> &'static str { match self { - // RepFlag::Character => "character", + RepFlag::Character => "character", RepFlag::CharacterCode => "character_code", RepFlag::InCharacterCode => "in_character_code", RepFlag::MaxArity => "max_arity", diff --git a/src/machine/system_calls.rs b/src/machine/system_calls.rs index 8e25941e..6027f570 100644 --- a/src/machine/system_calls.rs +++ b/src/machine/system_calls.rs @@ -830,8 +830,16 @@ impl MachineState { if let Ok(entries) = fs::read_dir(path) { for entry in entries { if let Ok(entry) = entry { - let name = entry.file_name().into_string().unwrap(); - files.push(self.heap.put_complete_string(&name)); + match entry.file_name().into_string() { + Ok(name) => { files.push(self.heap.put_complete_string(&name)); } + _ => { + let stub = MachineError::functor_stub(clause_name!("directory_files"), 2); + let err = MachineError::representation_error(RepFlag::Character); + let err = self.error_form(err, stub); + + return Err(err); + } + } } } }