2020-01-01 13:55:28 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
require "spec_helper"
|
|
|
|
|
2022-10-11 01:57:18 +05:30
|
|
|
RSpec.describe Gitlab::Git::Diff do
|
|
|
|
let_it_be(:project) { create(:project, :repository) }
|
|
|
|
let_it_be(:repository) { project.repository }
|
|
|
|
|
2018-12-05 23:21:45 +05:30
|
|
|
let(:gitaly_diff) do
|
|
|
|
Gitlab::GitalyClient::Diff.new(
|
|
|
|
from_path: '.gitmodules',
|
|
|
|
to_path: '.gitmodules',
|
|
|
|
old_mode: 0100644,
|
|
|
|
new_mode: 0100644,
|
|
|
|
from_id: '0792c58905eff3432b721f8c4a64363d8e28d9ae',
|
|
|
|
to_id: 'efd587ccb47caf5f31fc954edb21f0a713d9ecc3',
|
|
|
|
overflow_marker: false,
|
|
|
|
collapsed: false,
|
|
|
|
too_large: false,
|
|
|
|
patch: "@@ -4,3 +4,6 @@\n [submodule \"gitlab-shell\"]\n \tpath = gitlab-shell\n \turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n"
|
|
|
|
)
|
|
|
|
end
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
before do
|
|
|
|
@raw_diff_hash = {
|
|
|
|
diff: <<EOT.gsub(/^ {8}/, "").sub(/\n$/, ""),
|
|
|
|
@@ -4,3 +4,6 @@
|
|
|
|
[submodule "gitlab-shell"]
|
|
|
|
\tpath = gitlab-shell
|
|
|
|
\turl = https://github.com/gitlabhq/gitlab-shell.git
|
|
|
|
+[submodule "gitlab-grack"]
|
|
|
|
+ path = gitlab-grack
|
|
|
|
+ url = https://gitlab.com/gitlab-org/gitlab-grack.git
|
|
|
|
|
|
|
|
EOT
|
|
|
|
new_path: ".gitmodules",
|
|
|
|
old_path: ".gitmodules",
|
|
|
|
a_mode: '100644',
|
|
|
|
b_mode: '100644',
|
|
|
|
new_file: false,
|
|
|
|
renamed_file: false,
|
|
|
|
deleted_file: false,
|
|
|
|
too_large: false
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.new' do
|
|
|
|
context 'using a Hash' do
|
|
|
|
context 'with a small diff' do
|
|
|
|
let(:diff) { described_class.new(@raw_diff_hash) }
|
|
|
|
|
|
|
|
it 'initializes the diff' do
|
|
|
|
expect(diff.to_hash).to eq(@raw_diff_hash)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not prune the diff' do
|
|
|
|
expect(diff).not_to be_too_large
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'using a diff that is too large' do
|
|
|
|
it 'prunes the diff' do
|
2021-03-08 18:12:59 +05:30
|
|
|
diff = described_class.new({ diff: 'a' * 204800 })
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
expect(diff.diff).to be_empty
|
|
|
|
expect(diff).to be_too_large
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-12-05 23:21:45 +05:30
|
|
|
context 'using a GitalyClient::Diff' do
|
|
|
|
let(:gitaly_diff) do
|
|
|
|
Gitlab::GitalyClient::Diff.new(
|
|
|
|
to_path: ".gitmodules",
|
|
|
|
from_path: ".gitmodules",
|
|
|
|
old_mode: 0100644,
|
|
|
|
new_mode: 0100644,
|
|
|
|
from_id: '357406f3075a57708d0163752905cc1576fceacc',
|
|
|
|
to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
|
|
|
|
patch: raw_patch
|
|
|
|
)
|
|
|
|
end
|
2020-10-24 23:57:45 +05:30
|
|
|
|
2018-12-05 23:21:45 +05:30
|
|
|
let(:diff) { described_class.new(gitaly_diff) }
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
context 'with a small diff' do
|
2018-12-05 23:21:45 +05:30
|
|
|
let(:raw_patch) { @raw_diff_hash[:diff] }
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
it 'initializes the diff' do
|
2017-09-10 17:25:29 +05:30
|
|
|
expect(diff.to_hash).to eq(@raw_diff_hash)
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not prune the diff' do
|
|
|
|
expect(diff).not_to be_too_large
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'using a diff that is too large' do
|
2018-12-05 23:21:45 +05:30
|
|
|
let(:raw_patch) { 'a' * 204800 }
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2018-12-05 23:21:45 +05:30
|
|
|
it 'prunes the diff' do
|
2017-08-17 22:00:37 +05:30
|
|
|
expect(diff.diff).to be_empty
|
|
|
|
expect(diff).to be_too_large
|
|
|
|
end
|
2021-03-11 19:13:27 +05:30
|
|
|
|
|
|
|
it 'logs the event' do
|
|
|
|
expect(Gitlab::Metrics).to receive(:add_event)
|
|
|
|
.with(:patch_hard_limit_bytes_hit)
|
|
|
|
|
|
|
|
diff
|
|
|
|
end
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
context 'using a collapsable diff that is too large' do
|
2018-12-05 23:21:45 +05:30
|
|
|
let(:raw_patch) { 'a' * 204800 }
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
it 'prunes the diff as a large diff instead of as a collapsed diff' do
|
2018-12-05 23:21:45 +05:30
|
|
|
gitaly_diff.too_large = true
|
|
|
|
diff = described_class.new(gitaly_diff, expanded: false)
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
expect(diff.diff).to be_empty
|
|
|
|
expect(diff).to be_too_large
|
|
|
|
expect(diff).not_to be_collapsed
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
context 'when the patch passed is not UTF-8-encoded' do
|
|
|
|
let(:raw_patch) { @raw_diff_hash[:diff].encode(Encoding::ASCII_8BIT) }
|
|
|
|
|
|
|
|
it 'encodes diff patch to UTF-8' do
|
|
|
|
expect(diff.diff).to be_utf8
|
|
|
|
end
|
|
|
|
end
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
2020-06-23 00:09:42 +05:30
|
|
|
|
|
|
|
context 'using a Gitaly::CommitDelta' do
|
|
|
|
let(:commit_delta) do
|
|
|
|
Gitaly::CommitDelta.new(
|
|
|
|
to_path: ".gitmodules",
|
|
|
|
from_path: ".gitmodules",
|
|
|
|
old_mode: 0100644,
|
|
|
|
new_mode: 0100644,
|
|
|
|
from_id: '357406f3075a57708d0163752905cc1576fceacc',
|
|
|
|
to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0'
|
|
|
|
)
|
|
|
|
end
|
2020-10-24 23:57:45 +05:30
|
|
|
|
2020-06-23 00:09:42 +05:30
|
|
|
let(:diff) { described_class.new(commit_delta) }
|
|
|
|
|
|
|
|
it 'initializes the diff' do
|
|
|
|
expect(diff.to_hash).to eq(@raw_diff_hash.merge(diff: ''))
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'is not too large' do
|
|
|
|
expect(diff).not_to be_too_large
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'has an empty diff' do
|
|
|
|
expect(diff.diff).to be_empty
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'is not a binary' do
|
|
|
|
expect(diff).not_to have_binary_notice
|
|
|
|
end
|
|
|
|
end
|
2022-06-21 17:19:12 +05:30
|
|
|
|
|
|
|
context 'when diff contains invalid characters' do
|
|
|
|
let(:bad_string) { [0xae].pack("C*") }
|
|
|
|
let(:bad_string_two) { [0x89].pack("C*") }
|
2022-07-16 23:28:13 +05:30
|
|
|
let(:bad_string_three) { "@@ -1,5 +1,6 @@\n \xFF\xFE#\x00l\x00a\x00n\x00g\x00u\x00" }
|
2022-06-21 17:19:12 +05:30
|
|
|
|
|
|
|
let(:diff) { described_class.new(@raw_diff_hash.merge({ diff: bad_string })) }
|
|
|
|
let(:diff_two) { described_class.new(@raw_diff_hash.merge({ diff: bad_string_two })) }
|
2022-07-16 23:28:13 +05:30
|
|
|
let(:diff_three) { described_class.new(@raw_diff_hash.merge({ diff: bad_string_three })) }
|
2022-06-21 17:19:12 +05:30
|
|
|
|
|
|
|
context 'when replace_invalid_utf8_chars is true' do
|
|
|
|
it 'will convert invalid characters and not cause an encoding error' do
|
|
|
|
expect(diff.diff).to include(Gitlab::EncodingHelper::UNICODE_REPLACEMENT_CHARACTER)
|
|
|
|
expect(diff_two.diff).to include(Gitlab::EncodingHelper::UNICODE_REPLACEMENT_CHARACTER)
|
2022-07-16 23:28:13 +05:30
|
|
|
expect(diff_three.diff).to include(Gitlab::EncodingHelper::UNICODE_REPLACEMENT_CHARACTER)
|
2022-06-21 17:19:12 +05:30
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
expect { Oj.dump(diff) }.not_to raise_error
|
|
|
|
expect { Oj.dump(diff_two) }.not_to raise_error
|
|
|
|
expect { Oj.dump(diff_three) }.not_to raise_error
|
2022-06-21 17:19:12 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the diff is binary' do
|
|
|
|
let(:project) { create(:project, :repository) }
|
|
|
|
|
|
|
|
it 'will not try to replace characters' do
|
|
|
|
expect(Gitlab::EncodingHelper).not_to receive(:encode_utf8_with_replacement_character?)
|
|
|
|
expect(binary_diff(project).diff).not_to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when replace_invalid_utf8_chars is false' do
|
|
|
|
let(:not_replaced_diff) { described_class.new(@raw_diff_hash.merge({ diff: bad_string, replace_invalid_utf8_chars: false }) ) }
|
|
|
|
let(:not_replaced_diff_two) { described_class.new(@raw_diff_hash.merge({ diff: bad_string_two, replace_invalid_utf8_chars: false }) ) }
|
|
|
|
|
|
|
|
it 'will not try to convert invalid characters' do
|
|
|
|
expect(Gitlab::EncodingHelper).not_to receive(:encode_utf8_with_replacement_character?)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
describe 'straight diffs' do
|
|
|
|
let(:options) { { straight: true } }
|
|
|
|
let(:diffs) { described_class.between(repository, 'feature', 'master', options) }
|
|
|
|
|
|
|
|
it 'has the correct size' do
|
2022-10-11 01:57:18 +05:30
|
|
|
expect(diffs.size).to eq(21)
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
context 'diff' do
|
|
|
|
it 'is an instance of Diff' do
|
|
|
|
expect(diffs.first).to be_kind_of(described_class)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'has the correct new_path' do
|
|
|
|
expect(diffs.first.new_path).to eq('.DS_Store')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'has the correct diff' do
|
|
|
|
expect(diffs.first.diff).to include('Binary files /dev/null and b/.DS_Store differ')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.between' do
|
|
|
|
let(:diffs) { described_class.between(repository, 'feature', 'master') }
|
2020-01-01 13:55:28 +05:30
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
subject { diffs }
|
|
|
|
|
|
|
|
it { is_expected.to be_kind_of Gitlab::Git::DiffCollection }
|
|
|
|
|
|
|
|
describe '#size' do
|
|
|
|
subject { super().size }
|
|
|
|
|
|
|
|
it { is_expected.to eq(1) }
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'diff' do
|
|
|
|
subject { diffs.first }
|
|
|
|
|
|
|
|
it { is_expected.to be_kind_of described_class }
|
|
|
|
|
|
|
|
describe '#new_path' do
|
|
|
|
subject { super().new_path }
|
|
|
|
|
|
|
|
it { is_expected.to eq('files/ruby/feature.rb') }
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#diff' do
|
|
|
|
subject { super().diff }
|
|
|
|
|
|
|
|
it { is_expected.to include '+class Feature' }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.filter_diff_options' do
|
2017-09-10 17:25:29 +05:30
|
|
|
let(:options) { { max_files: 100, invalid_opt: true } }
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
context "without default options" do
|
|
|
|
let(:filtered_options) { described_class.filter_diff_options(options) }
|
|
|
|
|
2019-07-07 11:18:12 +05:30
|
|
|
it "filters invalid options" do
|
2017-08-17 22:00:37 +05:30
|
|
|
expect(filtered_options).not_to have_key(:invalid_opt)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context "with default options" do
|
|
|
|
let(:filtered_options) do
|
2017-09-10 17:25:29 +05:30
|
|
|
default_options = { max_files: 5, bad_opt: 1, ignore_whitespace_change: true }
|
2017-08-17 22:00:37 +05:30
|
|
|
described_class.filter_diff_options(options, default_options)
|
|
|
|
end
|
|
|
|
|
2019-07-07 11:18:12 +05:30
|
|
|
it "filters invalid options" do
|
2017-08-17 22:00:37 +05:30
|
|
|
expect(filtered_options).not_to have_key(:invalid_opt)
|
|
|
|
expect(filtered_options).not_to have_key(:bad_opt)
|
|
|
|
end
|
|
|
|
|
2019-07-07 11:18:12 +05:30
|
|
|
it "merges with default options" do
|
2017-09-10 17:25:29 +05:30
|
|
|
expect(filtered_options).to have_key(:ignore_whitespace_change)
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
|
2019-07-07 11:18:12 +05:30
|
|
|
it "overrides default options" do
|
2017-09-10 17:25:29 +05:30
|
|
|
expect(filtered_options).to have_key(:max_files)
|
|
|
|
expect(filtered_options[:max_files]).to eq(100)
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
describe '#json_safe_diff' do
|
|
|
|
let(:project) { create(:project, :repository) }
|
|
|
|
|
|
|
|
it 'fake binary message when it detects binary' do
|
|
|
|
diff_message = "Binary files files/images/icn-time-tracking.pdf and files/images/icn-time-tracking.pdf differ\n"
|
|
|
|
|
2022-06-21 17:19:12 +05:30
|
|
|
diff = binary_diff(project)
|
|
|
|
expect(diff.diff).not_to be_empty
|
|
|
|
expect(diff.json_safe_diff).to eq(diff_message)
|
2018-03-17 18:26:18 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'leave non-binary diffs as-is' do
|
2018-12-05 23:21:45 +05:30
|
|
|
diff = described_class.new(gitaly_diff)
|
2018-03-17 18:26:18 +05:30
|
|
|
|
|
|
|
expect(diff.json_safe_diff).to eq(diff.diff)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
describe '#submodule?' do
|
2018-12-05 23:21:45 +05:30
|
|
|
let(:gitaly_submodule_diff) do
|
|
|
|
Gitlab::GitalyClient::Diff.new(
|
|
|
|
from_path: 'gitlab-grack',
|
|
|
|
to_path: 'gitlab-grack',
|
|
|
|
old_mode: 0,
|
|
|
|
new_mode: 57344,
|
|
|
|
from_id: '0000000000000000000000000000000000000000',
|
|
|
|
to_id: '645f6c4c82fd3f5e06f67134450a570b795e55a6',
|
|
|
|
overflow_marker: false,
|
|
|
|
collapsed: false,
|
|
|
|
too_large: false,
|
|
|
|
patch: "@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n"
|
|
|
|
)
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
|
2018-12-05 23:21:45 +05:30
|
|
|
it { expect(described_class.new(gitaly_diff).submodule?).to eq(false) }
|
|
|
|
it { expect(described_class.new(gitaly_submodule_diff).submodule?).to eq(true) }
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
describe '#line_count' do
|
2021-01-03 14:25:43 +05:30
|
|
|
let(:diff) { described_class.new(gitaly_diff) }
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2021-01-03 14:25:43 +05:30
|
|
|
it 'returns the correct number of lines' do
|
2018-12-05 23:21:45 +05:30
|
|
|
expect(diff.line_count).to eq(7)
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-01-03 14:25:43 +05:30
|
|
|
describe "#diff_bytesize" do
|
|
|
|
let(:diff) { described_class.new(gitaly_diff) }
|
|
|
|
|
|
|
|
it "returns the size of the diff in bytes" do
|
|
|
|
expect(diff.diff_bytesize).to eq(diff.diff.bytesize)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
describe '#too_large?' do
|
|
|
|
it 'returns true for a diff that is too large' do
|
2021-01-29 00:20:46 +05:30
|
|
|
diff = described_class.new({ diff: 'a' * 204800 })
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
expect(diff.too_large?).to eq(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns false for a diff that is small enough' do
|
2021-01-29 00:20:46 +05:30
|
|
|
diff = described_class.new({ diff: 'a' })
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
expect(diff.too_large?).to eq(false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns true for a diff that was explicitly marked as being too large' do
|
2021-01-29 00:20:46 +05:30
|
|
|
diff = described_class.new({ diff: 'a' })
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
diff.too_large!
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
expect(diff.too_large?).to eq(true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#collapsed?' do
|
|
|
|
it 'returns false by default even on quite big diff' do
|
2021-01-29 00:20:46 +05:30
|
|
|
diff = described_class.new({ diff: 'a' * 20480 })
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
expect(diff).not_to be_collapsed
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns false by default for a diff that is small enough' do
|
2021-01-29 00:20:46 +05:30
|
|
|
diff = described_class.new({ diff: 'a' })
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
expect(diff).not_to be_collapsed
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns true for a diff that was explicitly marked as being collapsed' do
|
2021-01-29 00:20:46 +05:30
|
|
|
diff = described_class.new({ diff: 'a' })
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
diff.collapse!
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
expect(diff).to be_collapsed
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
describe '#collapsed?' do
|
2017-08-17 22:00:37 +05:30
|
|
|
it 'returns true for a diff that is quite large' do
|
2018-03-17 18:26:18 +05:30
|
|
|
diff = described_class.new({ diff: 'a' * 20480 }, expanded: false)
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
expect(diff).to be_collapsed
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns false for a diff that is small enough' do
|
2017-09-10 17:25:29 +05:30
|
|
|
diff = described_class.new({ diff: 'a' }, expanded: false)
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
expect(diff).not_to be_collapsed
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
describe '#collapse!' do
|
2017-08-17 22:00:37 +05:30
|
|
|
it 'prunes the diff' do
|
2021-01-29 00:20:46 +05:30
|
|
|
diff = described_class.new({ diff: "foo\nbar" })
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
diff.collapse!
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
expect(diff.diff).to eq('')
|
|
|
|
expect(diff.line_count).to eq(0)
|
|
|
|
end
|
|
|
|
end
|
2022-06-21 17:19:12 +05:30
|
|
|
|
|
|
|
def binary_diff(project)
|
|
|
|
# rugged will not detect this as binary, but we can fake it
|
|
|
|
described_class.between(project.repository, 'add-pdf-text-binary', 'add-pdf-text-binary^').first
|
|
|
|
end
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|