PageRenderTime 37ms CodeModel.GetById 14ms app.highlight 12ms RepoModel.GetById 1ms app.codeStats 1ms

/src/lib/io/low_level/basic_directory.e

http://github.com/tybor/Liberty
Specman e | 711 lines | 539 code | 45 blank | 127 comment | 38 complexity | 53777f334ba48cd9e4bde263b54d844b MD5 | raw file
  1-- This file is part of a Liberty Eiffel library.
  2-- See the full copyright at the end.
  3--
  4expanded class BASIC_DIRECTORY
  5   --
  6   -- Very low-level basic tools for file-system directory handling and file path manipulation.  This class
  7   -- intended to be platform independent as much as possible. In order to remove from the client side the
  8   -- burden of file path computation, this class tries to compute automatically the system file notation
  9   -- using argument(s) of some of the very first call(s).  As soon as the system notation has been properly
 10   -- detected, the result is internally memorized for all objects of type BASIC_DIRECTORY in a common private
 11   -- buffer. Besides the low-level nature of operations one can found in this class, all file path
 12   -- manipulations are done in a smart way (except when the system file path notation has not been detected
 13   -- automatically, which is quite uncommon). As an example, even if the directory separator is internally
 14   -- detected, this information is _intentionally_ kept private to avoid low-level manipulation from the
 15   -- client side. Finally, this class is expanded in order to avoid as much as possible memory allocations.
 16   --
 17   -- Also consider high level facade class DIRECTORY if you don't want to deal directly with low level
 18   -- directory streams.
 19   --
 20
 21feature {}
 22   directory_stream: POINTER
 23         -- This pointer memorize the current directory stream being scanned (used to compute `is_connected').
 24
 25   current_entry: POINTER
 26         -- When `is_connected', memorize the current entry in the current  `directory_stream'.
 27
 28feature {ANY} -- State of `Current' basic directory stream:
 29   is_connected: BOOLEAN
 30         -- Is `Current' connected to some directory stream ?
 31      do
 32         Result := directory_stream.is_not_null
 33      end
 34
 35   end_of_input: BOOLEAN
 36         -- Is end of input reached ?
 37      require
 38         is_connected
 39      do
 40         Result := current_entry.is_null
 41      end
 42
 43feature {ANY} -- Connect and disconnect:
 44   connect_to (directory_path: ABSTRACT_STRING)
 45         -- Try to connect `Current' to some existing `directory_path'. After this call, the client
 46         -- supposed to use `is_connected' to check that the stream is ready to be used.
 47      require
 48         not is_connected
 49         not directory_path.is_empty
 50         common_buffer_protection: last_entry /= directory_path
 51      local
 52         path_pointer: POINTER
 53      do
 54         path_pointer := directory_path.to_external
 55         directory_stream := directory_open(path_pointer)
 56         current_entry := directory_stream
 57         last_entry.clear_count
 58      ensure
 59         is_connected implies not end_of_input
 60      end
 61
 62   connect_with (some_path: ABSTRACT_STRING)
 63         -- Try to connect `Current' to some directory using `some_path' which may be either an existing
 64         -- directory path or some arbitrary file path name. When `some_path' is the path of some readable
 65         -- existing directory, this directory is opened and the effect of `connect_with' is equivalent to
 66         -- `connect_to'. When `some_path' is not an existing readable directory path, `connect_with' tries to
 67         -- open the directory which may contains `some_path' viewed as a file path name. After this call, the
 68         -- client is supposed to use `is_connected' to check that the stream is ready to be used and the
 69         -- `last_entry' buffer to know about the corresponding opened directory path. Whatever the result,
 70         -- `some_path' is left unchanged.
 71      require
 72         not is_connected
 73         not some_path.is_empty
 74         common_buffer_protection: last_entry /= some_path
 75      local
 76         p: POINTER
 77      do
 78         connect_to(some_path)
 79         if is_connected then
 80            last_entry.copy(some_path.out)
 81         else
 82            compute_parent_directory_of(some_path)
 83            if last_entry.count > 0 then
 84               p := last_entry.to_external
 85               directory_stream := directory_open(p)
 86               current_entry := directory_stream
 87               if directory_stream.is_null then
 88                  last_entry.clear_count
 89               end
 90            else
 91               last_entry.clear_count
 92            end
 93         end
 94      ensure
 95         is_connected implies not end_of_input
 96      end
 97
 98   connect_to_current_working_directory
 99         -- Try to connect `Current' to the current working directory. After this call, the client is supposed
100         -- to use `is_connected' to check that the stream is ready to be used and the `last_entry' buffer to
101         -- know about the name of the current working directory.
102      require
103         not is_connected
104      local
105         path: POINTER
106      do
107         path := directory_current_working_directory
108         if path.is_not_null then
109            last_entry.from_external_copy(path)
110            directory_stream := directory_open(path)
111            current_entry := directory_stream
112            if directory_stream.is_null then
113               last_entry.clear_count
114            else
115               if not system_notation_detected then
116                  set_notation_using(last_entry)
117               end
118               system_notation.to_directory_path(last_entry)
119            end
120         else
121            last_entry.clear_count
122         end
123      ensure
124         is_connected implies not end_of_input
125      end
126
127   disconnect
128         -- Do not forget to call this feature when you have finished with some previously opened directory
129         -- stream.
130      require
131         is_connected
132      local
133         null: POINTER
134      do
135         if directory_close(directory_stream) then
136            directory_stream := null
137            current_entry := null
138         end
139      ensure
140         not is_connected
141      end
142
143feature {ANY} -- Scanning:
144   last_entry: STRING
145         -- Unique global buffer (once object) to get the last information computed by many routines of this
146         -- class: `read_entry', `connect_with' `connect_to_current_working_directory',
147         -- `compute_parent_directory_of', ...
148      once
149         create Result.make(256)
150      end
151
152   read_entry
153         -- Read the next entry name and update `last_entry' and `end_of_input' accordingly.
154      require
155         is_connected
156         not end_of_input
157      local
158         name: POINTER
159      do
160         current_entry := directory_read_entry(directory_stream)
161         if current_entry.is_not_null then
162            name := directory_get_entry_name(current_entry)
163            last_entry.from_external_copy(name)
164         end
165      end
166
167feature {ANY} -- File path handling tools:
168   compute_parent_directory_of (some_path: ABSTRACT_STRING)
169         -- Using `some_path' (which may be either a file path or a directory path) tries to compute in the
170         -- `last_entry' buffer the parent directory of `some_path'. When `some_path' is a path with no parent
171         -- directory, the `last_entry' buffer `is_empty' after this call. This operation does not perform any
172         -- disk access.
173      require
174         not some_path.is_empty
175         common_buffer_protection: last_entry /= some_path
176      do
177         if system_notation /= Void then
178            last_entry.copy(some_path.out)
179            system_notation.to_parent_directory(last_entry)
180         else
181            set_notation_using(some_path)
182            if system_notation /= Void then
183               last_entry.copy(some_path.out)
184               system_notation.to_parent_directory(last_entry)
185            else
186               last_entry.clear_count
187            end
188         end
189      end
190
191   compute_subdirectory_with (parent_path, entry_name: ABSTRACT_STRING)
192         -- Try to compute in the `last_entry' buffer the new subdirectory path obtained when trying to
193         -- concatenate smartly `parent_path' with some `entry_name'. When this fails the `last_entry' buffer
194         -- `is_empty' after this call. This operation does not perform any disk access. Whatever the result,
195         -- `parent_path' and `entry_name' are left unchanged.
196      require
197         not parent_path.is_empty
198         not entry_name.is_empty
199         common_buffer_protection1: last_entry /= parent_path
200         common_buffer_protection2: last_entry /= entry_name
201      do
202         if system_notation /= Void then
203            last_entry.copy(parent_path.out)
204            system_notation.to_subdirectory_with(last_entry, entry_name.out)
205         else
206            set_notation_using(parent_path.out)
207            if system_notation /= Void then
208               last_entry.copy(parent_path.out)
209               system_notation.to_subdirectory_with(last_entry, entry_name.out)
210            else
211               last_entry.clear_count
212            end
213         end
214      end
215
216   compute_file_path_with (parent_path, file_name: ABSTRACT_STRING)
217         -- Try to compute in the `last_entry' buffer the new file path obtained when trying to concatenate
218         -- smartly `parent_path' with some `file_name'. When this fails the `last_entry' buffer `is_empty'
219         -- after this call. This operation does not perform any disk access. Whatever the result,
220         -- `parent_path' and `file_name' are left unchanged.
221      require
222         not parent_path.is_empty
223         not file_name.is_empty
224         common_buffer_protection1: last_entry /= parent_path
225         common_buffer_protection2: last_entry /= file_name
226      do
227         if system_notation /= Void then
228            last_entry.copy(parent_path.out)
229            system_notation.to_file_path_with(last_entry, file_name.out)
230         else
231            set_notation_using(parent_path)
232            if system_notation /= Void then
233               last_entry.copy(parent_path.out)
234               system_notation.to_file_path_with(last_entry, file_name.out)
235            else
236               last_entry.clear_count
237            end
238         end
239      end
240
241   compute_absolute_file_path_with (path: ABSTRACT_STRING)
242         -- Try to compute an absolute path equivalent to `path' and store it in `last_entry'. When this fails
243         -- the `last_entry' buffer `is_empty' after this call. This operation does not perform any disk
244         -- access.  Whatever the result, `path' is left unchanged.
245      require
246         valid_path(path)
247         common_buffer_protection: last_entry /= path
248      do
249         if system_notation /= Void then
250            last_entry.copy(current_working_directory.out)
251            system_notation.to_absolute_path_in(last_entry, path.out)
252         else
253            set_notation_using(path)
254            if system_notation /= Void then
255               last_entry.copy(current_working_directory.out)
256               system_notation.to_absolute_path_in(last_entry, path.out)
257            else
258               last_entry.clear_count
259            end
260         end
261      ensure
262         last_entry.is_empty or else system_notation.is_absolute_path(last_entry.out)
263      end
264
265   compute_short_name_of (path: ABSTRACT_STRING)
266         -- Try to find the short name of the file or directory given by its `path' and store it in
267         -- `last_entry'. When this fails the `last_entry' buffer `is_empty' after the call. This operation
268         -- does not perform any disk access.  Whatever the result, `path' is left unchanged.
269      do
270         if system_notation /= Void then
271            last_entry.copy(current_working_directory.out)
272            system_notation.to_short_name_in(last_entry, path.out)
273         else
274            set_notation_using(path)
275            if system_notation /= Void then
276               last_entry.copy(current_working_directory.out)
277               system_notation.to_short_name_in(last_entry, path.out)
278            else
279               last_entry.clear_count
280            end
281         end
282      end
283
284   valid_path (path: ABSTRACT_STRING): BOOLEAN
285         -- Is the syntax of `path' valid for the system notation?
286      do
287         if system_notation_detected then
288            Result := system_notation.is_valid_path(path.out)
289         else
290            Result := not path.is_empty
291         end
292      ensure
293         system_notation_detected implies Result = system_notation.is_valid_path(path.out)
294         not system_notation_detected implies Result = not path.is_empty
295      end
296
297   change_current_working_directory (directory_path: ABSTRACT_STRING)
298         -- Try to change the current working directory using some `directory_path'.  When the operation
299         -- possible, the `last_entry' buffer is updated with the new current working directory path,
300         -- otherwise, when the modification is not possible the `last_entry' buffer `is_empty' after this
301         -- call. Whatever the result, `directory_path' is left unchanged.
302      require
303         not is_connected
304         common_buffer_protection1: last_entry /= directory_path
305      local
306         p: POINTER
307      do
308         p := directory_path.to_external
309         if directory_chdir(p) then
310            connect_to_current_working_directory
311            if is_connected then
312               disconnect
313               check
314                  not last_entry.is_empty
315               end
316            else
317               last_entry.clear_count
318            end
319         else
320            last_entry.clear_count
321         end
322      ensure
323         not is_connected
324      end
325
326   current_working_directory: FIXED_STRING
327         -- The current working directory. Always returns the same once STRING.
328      local
329         path: POINTER; cwd: STRING
330      do
331         path := directory_current_working_directory
332         if path.is_not_null then
333            cwd := once ""
334            cwd.from_external_copy(path)
335            if not system_notation_detected then
336               set_notation_using(cwd)
337            end
338            system_notation.to_directory_path(cwd)
339            Result := cwd.intern
340         end
341      end
342
343   ensure_system_notation
344      local
345         dummy: BOOLEAN
346      once
347         dummy := require_system_notation
348      ensure
349         system_notation /= Void
350      end
351
352   require_system_notation: BOOLEAN
353         -- Same as `ensure_system_notation', useful for contracts
354      once
355         Result := current_working_directory /= Void
356      ensure
357         system_notation /= Void
358         Result
359      end
360
361feature {ANY} -- Disk modification:
362   create_new_directory (directory_path: ABSTRACT_STRING): BOOLEAN
363         -- Try to create a new directory using the `directory_path' name.
364         -- Returns True on success.
365      require
366         not is_connected
367      local
368         p: POINTER
369      do
370         p := directory_path.to_external
371         Result := directory_mkdir(p)
372      ensure
373         not is_connected
374      end
375
376   remove_directory (directory_path: ABSTRACT_STRING): BOOLEAN
377         -- Try to remove directory `directory_path' which must be empty.
378         -- Returns True on success.
379      require
380         not is_connected
381      local
382         p: POINTER
383      do
384         p := directory_path.to_external
385         Result := directory_rmdir(p)
386      ensure
387         not is_connected
388      end
389
390   remove_files_of (directory_path: ABSTRACT_STRING)
391         -- Try to remove all files (not subdirectories) of directory specified by `directory_path'.
392      require
393         not is_connected
394      local
395         ft: FILE_TOOLS
396      do
397         connect_to(directory_path)
398         if is_connected then
399            from
400               read_entry
401            until
402               end_of_input
403            loop
404               tmp_path.copy(last_entry)
405               compute_file_path_with(directory_path, tmp_path)
406               tmp_path.copy(last_entry)
407               if ft.is_file(tmp_path) then
408                  ft.delete(tmp_path)
409               end
410               read_entry
411            end
412            disconnect
413         end
414      ensure
415         not is_connected
416      end
417
418   remove_recursively (directory_path: ABSTRACT_STRING): BOOLEAN
419         -- Try to remove all files and all subdirectories of directory specified by `directory_path'.
420      require
421         not is_connected
422      local
423         ft: FILE_TOOLS
424      do
425         connect_to(directory_path)
426         Result := is_connected
427         if Result then
428            from
429               read_entry
430            until
431               end_of_input
432            loop
433               if not system_notation.is_current_directory(last_entry) and then not system_notation.is_parent_directory(last_entry) then
434                  tmp_path.copy(last_entry)
435                  compute_file_path_with(directory_path, tmp_path)
436                  tmp_path.copy(last_entry)
437                  if ft.is_directory(tmp_path) then
438                     disconnect
439                     Result := Result and remove_recursively(tmp_path.twin)
440                     connect_to(directory_path)
441                  else
442                     ft.delete(tmp_path)
443                  end
444               end
445               read_entry
446            end
447            disconnect
448         end
449         Result := Result and remove_directory(directory_path)
450      ensure
451         not is_connected
452      end
453
454feature {ANY} -- Miscellaneous:
455   is_case_sensitive: BOOLEAN
456      local
457         bd: like Current
458      do
459         if system_notation = Void then
460            -- Try to create system_notation
461            bd.connect_to_current_working_directory
462            if bd.is_connected then
463               if not last_entry.is_empty then
464                  set_notation_using(last_entry)
465                  if system_notation /= Void then
466                     Result := system_notation.is_case_sensitive
467                  end
468               end
469               bd.disconnect
470            end
471         else
472            Result := system_notation.is_case_sensitive
473         end
474      end
475
476   system_notation: DIRECTORY_NOTATION
477      do
478         Result := system_notation_buffer.item
479      end
480
481feature {DIRECTORY_NOTATION_HANDLER}
482   system_notation_buffer: REFERENCE[DIRECTORY_NOTATION]
483         -- Unique common buffer to memorize the system path notation.
484      once
485         create Result
486      end
487
488   system_notation_detected: BOOLEAN
489      do
490         Result := system_notation /= Void
491      end
492
493   unix_notation: BOOLEAN
494         -- The Unix like file path notation looks like:
495         --   /LibertyEiffel/sys/system.se
496      do
497         Result := {UNIX_DIRECTORY_NOTATION} ?:= system_notation
498      end
499
500   windows_notation: BOOLEAN
501         -- The Windows like file path notation looks like:
502         --   C:\LibertyEiffel\sys\system.se
503      do
504         Result := {WINDOWS_DIRECTORY_NOTATION} ?:= system_notation
505      end
506
507   cygwin_notation: BOOLEAN
508         -- The Cygwin like file path notation looks like:
509         --   //C/LibertyEiffel/sys/system.se
510      do
511         Result := {CYGWIN_DIRECTORY_NOTATION} ?:= system_notation
512      end
513
514   openvms_notation: BOOLEAN
515         -- The VMS file path notation looks like:
516         --    DISK:[LibertyEiffel.sys]system.se
517         -- The current working directory notation is:
518         --    DISK:[]
519         -- The equivalent of Unix .. is :
520         --    [-]
521         -- The equivalent of Unix ../.. is :
522         --    [-.-]
523         --
524      do
525         Result := {OPENVMS_DIRECTORY_NOTATION} ?:= system_notation
526      end
527
528   set_notation_using (some_path: ABSTRACT_STRING)
529         -- Try to detect automatically the file system notation.
530      require
531         not some_path.is_empty
532         not system_notation_detected
533      do
534         inspect
535            some_path.first
536         when '/', '.', '~' then
537            if some_path.count >= 4 and then some_path.item(2) = '/' and then some_path.item(4) = '/' then
538               system_notation_buffer.set_item(create {CYGWIN_DIRECTORY_NOTATION})
539            else
540               system_notation_buffer.set_item(create {UNIX_DIRECTORY_NOTATION})
541            end
542         when '\' then
543            system_notation_buffer.set_item(create {WINDOWS_DIRECTORY_NOTATION})
544         when '[' then
545            system_notation_buffer.set_item(create {OPENVMS_DIRECTORY_NOTATION})
546         when 'a' .. 'z', 'A' .. 'Z' then
547            if some_path.count >= 2 then
548               inspect
549                  some_path.item(2)
550               when ':' then
551                  if some_path.count = 2 then
552                     system_notation_buffer.set_item(create {WINDOWS_DIRECTORY_NOTATION})
553                  elseif some_path.has('\') then
554                     system_notation_buffer.set_item(create {WINDOWS_DIRECTORY_NOTATION})
555                  elseif some_path.has('/') then
556                     system_notation_buffer.set_item(create {CYGWIN_DIRECTORY_NOTATION})
557                  end
558               when 'a' .. 'z', 'A' .. 'Z' then
559                  if some_path.has('[') then
560                     system_notation_buffer.set_item(create {OPENVMS_DIRECTORY_NOTATION})
561                  elseif some_path.has(':') then
562                     if some_path.has('[') then
563                        system_notation_buffer.set_item(create {OPENVMS_DIRECTORY_NOTATION})
564                     end
565                  elseif some_path.has('/') then
566                     system_notation_buffer.set_item(create {UNIX_DIRECTORY_NOTATION})
567                  elseif some_path.has('\') then
568                     --|*** This looks weird <FM-23/03/2005>
569                     system_notation_buffer.set_item(create {UNIX_DIRECTORY_NOTATION})
570                  end
571               else
572               end
573            end
574         else
575         end
576      end
577
578   reset_notation_using (some_path: ABSTRACT_STRING)
579         -- Try to detect automatically the file system notation.
580      do
581         system_notation_buffer.set_item(Void)
582         set_notation_using(some_path)
583      end
584
585   reset_notation
586      do
587         reset_notation_using(current_working_directory)
588      end
589
590feature {}
591   tmp_path: STRING
592      once
593         create Result.make(256)
594      end
595
596   directory_open (path_pointer: POINTER): POINTER
597         -- Try to open some existing directory using `path'. When `Result' `is_not_null', the directory
598         -- correctly opened and `Result' is a valid handle for this directory.  Using `Result', one can then
599         -- scan the content of the directory using function `basic_directory_read_entry' and
600         -- `basic_directory_get_entry_name'. Finally, a `is_not_null' directory must be closed using function
601         -- `basic_directory_close'.
602      require
603         path_pointer.is_not_null
604      external "plug_in"
605      alias "{
606             location: "${sys}/plugins/io"
607             module_name: "directory"
608             feature_name: "directory_open"
609         }"
610      end
611
612   directory_read_entry (dirstream: POINTER): POINTER
613         -- Read an return a new entry using the directory handle `dirstream' obtained with function
614         -- `basic_directory_open'. When there is no more entry, the `Result' becomes `is_null'.
615      require
616         dirstream.is_not_null
617      external "plug_in"
618      alias "{
619             location: "${sys}/plugins/io"
620             module_name: "directory"
621             feature_name: "directory_read_entry"
622         }"
623      end
624
625   directory_get_entry_name (entry: POINTER): POINTER
626         -- Read an return a new entry using the directory handle `dirstream' obtained with function
627         -- `basic_directory_open'. When there is no more entry, the `Result' becomes `is_null'.
628      require
629         entry.is_not_null
630      external "plug_in"
631      alias "{
632             location: "${sys}/plugins/io"
633             module_name: "directory"
634             feature_name: "directory_get_entry_name"
635         }"
636      end
637
638   directory_close (dirstream: POINTER): BOOLEAN
639         -- Try to close some opened `dirstream' directory. A True result indicates that the directory
640         -- correctly closed.
641      require
642         dirstream.is_not_null
643      external "plug_in"
644      alias "{
645             location: "${sys}/plugins/io"
646             module_name: "directory"
647             feature_name: "directory_close"
648         }"
649      end
650
651   directory_current_working_directory: POINTER
652         -- Try to get the current working directory path.
653      external "plug_in"
654      alias "{
655             location: "${sys}/plugins/io"
656             module_name: "directory"
657             feature_name: "directory_current_working_directory"
658         }"
659      end
660
661   directory_chdir (destination: POINTER): BOOLEAN
662         -- Try to change the current working directory using `destination'.
663      external "plug_in"
664      alias "{
665             location: "${sys}/plugins/io"
666             module_name: "directory"
667             feature_name: "directory_chdir"
668         }"
669      end
670
671   directory_mkdir (directory_path: POINTER): BOOLEAN
672         -- Try to create a new directory using `directory_path'.
673      external "plug_in"
674      alias "{
675             location: "${sys}/plugins/io"
676             module_name: "directory"
677             feature_name: "directory_mkdir"
678         }"
679      end
680
681   directory_rmdir (directory_path: POINTER): BOOLEAN
682         -- Try to remove `directory_path'.
683      external "plug_in"
684      alias "{
685             location: "${sys}/plugins/io"
686             module_name: "directory"
687             feature_name: "directory_rmdir"
688         }"
689      end
690
691end -- class BASIC_DIRECTORY
692--
693-- Copyright (C) 2009-2017: by all the people cited in the AUTHORS file.
694--
695-- Permission is hereby granted, free of charge, to any person obtaining a copy
696-- of this software and associated documentation files (the "Software"), to deal
697-- in the Software without restriction, including without limitation the rights
698-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
699-- copies of the Software, and to permit persons to whom the Software is
700-- furnished to do so, subject to the following conditions:
701--
702-- The above copyright notice and this permission notice shall be included in
703-- all copies or substantial portions of the Software.
704--
705-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
706-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
707-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
708-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
709-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
710-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
711-- THE SOFTWARE.