123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927 |
- \input lisp-tutorial.tex
- @setfilename lisp-tutorial.info
- @settitle Programming Lisp translators
- @copying
- Copyright (C) 2008
- Free Software Foundation, Inc.
- @quotation
- Permission is granted to copy, distribute and/or modify this document
- under the terms of the GNU Free Documentation License, Version 1.1 or
- any later version published by the Free Software Foundation; with no
- Invariant Sections, with the Front-Cover texts being ``A GNU
- Manual'', and with the Back-Cover Texts as in (a) below. A copy of the
- license is included in the section entitled ``GNU Free Documentation
- License'' in the Emacs manual.
- (a) The FSF's Back-Cover Text is: ``You have freedom to copy and modify
- this GNU Manual, like GNU software. Copies published by the Free
- Software Foundation raise funds for GNU development.''
- This document is part of a collection distributed under the GNU Free
- Documentation License. If you want to distribute this document
- separately from the collection, you can do so by adding a copy of the
- license to the document, as described in section 6 of the license.
- @end quotation
- @end copying
- @dircategory Programming Lisp translators
- @direntry
- * Lisp translators (lisp translators). How to program Lisp translators.
- @end direntry
- @titlepage
- @title Programming Lisp Translators
- @author Flavio Cruz
- @page
- @insertcopying
- @end titlepage
- @contents
- @node Top
- @top Introduction
- This document will introduce you to a new Hurd translator library that can be used to easily implement new file system servers.
- The main advantages of this library compared to libnetfs or libtrivfs, are:
- @itemize @bullet
- @item
- A much more fast development - What can take weeks to do in libnetfs, can be done in a few hours or less.
- @item
- Easy development - You can put more focus on high level details of your translators and less on the little details that arise when writing C based translators.
- @item
- Use of the Common Lisp language - Programming in Lisp is great fun.
- @item
- Prototyping - You can test and validate new translator ideas and then build a fast implementation in C.
- @end itemize
- Of course, there are also some drawbacks:
- @itemize @bullet
- @item
- Efficiency - Don't expect to run your Lisp based translators as fast as the C based ones.
- @item
- Disk based translators - Don't expect to write the next generation ext file system.
- @end itemize
- Given these pros and cons, it's easy to deduce that this library is more suited to implement virtual file systems. Examples are:
- @itemize @bullet
- @item
- Translators were data is located in a local file (like zipfs, tarfs, rarfs, ...)
- @item
- Single file translators (that do content filtering, output of a command, etc).
- @item
- Network based file systems (ftpfs, httpfs, ircfs, ...)
- @item
- Proxy file systems (like hostmux, usermux, etc..).
- @end itemize
- @menu
- * Dependencies:: What needs to be installed before using the library.
- * Installation:: How to install the library.
- * Using the provided translators:: How to use the unzip, irc, and mux translators
- * A /dev/zero like translator:: Introductory example.
- * A more complex example:: A more comprehensive example.
- * Conclusion:: Conclusion text.
- @end menu
- @node Dependencies, Installation, Top, Top
- @chapter Dependencies
- Right now, only CLISP is supported. Porting to new Lisp implementations will happen has soon they run and compile sanely on the Hurd operating system (especially SBCL ;-)).
- You must also have these asdf packages installed on your system:
- @itemize @bullet
- @item
- @uref{http://www.common-lisp.net/project/cffi/,CFFI}
- It can be installed on Debian GNU/Hurd with @command{apt-get install cl-cffi}.
- @item
- @uref{http://www.weitz.de/flexi-streams/,flexi-streams}
- Can also be installed with @command{apt-get install cl-flexi-streams}.
- @item
- @uref{http://www.cliki.net/trivial-garbage?v=20,trivial-garbage}
- Not apt-get installable, but please grab the tarball from the project web page and install it as described:
- @example
- # cd /usr/share/common-lisp/source
- # tar zxvf <path to trivial_garbage.tar.gz>
- # ln -sf $PWD/trivial-garbage_0.16/trivial-garbage.asd ../systems/
- @end example
- @end itemize
- @node Installation, A /dev/zero like translator, Dependencies, Top
- @chapter Installation
- To install the translator library, grab the Hurd source tree from the branch @code{flaviocruz-soc2008-lisp-branch} and build it:
- @example
- # cvs -z3 -d:pserver:anonymous@@cvs.savannah.gnu.org:/sources/hurd co -r flaviocruz-soc2008-lisp-branch hurd
- # mkdir hurd-build
- # cd hurd-build
- # ../hurd/configure --prefix=/usr --enable-lisp
- # make
- # make install
- @end example
- With everything in place I recommend you to compile everything before using the library. In the CLISP read-eval-print loop do:
- @example
- * (asdf:operate 'asdf:load-op 'tree-translator)
- @end example
- @node Using the provided translators, A /dev/zero like translator, Installation, Top
- @chapter Using the provided translators
- This library already provides some Lisp translators that can be used production wise.
- The more noteworthy translators are as follow:
- @itemize @bullet
- @item unzip
- This translator will sit on a zip file node and expose the file contents as a traditional directory, where the packed files can be read and write as any other file in the system.
- To startup this translator
- @example
- $ settrans -a file.zip /hurd/unzip.lisp
- @end example
- If you change the contents of an archived file the translator, when shutdown, will zip the original file, saving the new modifications along the way.
- @item ircfs
- The ircfs translator is a simple IRC client that enables you to listen for private and channel messages, sending messages, join channels and kick people, among others. Everything using your beloved file system interface.
- To launch this translator, you should type:
- @example
- $ settrans -ac irc /hurd/irc.lisp your_nickname irc_server
- $ ls irc/
- notice
- @end example
- The first file the translator will create, is the @file{notice} file, it contains the server notices and messages.
- Also, you may want to join a channel:
- @example
- $ mkdir hurd
- $ ls hurd/
- conversation kick topic users
- @end example
- Each channel has four files:
- @itemize @bullet
- @item conversation
- Contains the channel's conversation log, for instance you can @command{tail -f conversation} to wait for new messages. To send a channel message, @command{echo "hello" > conversation}.
- @item kick
- Use this file to kick people on the channel @command{echo "user_you_dont_like" > kick}.
- @item topic
- Contains the channel topic.
- @item users
- Contains a list of the channel's users.
- @end itemize
- To part from a channel just remove the channel directory.
- To make private conversations:
- @example
- $ pwd
- .../irc
- $ touch nick
- @end example
- That @file{nick} file will act just like a channel's @file{conversation} file.
- @item mux
- The mux translator acts like a directory organizer. For example, you may want to organize your mp3 files in a per artist fashion, for that purpose you should write a script that outputs the mp3 artist and a file that lists all the files you want to classify, and then the translator will create virtual directories and proxies for the real files.
- @example
- $ settrans -ac organized /hurd/mux.lisp list-of-mp3s.txt ./classify-mp3.sh
- @end example
- @end itemize
- @node A /dev/zero like translator, A more complex example, Using the provided translators, Top
- @chapter A /dev/zero like translator
- Now that we can really starting doing some interesting things, we will start with a basic translator that can easily introduce ourselves to important concepts and at the same time don't be overly complex.
- The translator we will be implementing is very similar to the @file{/dev/zero} file that probably any Unix like system implements. This file ignores everything that is written to it and when it is read it only gives us zeroes. It's also very similar to the @uref{www.gnu.org/software/hurd/hacking-guide/hhg.html,HHG's} hurd-one.c example.
- Before getting into actual code we will properly package the new translator, using the ASDF method. Let's create a file named @file{zero-translator.asd} with the following content:
- @lisp
- (defpackage #:zero-translator-asd
- (:use :cl :asdf))
- (in-package :zero-translator-asd)
- (defsystem zero-translator
- :name "zero-translator"
- :version "0.0.0"
- :maintainer "Your name"
- :author "Your name"
- :license "GPL v3.0"
- :description "/dev/zero translator."
- :depends-on (:hurd-translator)
- :components ((:file "zero")))
- @end lisp
- As you can see, this ASDF package has only one file (@file{zero.lisp}) and only depends on the hurd-translator ASDF package, which is the basic translator library that we will be using. If you need to use more ASDF packages please declare them here.
- Now to the @file{zero.lisp} file. At the top we put:
- @lisp
- (defpackage :zero-translator
- (:use :cl :hurd-common :mach
- :hurd :hurd-translator))
- (in-package :zero-translator)
- @end lisp
- This basically tells the Lisp system to create a new package which uses the Hurd translator library (hurd-common, mach, hurd, hurd-translator and tree-translator packages are all from this library) and then sets the currently used package to the zero-translator package.
- Now inside the new package, we must define a new @code{translator} class, so that we can customize the translator methods to this new class. These translator methods act like hooks/callbacks that run whatever some request is made upon the server.
- @lisp
- (defclass zero-translator (translator)
- ()
- (:documentation "The zero-translator."))
- @end lisp
- This definition is remarkably simple, it just tells that the @code{translator} class is the base class of the @code{zero-translator} class and doesn't add any new slots.
- If you want to see how the translator class is defined please look at the file @file{translator/class.lisp}. The important slots stored on this class follow:
- @itemize @bullet
- @item
- underlying-node
- It's the port to the underlying node where the translator is set. You can execute RPC's on it to request some information you need. For RPCs look at @file{hurd/io/}, @file{hurd/fs/} and @file{hurd/fsys}.
- @item
- root
- It's simply the translator's root node. If the translator simply exposes a new regular file this represents that file, if it's a directory it will refer to a directory node.
- @item
- statfs
- Contains file system meta data that should be set by the translator writer. Look at @file{common/statfs.lisp} for methods you can use on this object.
- @item
- storage
- Type of storage for this file system, defaults to :memory. Look at @file{hurd/fs/storage.lisp} for more types.
- @item
- options
- Currently used file system options that can be set using the fsysopts utility. It's a translator-options object and it's implemented in @file{hurd/translator-options.lisp}.
- @item
- name and version
- Translator name and 3 number list with version. It's used in request to the io-server-version RPC.
- @end itemize
- Now, you also must know what kinds of methods can be applied to a translator object, so that you can get the desired translator behavior. All these callbacks are extensively documented and described in the file @file{translator/api.lisp} along with some good default implementations.
- If you want to dig at a lower level, it's also possible to define how core RPC's like fsys-getroot and dir-lookup work, specializing the do-fsys-getroot and do-dir-lookup (look at @file{translator/interfaces/fsys-getroot.lisp} and @file{translator/interfaces/dir-lookup.lisp}, respectively).
- And if these methods don't satisfy your needs, you can even replace the lower level callbacks (implemented at @file{translator/interfaces/}), but be aware that you must deal with nasty things, like pointers, memory, etc and you must also be knowledgeable in using CFFI to deal with foreign calls.
- Alright, let's forget about that and get to the good stuff!
- Now that you have some basic understanding how everything fits together, we should begin implementing our first translator method: @code{make-root-node}.
- This method is only called right at the beginning of execution and it wants us to create a new 'node' object that will represent the initial translator file, which can be anything: a regular file, directory, character device, etc.
- To define translator methods we use the define-callback macro, which has following syntax:
- @lisp
- (define-callback <method-name> <translator-type> <argument list> <body>)
- @end lisp
- Which is transformed (as you may be guessing) into this:
- @lisp
- (defmethod <method-name> ((translator <translator-type>) <argument list>)
- <body>)
- @end lisp
- As you noted, the translator argument is always present in translator methods! It represents your yet to be created, translator object, of the class @code{zero-translator}. Apart from that 'hidden' argument, you can also use the *translator* dynamic variable that references the exact same object, but can be accessed anytime and anywhere you want.
- Before implementing the @code{make-root-node} callback, you need some basic knowledge of the @code{node} class. It's implemented in the file @file{translator/node.lisp} and has the following slots:
- @itemize @bullet
- @item
- owner
- File owner. Indicates the owner process ID and can be changed through the io-mod-owner RPC. Defaults to 0.
- @item
- stat
- This is a very important slot. It represents the node meta data and it's the same thing as the C @code{struct stat}.
- You should really know how to retrieve and change the @code{stat} information. Please look at @file{common/stat.lisp} to have a basic idea. Also note that the @code{stat} meta data also contains a mode object, which itself, describes node permissions, file type, among other things. Look at @file{common/mode.lisp} to see what methods are available to you.
- Important note: every method applicable to a @code{mode} object can also be directly applied to a @code{stat} object.
- @item
- box
- Translator box, can contain the current passive or/and active translator set on this node.
- @item
- nusers
- Number of users using this node. When it drops to 0 the method @code{report-no-users} is called and when it gets from the 'no users' state to a new user, the method @code{report-new-user} is called.
- @item
- link
- Contains a string indicating the file this node symbolic links to. Only makes sense when the node mode type is a link.
- @end itemize
- Alright, here is the @code{make-root-node} method:
- @lisp
- (define-callback make-root-node zero-translator
- (underlying-node underlying-stat)
- (declare (ignore underlying-node))
- (let ((mode (make-mode :perms '((:owner :read :write)
- (:group :read :write)
- (:others :read :write))
- :type :chr)))
- (make-instance 'node
- :stat (make-stat underlying-stat
- :mode mode))))
- @end lisp
- The first thing we must notice here are the arguments. This method accepts the underlying node port and @code{underlying-stat}, which refers to the @code{stat} meta data from the underlying node where the translator is being set.
- The first thing we do, is creating a sane @code{mode} meta data, indicating the types of permissions we want for the node and then the file type, in this case character device @code{:chr}. Other possible types are:
- @itemize @bullet
- @item
- :dir
- Directory.
- @item
- :chr
- Character device.
- @item
- :blk
- Block device.
- @item
- :reg
- Regular file.
- @item
- :lnk
- Symbolic link.
- @item
- sock
- Socket.
- @end itemize
- With the mode object created, we then create a @code{node} instance, indicating the @code{stat} object we want for that node, with @code{make-stat}.
- @code{make-stat} accepts an old @code{stat} object, and so the meta data will be copied to the new @code{stat} object. It means that this new @code{stat} object will be equal to @code{underlying-stat}, except the mode object which will be the node we created early. @code{make-stat} can also accept other arguments, please see @file{common/stat.lisp}.
- In the end, the newly created node is returned and put in the @code{root-node} translator slot, so that you can access it later on, if you need.
- What else needs to be implemented? :-) Ah, file writing!
- File writing is pretty simple on this translator, we only need to ignore everything we get. The method that deals with file writing is @code{write-file}.
- @lisp
- (define-callback write-file zero-translator
- (node user offset stream amount)
- (declare (ignore translator offset amount))
- (when (has-access-p node user :write)
- ; Empty the stream to look like we used it all.
- (loop while (read-byte stream nil))
- t))
- @end lisp
- Here, we can see that @code{write-file} gets lots of arguments, which I will explain:
- @itemize @bullet
- @item
- node
- It's the node the user wants to write in.
- @item
- user
- The user making the write request. It's an @code{iouser} object contain the @code{uid} and @code{gid} sets from that user. Implemented at @file{hurd/iohelp/iouser.lisp}.
- @item
- offset
- The place in the file the user wants to start writing to.
- @item
- stream
- An input byte stream containing the data to be wrote, you can used @code{read-byte to get the data}, or @code{read-sequence}.
- @item
- amount
- Amount of bytes present in this stream. Very useful if you want to put data into an array:
- @lisp
- (let ((my-array (make-array amount :element-type '(unsigned-byte 8))))
- (read-sequence my-array stream))
- @end lisp
- You can even convert it to string form if you need to:
- @lisp
- (octets-to-string my-array)
- @end lisp
- @end itemize
- How can the library know how much has been wrote? Well, simple looking at how much you read from the stream. So, if you read everything from the stream, the library will get the idea that everything was used. Finally we return T meaning success.
- How about file reading? @code{read-file} is the method for reading nodes.
- @lisp
- (define-callback read-file zero-translator
- (node user start amount stream)
- (declare (ignore translator start))
- (when (has-access-p node user :read)
- (loop for i from 0 below amount
- do (write-byte 0 stream))
- t))
- @end lisp
- The arguments, explained:
- @itemize @bullet
- @item
- node
- The node where the user wants to read.
- @item
- user
- The user making the request.
- @item
- start
- The file position the user wants to start reading at.
- @item
- amount
- Amount of data the user wants to read. You don't need to provide that amount of data, only what you have or want deliberately.
- @item
- stream
- Output stream were byte data should be written to.
- @end itemize
- As we said earlier, the translator should give back only zeroes to read requests, because of that we simply write 'amount' zeroes to the output stream, and then return T signaling success.
- What's missing? Ah! Start-up code:
- @lisp
- (defun main ()
- (run-translator (make-instance 'zero-translator
- :name "zero-translator")))
- (main)
- @end lisp
- Here we just create a new zero-translator instance and then run it. Boring, isn't it?
- And that's it! Let's wrap everything with a new file: @file{run-zero.lisp}:
- @example
- #!/usr/bin/run-lisp-trans
- (asdf:operate 'asdf:load-op 'zero-translator)
- @end example
- Now we can run the translator:
- @example
- $ chmod +x run-zero.lisp
- $ settrans -ac zero ./run-zero.lisp
- @end example
- And then you verify that it's really a character device:
- @example
- $ ls -l zero
- crw-rw-rw- 2 root root 0, 0 Aug 14 15:09 zero
- @end example
- You can also do some tests. Run a CLISP instance and do this:
- @example
- * (asdf:operate 'asdf:load-op 'hurd)
- * (use-package :hurd)
- * (defvar *p* (file-name-lookup "zero" :flags '(:read :write)))
- * (io-read *p* :amount 5)
- #(0 0 0 0 0)
- * (io-write *p* #(1 2))
- 2
- * (use-package :mach)
- * (port-deallocate *p*)
- T
- @end example
- As expected.
- Now, remove the translator:
- @example
- $ settrans -g zero
- @end example
- @node A more complex example, Conclusion, A /dev/zero like translator, Top
- @chapter A more complex example
- Now we'll try to implement a more complex translator example.
- The translator we are going implement will provide directory listing and the traditional file creation, writing, etc.
- At first it should create a directory structure mirroring Lisp lists. Here's an example:
- @lisp
- (:dir "root"
- (:file "a" "abcdefghijklmnopqrstuvwxyz")
- (:file "b" "123456789012345678901234567890")
- (:file "c" "")
- (:file "d" "456")
- (:dir "dir1"
- (:file "k" "1a2b3c")
- (:file "a" "")
- (:file "b" "a")
- (:file "c" "c"))
- (:dir "dir2")
- (:link "f" "c")
- (:dir "dir3"
- (:file "a" "12345"))
- (:link "g" "g") ; circular link
- (:link "h" "/usr"))
- @end lisp
- Our translator node must have a directory named "root" and then some files and links on it. The third list argument for regular files describe file contents and the third argument for symbolic links describe the link target.
- First, let's describe this new translator class (we'll name it @code{test-translator}):
- @lisp
- (defclass test-translator (tree-translator)
- ((file-stat :initarg :file-stat
- :initform nil
- :accessor file-stat)
- (dir-stat :initarg :dir-stat
- :initform nil
- :accessor dir-stat)))
- @end lisp
- You might be wondering what's up with that @code{tree-translator} business. We'll, the tree-translator is a special class of translators that do some work for us. The @code{tree-translator} has the special ability of implementing all the directory lookup and listing automatically, without the need for us to do that. If you want to see what methods are implemented please see @file{tree-translator/class.lisp}.
- It also extends the base node class with two new node types:
- @itemize @bullet
- @item
- entry
- Is a node with the information about the parent directory.
- @item
- dir-entry
- Is an @code{entry} and also contains the directory entries.
- @end itemize
- Please check the file @file{tree-translator/dir.lisp} for methods on directories.
- Some important meta data is also managed by this type of translator, specially the st-nlink and st-ino fields for the @code{stat} object.
- In our @code{test-translator} we put the @code{file-stat} and @code{dir-stat} slots. They act like a template meta data for new nodes.
- Another thing you should take note is that the previously used method @code{make-root-node} is already implemented for us in @code{tree-translator}, in addition, it creates a new method named @code{fill-root-node} with the root node as an argument. In this method you should fill your root directory with the stuff you want.
- For this case, this might be implemented like this:
- @lisp
- (define-callback fill-root-node test-translator
- ((node dir-entry))
- (setf (file-stat translator)
- (make-stat (stat node)
- :mode (make-mode :perms '((:owner :read)
- (:group :read)))
- :type :reg)
- (dir-stat translator)
- (make-stat (stat node)
- :mode (make-mode :perms '((:owner :read :exec)
- (:group :read :exec)))
- :type :dir))
- (%fill-node translator (with-open-file (s +file+)
- (read stream))
- node))
- @end lisp
- As you can see it defines the translator slots with fresh meta data templates and then executes the function @code{%fill-node} with these arguments: the translator, the directory structure (as read from the @code{+file+}) and the root node.
- @lisp
- (defun %fill-node (translator ls node)
- (let ((type (first ls))
- (name (second ls))
- (args (rest (rest ls))))
- (case type
- (:dir
- (let ((dir (make-instance 'dir-entry
- :stat (make-stat (dir-stat translator))
- :parent node)))
- (add-entry node dir name)
- (loop for item in args
- do (%fill-node translator item dir))))
- (:file
- (let* ((data (first args))
- (file (make-instance 'test-entry
- :stat (make-stat
- (file-stat translator)
- :size (length data))
- :parent node
- :data (%read-file-data data))))
- (add-entry node file name)))
- (:link
- (let ((target (first args))
- (new (make-instance 'entry
- :stat (make-stat
- (file-stat translator)
- :type :lnk)
- :parent node)))
- (setf (link new) target)
- (add-entry node new name))))))
- @end lisp
- This is a recursive function. It checks the requested file type, creates the specific node with that type and then puts it in the directory as a new directory entry.
- You should always give the :parent argument when you are creating new @code{entry}'s.
- Take special attention for the symbolic links case "@code{(setf (link new) target)}". It defines the link (as a string) this node points to, but it also sets the file type to :lnk, even if it was not specified in @code{make-stat}.
- You may also be wondering what's that @code{test-entry} class, well, it's just a new kind of @code{node}.
- @lisp
- (defclass test-entry (entry)
- ((contents :initarg :data
- :initform (%create-data-array 0 nil)
- :accessor data)))
- @end lisp
- In this case we add a new slot that contains the file contents. And what does @code{%create-data-array} do? It just creates a new adjustable array:
- @lisp
- (defun %create-data-array (size contents)
- (make-array size
- :initial-contents contents
- :adjustable t
- :fill-pointer t
- :element-type '(unsigned-byte 8)))
- @end lisp
- You can use the CLOS dispatch system to specialize node behaviors, for example, you can have various node types that react differently to the @code{read-file} method. For example, one node type would always return the data in upper case and in some others all in lower case.
- As for that @code{%read-file-data} function, here it is:
- @lisp
- (defun %read-file-data (str)
- (%create-data-array (length str)
- (loop for char across str
- collect (char-code char))))
- @end lisp
- It takes the data string and converts it to an @code{unsigned-byte} array.
- Now that the directory structure is being created, you need to start exposing the file contents with @code{read-file} (just like in the first example). Please also keep in mind that you should always keep the @code{st-size stat} field updated after a refresh-node method call. Sometimes the translator clients want to know how much data there is to read, and the file size is an important factor in this case, when it's being compared to an internally saved file offset.
- @lisp
- (define-callback read-file test-translator
- (node user start amount stream)
- (when (has-access-p node user :read)
- (let* ((size (stat-get (stat node) 'st-size))
- (size-res (- size start)))
- (unless (plusp size-res)
- (return-from read-file t))
- (let* ((total (min size-res amount))
- (end (+ start total)))
- (write-sequence (subseq (data node) start end)
- stream)
- t))))
- @end lisp
- In the @code{read-file} method we check if the user has read access to that node and then calculate how much data there is left to read from the 'start' position. When we know that, we just write a sub sequence of the file contents to the output stream and then return T to report success.
- Another good thing to provide in translators (specially those with write support) is file truncating. For that, the library has the @code{file-change-size} method.
- @lisp
- (define-callback file-change-size test-translator
- (node user new-size)
- (when (is-dir-p (stat node))
- (return-from file-change-size :is-a-directory))
- (when (has-access-p node user :write)
- (adjust-array (data node) new-size :fill-pointer t)
- (setf (stat-get (stat node) 'st-size) new-size)
- t))
- @end lisp
- First we verify if the file is a directory, and then if the requesting user has write access. When these tests are passed we can just adjust the file contents array and define a new size for that file in the @code{stat} object.
- And now for file writing!
- @lisp
- (defun %read-sequence (stream amount)
- (let ((arr (make-array amount
- :element-type '(unsigned-byte 8))))
- (read-sequence arr stream)
- arr))
- (define-callback write-file test-translator
- (node user offset stream amount)
- (unless (has-access-p node user :write)
- (return-from write-file nil))
- (when (is-dir-p (stat node))
- (return-from write-file :is-a-directory))
- (let* ((size (stat-get (stat node) 'st-size))
- (arr (%read-sequence stream amount))
- (final-size (max (+ amount offset) size)))
- (unless (= final-size size)
- (adjust-array (data node)
- final-size
- :fill-pointer t))
- (replace (data node) arr :start1 offset)
- ; Update stat size.
- (setf (stat-get (stat node) 'st-size) final-size)
- t))
- @end lisp
- Well, let's see. We first do some boring tests and then we read all data from the stream with @code{%read-sequence}, after that we need to know how much data the file will now have, and then we have three possible situations: the data will just be replaced, the file will get new data, and both. Anyway, we simply just need to know the maximum of the current size and the offset plus the new data.
- After knowing the final size and if the file will grow we adjust the array according to this new size. Then simply replace the current array data with the stream data and update the file size.
- Now, you'll probably want to support file creation, for that, you must implement the @code{create-file} method.
- @lisp
- (define-callback create-file test-translator
- (node user filename mode)
- (unless (has-access-p node user :write)
- (return-from create-file nil))
- (let ((entry (make-instance 'test-entry
- :stat (make-stat (stat node)
- :mode mode
- :size 0)
- :parent node)))
- (add-entry node entry filename)
- entry))
- @end lisp
- The create-file method has 4 arguments:
- @itemize @bullet
- @item
- node
- The directory where the user wants to create the file.
- @item
- user
- The user creating the file.
- @item
- filename
- The new file name.
- @item
- mode
- Permission bits for that file.
- @end itemize
- As you can see, we just create a new @code{test-entry} instance with @code{node} as the parent directory and then we add this new node to the parent directory as a new directory entry.
- How about hard link creation? It's already implemented by default in the @code{tree-translator}. It works by simply using the same node reference and adding a new point of access using a new filename.
- And symbolic link creation? Three methods must be implemented for it to work:
- @itemize @bullet
- @item
- @code{allow-link-p}
- Allows link reading, implemented by default by the @code{translator} class.
- @item
- @code{create-symlink}
- Turns a node into a symlink, implemented by default by the @code{translator} class.
- @item
- @code{create-anonymous-file}
- Not implemented. It must create a detached node. Detached means that file will have a parent directory but will not be present when listed, and it will exist as a Lisp object that will be hard linked to a new symlink node. See @file{sysdeps/mach/hurd/symlink.c} from glibc's source code for more insights and reasons for this.
- @end itemize
- Here's a simple implementation for @code{create-anonymous-file}:
- @lisp
- (define-callback create-anonymous-file test-translator
- (node user mode)
- (when (can-modify-dir-p node user)
- (make-instance 'test-entry
- :stat (make-stat (stat node)
- :mode mode)
- :parent node)))
- @end lisp
- Here @code{node} represents the parent directory and @code{mode} the permission bits.
- How about the start-up code? First we need to get the file from the arguments that will be passed to our program when it is launched. This file contains the mentioned directory structure.
- @lisp
- (unless (= (length ext:*args*) 1)
- (error "You must provide one argument with a spec file."))
- (defconstant +file+ (first ext:*args*))
- @end lisp
- Then, the usual main function:
- @lisp
- (defun main ()
- (let ((translator
- (make-instance 'test-translator
- :name "test-translator"
- :version (list 1 2 3))))
- (run-translator translator)))
- (main)
- @end lisp
- Please see @file{examples/test.lisp}, @file{examples/test-translator.asd} and @file{examples/run-test.lisp} for the complete example.
- To see it running:
- @example
- $ settrans -ac foo ./run-test.lisp data/test.lisp
- $ cd foo
- $ ls
- root
- $ cd root
- $ ls -l
- total 40
- -r--r----- 1 root root 26 Aug 14 19:13 a
- -r--r----- 1 root root 30 Aug 14 19:13 b
- -r--r----- 1 root root 0 Aug 14 19:13 c
- -r--r----- 1 root root 3 Aug 14 19:13 d
- dr-xr-x--- 6 root root 0 Aug 14 19:13 dir1
- dr-xr-x--- 2 root root 0 Aug 14 19:13 dir2
- dr-xr-x--- 3 root root 0 Aug 14 19:13 dir3
- lr--r----- 1 root root 1 Aug 14 19:13 f -> c
- lr--r----- 1 root root 1 Aug 14 19:13 g -> g
- lr--r----- 1 root root 4 Aug 14 19:13 h -> /usr
- @end example
- @node Conclusion,, A more complex example, Top
- @chapter Conclusion
- Now that you got some taste of what's like to program Lisp translators, I invite you to read more complex translator examples that are present in the package, namely the zip and irc translators.
- Happy hacking!
- @bye
|