Skip to content

Commit

Permalink
feat: support HarmonyOS package
Browse files Browse the repository at this point in the history
Signed-off-by: InjoyDeng <me@injoydeng.com>
  • Loading branch information
InjoyDeng committed Aug 5, 2024
1 parent 99cfc36 commit 9936afb
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 0 deletions.
34 changes: 34 additions & 0 deletions lib/app_info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require 'app_info/file'
require 'app_info/info_plist'
require 'app_info/mobile_provision'
require 'app_info/pack_info'

require 'app_info/apple'
require 'app_info/macos'
Expand All @@ -21,6 +22,10 @@
require 'app_info/apk'
require 'app_info/aab'

require 'app_info/harmonyos'
require 'app_info/happ'
require 'app_info/hap'

require 'app_info/proguard'
require 'app_info/dsym'

Expand All @@ -40,6 +45,8 @@ def parse(file)
when Format::IPA then IPA.new(file)
when Format::APK then APK.new(file)
when Format::AAB then AAB.new(file)
when Format::HAP then HAP.new(file)
when Format::HAPP then HAPP.new(file)
when Format::MOBILEPROVISION then MobileProvision.new(file)
when Format::DSYM then DSYM.new(file)
when Format::PROGUARD then Proguard.new(file)
Expand Down Expand Up @@ -96,6 +103,8 @@ def detect_zip_file(file)
return Format::AAB if aab_clues?(zip_file)
return Format::MACOS if macos_clues?(zip_file)
return Format::PE if pe_clues?(zip_file)
return Format::HAP if hap_clues?(zip_file)
return Format::HAPP if happ_clues?(zip_file)
return Format::UNKNOWN unless clue = other_clues?(zip_file)

clue
Expand Down Expand Up @@ -131,6 +140,31 @@ def pe_clues?(zip_file)
!zip_file.glob('*.exe').empty?
end

# :nodoc:
def hap_clues?(zip_file)
!zip_file.find_entry('pack.info').nil? && !zip_file.find_entry('module.json').nil?
end

# :nodoc:
def happ_clues?(zip_file)
pack_info_count = 0
hap_count = 0

zip_file.each do |f|
path = f.name

if path == 'pack.info'
pack_info_count += 1
elsif path.end_with?('.hap')
hap_count += 1
else
return false
end
end

pack_info_count == 1 && hap_count >= 1
end

# :nodoc:
def other_clues?(zip_file)
zip_file.each do |f|
Expand Down
24 changes: 24 additions & 0 deletions lib/app_info/const.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ module Format
AAB = :aab
PROGUARD = :proguard

# HarmonyOS

HAP = :hap
HAPP = :app

# Windows

PE = :pe
Expand All @@ -35,6 +40,7 @@ module Manufacturer
APPLE = :apple
GOOGLE = :google
MICROSOFT = :microsoft
HUAWEI = :huawei
end

# Platform
Expand All @@ -44,6 +50,7 @@ module Platform
ANDROID = :android
APPLETV = :appletv
WINDOWS = :windows
HARMONYOS = :harmonyos
end

# Device Type
Expand Down Expand Up @@ -77,6 +84,23 @@ module Google
AUTOMOTIVE = :automotive
end

module Huawei
# HarmonyOS Default
DEFAULT = :default
# HarmonyOS Phone
PHONE = :phone
# HarmonyOS Tablet
TABLET = :tablet
# HarmonyOS TV
TV = :tv
# HarmonyOS wearable
WEARABLE = :wearable
# HarmonyOS Car
CAR = :car
# HarmonyOS 2-in-1 tablet and laptop
TWO_IN_ONE = :two_in_one
end

module Microsoft
# Windows
WINDOWS = :windows
Expand Down
73 changes: 73 additions & 0 deletions lib/app_info/hap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

module AppInfo
# Parse HAP file parser
class HAP < HarmonyOS
# Full icons metadata
# @example
# ipa.icons
# # => [
# # {
# # name: 'icon.png',
# # file: '/path/to/icon.png',
# # uncrushed_file: '/path/to/uncrushed_icon.png',
# # dimensions: [64, 64]
# # },
# # {
# # name: 'icon1.png',
# # file: '/path/to/icon1.png',
# # uncrushed_file: '/path/to/uncrushed_icon1.png',
# # dimensions: [120, 120]
# # }
# # ]
# @return [Array<Hash{Symbol => String, Array<Integer>}>] icons paths of icons
def icons
@icons ||= icons_path.each_with_object([]) do |file, obj|
obj << {
name: ::File.basename(file),
file: file,
uncrushed_file: file,
dimensions: ImageSize.path(file).size
}
end
end

# @return [Array<String>]
def icons_path
@icons_path ||= [::File.join(contents, 'resources', 'base', 'media', 'app_icon.png')]
end

# @return [JSON]
def module_info
@module_info ||= JSON.parse(::File.read(module_info_path))
end

