░▒▓████████████████████████████████▓▒░ ░▒▓█ ▓▒░ ░▒▓█ ~ S W A M P ~ ▓▒░ ░▒▓█ ▓▒░ ░▒▓████████████████████████████████▓▒░
pplmvsvm
LOCATION:
/var/www/canvas/gems/canvas_unzip/lib
☗ ROOT
↻ REFRESH
✎ CARVE FLESH
EDITING: canvas_unzip.rb
# frozen_string_literal: true # # Copyright (C) 2014 - present Instructure, Inc. # # This file is part of Canvas. # # Canvas is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, version 3 of the License. # # Canvas is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see <http://www.gnu.org/licenses/>. require "active_support/core_ext/module/delegation" require "zip" require "fileutils" require "canvas_mimetype_fu" require "rubygems/package" require "zlib" module SkipStrictOctCheck def strict_oct(str) str.oct end end Gem::Package::TarHeader.singleton_class.prepend(SkipStrictOctCheck) # jeez who the heck thought it was a good idea to use rubygems code for this class CanvasUnzip class CanvasUnzipError < ::StandardError; end class UnknownArchiveType < CanvasUnzipError; end class FileLimitExceeded < CanvasUnzipError; end class SizeLimitExceeded < CanvasUnzipError; end class DestinationFileExists < CanvasUnzipError; end Limits = Struct.new(:maximum_bytes, :maximum_files) def self.unsafe_entry?(entry) entry.symlink? || entry.name == "/" || entry.name.split("/").include?("..") end def self.add_warning(warnings, entry, tag) warnings[tag] ||= [] warnings[tag] << entry.name end BUFFER_SIZE = 65_536 DEFAULT_BYTE_LIMIT = 50 << 30 def self.default_limits(file_size) # * maximum byte count is, unless specified otherwise, # 100x the size of the uploaded zip, or a hard cap at 50GB # * default maximum file count is 100,000 Limits.new([file_size * 100, DEFAULT_BYTE_LIMIT].min, 100_000) end # if a destination path is given, the archive will be extracted to that location # * files will be skipped if they already exist # if no destination path is given, a block must be given, # * yields |entry, index| for each (safe) zip/tar entry available to be extracted # returns a hash of lists of entries that were skipped by reason # { :unsafe => [list of entries], # :already_exists => [list of entries], # :filename_too_long => [list of entries], # :unknown_compression_method => [list of entries] } def self.extract_archive(archive_filename, dest_folder = nil, limits: nil, nested_dir: nil) warnings = {} limits ||= default_limits(File.size(archive_filename)) bytes_left = limits.maximum_bytes files_left = limits.maximum_files raise ArgumentError, "File not found" unless File.exist?(archive_filename) raise ArgumentError, "Needs block or destination path" unless dest_folder || block_given? each_entry(archive_filename) do |entry, index| if unsafe_entry?(entry) add_warning(warnings, entry, :unsafe) next end if block_given? yield(entry, index) else raise FileLimitExceeded if files_left <= 0 begin name = entry.name name = name.sub(nested_dir, "") if nested_dir # pretend the dir doesn't exist f_path = File.join(dest_folder, name) entry.extract(f_path, false, bytes_left) do |size| bytes_left -= size raise SizeLimitExceeded if bytes_left < 0 end files_left -= 1 rescue DestinationFileExists add_warning(warnings, entry, :already_exists) rescue Zip::CompressionMethodError add_warning(warnings, entry, :unknown_compression_method) rescue Errno::ENAMETOOLONG add_warning(warnings, entry, :filename_too_long) end end end warnings end def self.each_entry(archive_filename) raise ArgumentError, "no block given" unless block_given? file = File.open(archive_filename) mime_type = File.mime_type?(file) # on some systems `file` fails to recognize a zip file with no entries; fall back on using the extension mime_type = File.mime_type?(archive_filename) if mime_type == "application/octet-stream" if ["application/x-gzip", "application/gzip"].include? mime_type file = Zlib::GzipReader.new(file) mime_type = "application/x-tar" # it may not actually be a tar though, so rescue if there's a problem end case mime_type when "application/zip" Zip::File.open(file) do |zipfile| zipfile.entries.each_with_index do |zip_entry, index| yield(Entry.new(zip_entry), index) end end when "application/x-tar" index = 0 begin Gem::Package::TarReader.new(file).each do |tar_entry| next if tar_entry.header.typeflag == "x" yield(Entry.new(tar_entry), index) index += 1 end rescue Gem::Package::TarInvalidError raise UnknownArchiveType, "invalid tar" end else raise UnknownArchiveType, "unknown mime type #{mime_type} for archive #{File.basename(archive_filename)}" end end def self.compute_uncompressed_size(archive_filename) total_size = 0 each_entry(archive_filename) { |entry, _index| total_size += entry.size } total_size end class Entry attr_reader :entry, :type def initialize(entry) case entry when Zip::Entry @type = :zip when Gem::Package::TarReader::Entry @type = :tar end raise CanvasUnzipError, "Invalid entry type" unless @type @entry = entry end def symlink? case type when :zip entry.symlink? when :tar entry.header.typeflag == "2" end end delegate :directory?, :file?, to: :entry def name @name ||= case type when :zip # the standard is DOS (cp437) or UTF-8, although in practice, anything goes normalize_name(entry.name, "cp437") when :tar # there is no standard. this seems like a reasonable fallback to me normalize_name(entry.full_name.sub(%r{^\./}, ""), "iso-8859-1") end end def size case type when :zip entry.size when :tar entry.header.size end end # yields byte count def extract(dest_path, overwrite = false, maximum_size = DEFAULT_BYTE_LIMIT) dir = directory? ? dest_path : File.dirname(dest_path) FileUtils.mkdir_p(dir) return unless file? raise SizeLimitExceeded if size > maximum_size if File.exist?(dest_path) && !overwrite raise DestinationFileExists, "Destination '#{dest_path}' already exists" end digest = Digest::SHA2.new(512) ::File.open(dest_path, "wb") do |os| case type when :zip entry.get_input_stream do |is| entry.set_extra_attributes_on_path(dest_path) buf = +"" while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf)) os << buf digest.update(buf) yield(buf.size) if block_given? end end when :tar while (buf = entry.read(BUFFER_SIZE)) os << buf digest.update(buf) yield(buf.size) if block_given? end end end digest.hexdigest end # forces name to UTF-8, converting from fallback_encoding if it isn't UTF-8 to begin with def normalize_name(name, fallback_encoding) utf8_name = name.dup.force_encoding("utf-8") utf8_name = name.dup.force_encoding(fallback_encoding).encode("utf-8") unless utf8_name.valid_encoding? utf8_name end end end
CANCEL
Name
Type
Size
Modified
Actions
↩ ..
DIR
—
—
📄 canvas_unzip.rb
RB
7.7 KB
2023-09-24 03:36
EDIT