1 //: Clean syntax to manipulate and check the file system in scenarios.
  2 //: Instruction 'assume-resources' implicitly creates a variable called
  3 //: 'resources' that is accessible to later instructions in the scenario.
  4 
  5 :(scenarios run_mu_scenario)
  6 :(scenario simple_filesystem)
  7 scenario simple-filesystem [
  8   local-scope
  9   assume-resources [
 10   ¦ # file 'a' containing two lines of data
 11   ¦ [a] <- [
 12   ¦ ¦ |a bc|
 13   ¦ ¦ |de f|
 14   ¦ ]
 15   ¦ # directory 'b' containing two files, 'c' and 'd'
 16   ¦ [b/c] <- []
 17   ¦ [b/d] <- [
 18   ¦ ¦ |xyz|
 19   ¦ ]
 20   ]
 21   data:&:@:resource <- get *resources, data:offset
 22   file1:resource <- index *data, 0
 23   file1-name:text <- get file1, name:offset
 24   10:@:char/raw <- copy *file1-name
 25   file1-contents:text <- get file1, contents:offset
 26   100:@:char/raw <- copy *file1-contents
 27   file2:resource <- index *data, 1
 28   file2-name:text <- get file2, name:offset
 29   30:@:char/raw <- copy *file2-name
 30   file2-contents:text <- get file2, contents:offset
 31   40:@:char/raw <- copy *file2-contents
 32   file3:resource <- index *data, 2
 33   file3-name:text <- get file3, name:offset
 34   50:@:char/raw <- copy *file3-name
 35   file3-contents:text <- get file3, contents:offset
 36   60:@:char/raw <- copy *file3-contents
 37   memory-should-contain [
 38   ¦ 10:array:character <- [a]
 39   ¦ 100:array:character <- [a bc
 40 de f
 41 ]
 42   ¦ 30:array:character <- [b/c]
 43   ¦ 40:array:character <- []
 44   ¦ 50:array:character <- [b/d]
 45   ¦ 60:array:character <- [xyz
 46 ]
 47   ]
 48 ]
 49 
 50 :(scenario escaping_file_contents)
 51 scenario escaping-file-contents [
 52   local-scope
 53   assume-resources [
 54   ¦ # file 'a' containing a '|'
 55   ¦ # need to escape '\' once for each block
 56   ¦ [a] <- [
 57   ¦ ¦ |x\\\\|yz|
 58   ¦ ]
 59   ]
 60   data:&:@:resource <- get *resources, data:offset
 61   file1:resource <- index *data, 0
 62   file1-name:text <- get file1, name:offset
 63   10:@:char/raw <- copy *file1-name
 64   file1-contents:text <- get file1, contents:offset
 65   20:@:char/raw <- copy *file1-contents
 66   memory-should-contain [
 67   ¦ 10:array:character <- [a]
 68   ¦ 20:array:character <- [x|yz
 69 ]
 70   ]
 71 ]
 72 
 73 :(before "End Globals")
 74 extern const int RESOURCES = Next_predefined_global_for_scenarios++;
 75 //: give 'resources' a fixed location in scenarios
 76 :(before "End Special Scenario Variable Names(r)")
 77 Name[r]["resources"] = RESOURCES;
 78 //: make 'resources' always a raw location in scenarios
 79 :(before "End is_special_name Special-cases")
 80 if (s == "resources") return true;
 81 :(before "End Initialize Type Of Special Name In Scenario(r)")
 82 if (r.name == "resources") r.type = new_type_tree("address:resources");
 83 
 84 :(before "End initialize_transform_rewrite_literal_string_to_text()")
 85 recipes_taking_literal_strings.insert("assume-resources");
 86 
 87 //: screen-should-contain is a regular instruction
 88 :(before "End Primitive Recipe Declarations")
 89 ASSUME_RESOURCES,
 90 :(before "End Primitive Recipe Numbers")
 91 put(Recipe_ordinal, "assume-resources", ASSUME_RESOURCES);
 92 :(before "End Primitive Recipe Checks")
 93 case ASSUME_RESOURCES: {
 94   break;
 95 }
 96 :(before "End Primitive Recipe Implementations")
 97 case ASSUME_RESOURCES: {
 98   assert(scalar(ingredients.at(0)));
 99   assume_resources(current_instruction().ingredients.at(0).name, current_recipe_name());
100   break;
101 }
102 
103 :(code)
104 void assume_resources(const string& data, const string& caller) {
105   map<string, string> contents;
106   parse_resources(data, contents, caller);
107   construct_resources_object(contents);
108 }
109 
110 void parse_resources(const string& data, map<string, string>& out, const string& caller) {
111   istringstream in(data);
112   in >> std::noskipws;
113   while (true) {
114   ¦ if (!has_data(in)) break;
115   ¦ skip_whitespace_and_comments(in);
116   ¦ if (!has_data(in)) break;
117   ¦ string filename = next_word(in);
118   ¦ if (filename.empty()) {
119   ¦ ¦ assert(!has_data(in));
120   ¦ ¦ raise << "incomplete 'resources' block at end of file (0)\n" << end();
121   ¦ ¦ return;
122   ¦ }
123   ¦ if (*filename.begin() != '[') {
124   ¦ ¦ raise << caller << ": assume-resources: filename '" << filename << "' must begin with a '['\n" << end();
125   ¦ ¦ break;
126   ¦ }
127   ¦ if (*filename.rbegin() != ']') {
128   ¦ ¦ raise << caller << ": assume-resources: filename '" << filename << "' must end with a ']'\n" << end();
129   ¦ ¦ break;
130   ¦ }
131   ¦ filename.erase(0, 1);
132   ¦ filename.erase(SIZE(filename)-1);
133   ¦ if (!has_data(in)) {
134   ¦ ¦ raise << caller << ": assume-resources: no data for filename '" << filename << "'\n" << end();
135   ¦ ¦ break;
136   ¦ }
137   ¦ string arrow = next_word(in);
138   ¦ if (arrow.empty()) {
139   ¦ ¦ assert(!has_data(in));
140   ¦ ¦ raise << "incomplete 'resources' block at end of file (1)\n" << end();
141   ¦ ¦ return;
142   ¦ }
143   ¦ if (arrow != "<-") {
144   ¦ ¦ raise << caller << ": assume-resources: expected '<-' after filename '" << filename << "' but got '" << arrow << "'\n" << end();
145   ¦ ¦ break;
146   ¦ }
147   ¦ if (!has_data(in)) {
148   ¦ ¦ raise << caller << ": assume-resources: no data for filename '" << filename << "' after '<-'\n" << end();
149   ¦ ¦ break;
150   ¦ }
151   ¦ string contents = next_word(in);
152   ¦ if (contents.empty()) {
153   ¦ ¦ assert(!has_data(in));
154   ¦ ¦ raise << "incomplete 'resources' block at end of file (2)\n" << end();
155   ¦ ¦ return;
156   ¦ }
157   ¦ if (*contents.begin() != '[') {
158   ¦ ¦ raise << caller << ": assume-resources: file contents '" << contents << "' for filename '" << filename << "' must begin with a '['\n" << end();
159   ¦ ¦ break;
160   ¦ }
161   ¦ if (*contents.rbegin() != ']') {
162   ¦ ¦ raise << caller << ": assume-resources: file contents '" << contents << "' for filename '" << filename << "' must end with a ']'\n" << end();
163   ¦ ¦ break;
164   ¦ }
165   ¦ contents.erase(0, 1);
166   ¦ contents.erase(SIZE(contents)-1);
167   ¦ put(out, filename, munge_resources_contents(contents, filename, caller));
168   }
169 }
170 
171 string munge_resources_contents(const string& data, const string& filename, const string& caller) {
172   if (data.empty()) return "";
173   istringstream in(data);
174   in >> std::noskipws;
175   skip_whitespace_and_comments(in);
176   ostringstream out;
177   while (true) {
178   ¦ if (!has_data(in)) break;
179   ¦ skip_whitespace(in);
180   ¦ if (!has_data(in)) break;
181   ¦ if (in.peek() != '|') {
182   ¦ ¦ raise << caller << ": assume-resources: file contents for filename '" << filename << "' must be delimited in '|'s\n" << end();
183   ¦ ¦ break;
184   ¦ }
185   ¦ in.get();  // skip leading '|'
186   ¦ string line;
187   ¦ getline(in, line);
188   ¦ for (int i = 0;  i < SIZE(line);  ++i) {
189   ¦ ¦ if (line.at(i) == '|') break;
190   ¦ ¦ if (line.at(i) == '\\') {
191   ¦ ¦ ¦ ++i;  // skip
192   ¦ ¦ ¦ if (i == SIZE(line)) {
193   ¦ ¦ ¦ ¦ raise << caller << ": assume-resources: file contents can't end a line with '\\'\n" << end();
194   ¦ ¦ ¦ ¦ break;
195   ¦ ¦ ¦ }
196   ¦ ¦ }
197   ¦ ¦ out << line.at(i);
198   ¦ }
199   ¦ // todo: some way to represent a file without a final newline
200   ¦ out << '\n';
201   }
202   return out.str();
203 }
204 
205 void construct_resources_object(const map<string, string>& contents) {
206   int resources_data_address = allocate(SIZE(contents)*2 + /*array length*/1);
207   int curr = resources_data_address + /*skip refcount and length*/2;
208   for (map<string, string>::const_iterator p = contents.begin();  p != contents.end();  ++p) {
209   ¦ put(Memory, curr, new_mu_text(p->first));
210   ¦ trace(9999, "mem") << "storing file name " << get(Memory, curr) << " in location " << curr << end();
211   ¦ put(Memory, get(Memory, curr), 1);
212   ¦ trace(9999, "mem") << "storing refcount 1 in location " << get(Memory, curr) << end();
213   ¦ ++curr;
214   ¦ put(Memory, curr, new_mu_text(p->second));
215   ¦ trace(9999, "mem") << "storing file contents " << get(Memory, curr) << " in location " << curr << end();
216   ¦ put(Memory, get(Memory, curr), 1);
217   ¦ trace(9999, "mem") << "storing refcount 1 in location " << get(Memory, curr) << end();
218   ¦ ++curr;
219   }
220   curr = resources_data_address+/*skip refcount*/1;
221   put(Memory, curr, SIZE(contents));  // size of array
222   trace(9999, "mem") << "storing resources size " << get(Memory, curr) << " in location " << curr << end();
223   put(Memory, resources_data_address, 1);  // initialize refcount
224   trace(9999, "mem") << "storing refcount 1 in location " << resources_data_address << end();
225   // wrap the resources data in a 'resources' object
226   int resources_address = allocate(size_of_resources());
227   curr = resources_address+/*skip refcount*/1+/*offset of 'data' element*/1;
228   put(Memory, curr, resources_data_address);
229   trace(9999, "mem") << "storing resources data address " << resources_data_address << " in location " << curr << end();
230   put(Memory, resources_address, 1);  // initialize refcount
231   trace(9999, "mem") << "storing refcount 1 in location " << resources_address << end();
232   // save in product
233   put(Memory, RESOURCES, resources_address);
234   trace(9999, "mem") << "storing resources address " << resources_address << " in location " << RESOURCES << end();
235 }
236 
237 int size_of_resources() {
238   // memoize result if already computed
239   static int result = 0;
240   if (result) return result;
241   assert(get(Type_ordinal, "resources"));
242   type_tree* type = new type_tree("resources");
243   result = size_of(type)+/*refcount*/1;
244   delete type;
245   return result;
246 }