frafferz/geek

# This is a comment

Method in the Madness: Ensure and Return

Was pondering the question: what code runs when method level rescue, else and ensure are used in ruby?

TL;DR summary

1
2
3
4
5
6
7
8
9
def some_method
# main body
rescue
# rescue code
else
# alternative to rescue
ensure
# always run me last
end
  1. Without return the last computed value that is not in the ensure block is returned (this will either be the main body, the rescue block or the else block).
  2. Using return in the main body of the method means that else block doesn’t run.
  3. Using return in an ensure block always overrides any other value returned by the method, regardless of whether any other section of the method also used the return keyword.
  4. Values from an ensure block are only ever returned when the return keyword is used.

Simple function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def value(sym)
puts("ran #{sym}")
sym
end
def fn1
value(:fn1)
rescue
value(:rescue1)
else
value(:else1)
ensure
value(:ensure1)
end
fn1() #=> :else1

Output:

ran fn1
ran else1
ran ensure1

Function with error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def value(sym)
puts("ran #{sym}")
sym
end
def fn2
raise value(:fn2)
rescue
value(:rescue2)
else
value(:else2)
ensure
value(:ensure2)
end
fn2() #=> :rescue2

Output:

ran fn2
ran rescue2
ran ensure2

Function with return in main body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def value(sym)
puts("ran #{sym}")
sym
end
def fn3
return value(:fn3)
rescue
value(:rescue3)
else
value(:else3)
ensure
value(:ensure3)
end
fn3() #=> :fn3

Output:

ran fn3
ran ensure3

Function with return in main body and return in ensure block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def value(sym)
puts("ran #{sym}")
sym
end
def fn4
return value(:fn4)
rescue
return value(:rescue4)
else
return value(:else4)
ensure
return value(:ensure4)
end
fn4() #=> :ensure4

Output:

ran fn4
ran ensure4

Function with return in main body and in ensure and error raised

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def value(sym)
puts("ran #{sym}")
sym
end
def fn5
return raise value(:fn5)
rescue
return value(:rescue5)
else
return value(:else5)
ensure
return value(:ensure5)
end
fn5() #=> :ensure5

Output:

ran fn5
ran rescue5
ran ensure5

Passenger Standalone on Ubuntu 11.10 (Oneiric Ocelot)

Until issue 704 is resolved, Passenger Standalone won’t compile properly on Ubuntu 11.10 (Oneirc Ocelot - currently pre-release) using the default settings.

To work around this, use GCC 4.4 instead. You’ll need to install gcc-4.4 and libstdc++6-4.4-dev and then specify GCC 4.4 at compile time using the CC environment variable.

1
2
$ sudo apt-get install gcc-4.4 libstdc++6-4.4-dev
$ CC=gcc-4.4 passenger start

Hopefully this will help anyone else who’s updated to the latest Ubuntu pre-release and still wants to use Passenger Standalone.

Playing With Zip - Russian Dolls

The standard answer is that zip files can’t contain more than one copy of a file without containing more than one copy of a file. In other words, there’s not a portable version of a *nix style hard link.

And that’s kind of true. However it is theoretically possible to create valid zip files that violate this principle in a platform independant manner. Unfortunately this doesn’t work properly with Stuffit :(

The data for a file entry must start immediately following the header, but the header can be upto ~65k and ends with fields that should be ignored if they are not understood. So we can stuff a local file header inside the end of a parent local file header (and prefix 32 bytes of “unknown” extra field) so that we have two valid local file headers that each end immediately before the only copy of the file data, as pictured:

Stuffing headers inside headers like Russian Dolls

And then we add the entries to Central Directory as if they were normal file entries.

Tests work fine with Info-ZIP, 7-Zip and the Windows built-in zip support. Unfortunately Stuffit on OS X only appears to recognise the “normal” entries (ie. doesn’t extract the embedded headers).

Playing With Zip

I wanted to stream zip files with lots of JPEGs in. Hundreds of JPEGs from digital cameras and, being as they were JPEGs, didn’t really care about trying to compress them any further.

  1. I wanted to create (potentially) huge archives. So I’d need something that supported ZIP64 extensions.
  2. I wanted to mix local files and files streamed from internal web servers.
  3. I wanted to create a zip file on the fly, with minimal buffering, to minimize disk and memory requirements.
  4. I wanted to support large numbers of simultaneous downloads.
  5. I also wanted (if possible) to efficiently include the same file more than once in an archive with different filenames.
  6. I wanted to continue to use zip archives.

Simples?

If only.

Streaming ZIP64 support (or lack thereof)

There are several ruby zip libraries, e.g. rubyzip, zip-ruby and archive-zip - but they seem to fall into two camps: pure ruby with no ZIP64 or wrapping a C library (e.g. libzip) but with no obvious way to create a zip file and start streaming it before it’s complete.

So I indulged my NIH syndrome reflex and wrote zip64writer which streams zip files and can automatically starts using ZIP64 extensions when needed. (I did look at adding ZIP64 support to rubyzip, but I figured fairly quickly that it would be easier to roll a specifically targetted library than adapt it to my needs.)

So writing a zip file to a stream works something like:

1
2
3
4
5
6
7
8
9
10
11
require 'zip64/writer'
File.open("output.zip", "wb") do |fp|
Zip64::ZipWriter.new(fp) do |zip|
File.open("sample.jpg", "rb") do |rfp|
zip.add_entry(rfp,
:mtime => Time.now,
:name => 'myphoto.jpg')
end
end # Implicit close writes central directory to stream
end

ZIP64 extensions are extra header fields, and an extra couple of blocks at the end of the zip file, which allow zip files to contain more than 65,535 entries (the limit of a 16bit integer) & for the zip archives (and the files inside them) to be greater than 4 Gb (the limit of a 32bit integer) in size.

ASCII Art Diagram

The writer detects when an offset requires a 64bit integer (ie. offset >4Gb) and automatically starts using ZIP64 extensions - so the files are still as compatible as possible with old zip implementations that don’t support ZIP64 (e.g. Windows XP shell).

Basic testing reveals that ZIP64 files created this way (ie. a mix of standard encoding and ZIP64 encoding) work fine on Windows 7, OS X 10.5+. (Also the version of file-roller shipped with Lucid Lynx opens them fine, although the version of zip shipped with Hardy Heron is too old.)