# @return [String]
def module_info_path
@module_info_path ||= ::File.join(contents, 'module.json')
end

# @return [String]
def name
# TODO: The application display name should be determined by looking up
# the value of the variable named in the "label" field of the "module.json"
# file within the "resources.index" file.
pack_info.bundle_name
end

def clear!
return unless @contents

FileUtils.rm_rf(@contents)

@pack_info = nil
@info_path = nil
@contents = nil

@module_info_path = nil
@module_info = nil
@icons_path = nil
@icons = nil
end
end
end
46 changes: 46 additions & 0 deletions lib/app_info/happ.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module AppInfo
# parser for HarmonyOS .APP file
class HAPP < HarmonyOS
def_delegators :default_entry, :icons
# @return [HAP]
def default_entry
hap_path = ::File.join(contents, "#{default_entry_name}.hap")
@default_entry ||= HAP.new(hap_path)
end

# @return [String]
def default_entry_name
return @default_entry_name if @default_entry_name

pack_info.packages.each do |package|
if package['moduleType'] == 'entry' && package['deliveryWithInstall']
@default_entry_name ||= package['name']
break
end
end
@default_entry_name
end

# @return [String]
def name
default_entry.name
end

def clear!
return unless @contents

FileUtils.rm_rf(@contents)

@pack_info = nil
@info_path = nil
@contents = nil

@default_entry_name = nil
@default_entry&.clear!

@default_entry = nil
end
end
end
65 changes: 65 additions & 0 deletions lib/app_info/harmonyos.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module AppInfo
# HarmonyOS base parser for hap and app file
class HarmonyOS < File
extend Forwardable
include Helper::HumanFileSize
include Helper::Archive

def_delegators :pack_info, :build_version, :release_version, :bundle_id

# return file size
# @example Read file size in integer
# aab.size # => 3618865
#
# @example Read file size in human readabale
# aab.size(human_size: true) # => '3.45 MB'
#
# @param [Boolean] human_size Convert integer value to human readable.
# @return [Integer, String]
def size(human_size: false)
file_to_human_size(@file, human_size: human_size)
end

# @return [Symbol] {Manufacturer}
def manufacturer
Manufacturer::HUAWEI
end

# @return [Symbol] {Platform}
def platform
Platform::HARMONYOS
end

# @return [Symbol] {Device}
def device
Device::Huawei::DEFAULT
end

# @return [PackInfo]
def pack_info
@pack_info ||= PackInfo.new(info_path)
end

# @return [String]
def info_path
@info_path ||= ::File.join(contents, 'pack.info')
end

# @return [String] unzipped file path
def contents
@contents ||= unarchive(@file, prefix: format.to_s)
end

# @abstract Subclass and override {#name} to implement.
def name
not_implemented_error!(__method__)
end

# @abstract Subclass and override {#clear!} to implement.
def clear!
not_implemented_error!(__method__)
end
end
end
52 changes: 52 additions & 0 deletions lib/app_info/pack_info.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require 'json'

module AppInfo
# HarmonyOS pack.info parser
class PackInfo < File

# @return [String]
def version_code
app['version']['code']
end
alias build_version version_code

# @return [String]
def version_name
app['version']['name']
end
alias release_version version_name

# @return [String]
def bundle_name
app['bundleName']
end
alias bundle_id bundle_name

# @return [JSON]
def app
@app ||= summary['app']
end

# @return [Array<JSON>]
def modules
@modules ||= summary['modules']
end

# @return [JSON]
def summary
@summary ||= content['summary']
end

# @return [Array<JSON>]
def packages
@packages ||= content['packages']
end

# @return [JSON]
def content
JSON.parse(::File.read(@file))
end
end
end
23 changes: 23 additions & 0 deletions spec/app_info/hap_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
describe AppInfo::HAP do
subject { AppInfo::HAP.new(file) }
after { subject.clear! }

context 'with valid HAP file' do
let(:file) { fixture_path('apps/harmony.hap') }

it { expect(subject.file).to eq file }
it { expect(subject.size).to eq(142283) }
it { expect(subject.size(human_size: true)).to eq('138.95 KB') }
it { expect(subject.format).to eq(AppInfo::Format::HAP) }
it { expect(subject.format).to eq(:hap) }
it { expect(subject.manufacturer).to eq(AppInfo::Manufacturer::HUAWEI) }
it { expect(subject.manufacturer).to eq(:huawei) }
it { expect(subject.platform).to eq(AppInfo::Platform::HARMONYOS) }
it { expect(subject.platform).to eq(:harmonyos) }
it { expect(subject.name).to eq('com.example.myapplication') }
it { expect(subject.bundle_id).to eq('com.example.myapplication') }
it { expect(subject.icons.length).not_to be_nil }
it { expect(subject.release_version).to eq('1.0.0') }
it { expect(subject.build_version).to eq(1000000) }
end
end
Loading

0 comments on commit 9936afb

Please sign in to comment.