Going to propose an answer to my own question here, that I think better fits what I was trying to do. This method really makes no file (no temp file).
Since ZipFile extends, and is really just a bunch of convenience methods around ZipCentralDirectory, you can work directly with ZipCentralDirectory instead of ZipFile. That will alow you to use IO streams for creating and writing a zip file. Plus throw in use of StringIO and you can do it from a string:
# load a zip file from a URL into a string
resp = Net::HTTP.new("www.somewhere.com", 80).get("/some.zip")
zip_as_string = response.body
# open as a zip
zip = Zip::ZipCentralDirectory.read_from_stream(StringIO.new(zip_as_string))
# work with the zip file.
# i just output the names of each entry to show that it was read correctly
zip.each { |zf| puts zf.name }
# write zip back to an output stream
out = StringIO.new
zip.write_to_stream(out)
# use 'out' or 'out.string' to do whatever with the resulting zip file.
out.string
Update:
This actually doesn't work at all. It will write a readable zip file, but ONLY the zip file's 'table of contents'. All the internal files are 0 length. Digging further into the Zip implementation, it looks like it only holds the zip entry 'metadata' in memory, and it goes back to the underlying file to read everything else. Based on this, it looks like it is impossible to use the Zip implementation at all without writing to the filesystem.