├── Gemfile ├── spec ├── fixtures │ ├── bom.properties │ ├── test_out_skip_unicode.properties │ ├── test_out.properties │ ├── test_out_skip_separators.properties │ ├── test_out_skip_special_chars.properties │ ├── test_normalized.properties │ └── test.properties ├── java-properties │ ├── version_spec.rb │ ├── parsing │ │ ├── normalizer_spec.rb │ │ └── parser_spec.rb │ ├── encoding │ │ ├── separators_spec.rb │ │ ├── special_chars_spec.rb │ │ └── unicode_spec.rb │ ├── generating │ │ └── generator_spec.rb │ └── encoding_spec.rb ├── helper.rb └── java-properties_spec.rb ├── lib ├── java-properties │ ├── generating.rb │ ├── parsing.rb │ ├── version.rb │ ├── properties.rb │ ├── encoding │ │ ├── special_chars.rb │ │ ├── separators.rb │ │ └── unicode.rb │ ├── encoding.rb │ ├── parsing │ │ ├── parser.rb │ │ └── normalizer.rb │ └── generating │ │ └── generator.rb └── java-properties.rb ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── Rakefile ├── LICENSE ├── java-properties.gemspec └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /spec/fixtures/bom.properties: -------------------------------------------------------------------------------- 1 | pageTitle=Some ü text 2 | tagOther=Other 3 | -------------------------------------------------------------------------------- /lib/java-properties/generating.rb: -------------------------------------------------------------------------------- 1 | require 'java-properties/generating/generator' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.ruby-version 2 | /.ruby-gemset 3 | 4 | /Gemfile.lock 5 | 6 | /.yardoc/ 7 | /coverage/ 8 | /pkg/ 9 | -------------------------------------------------------------------------------- /lib/java-properties/parsing.rb: -------------------------------------------------------------------------------- 1 | require 'java-properties/parsing/normalizer' 2 | require 'java-properties/parsing/parser' -------------------------------------------------------------------------------- /lib/java-properties/version.rb: -------------------------------------------------------------------------------- 1 | module JavaProperties 2 | 3 | # Current version 4 | # @return [String] 5 | VERSION = "0.3.0".freeze 6 | 7 | end 8 | -------------------------------------------------------------------------------- /lib/java-properties/properties.rb: -------------------------------------------------------------------------------- 1 | module JavaProperties 2 | # Simple representation of a properties file content 3 | # by a simple ruby hash object 4 | class Properties < Hash 5 | end 6 | end -------------------------------------------------------------------------------- /spec/java-properties/version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe JavaProperties do 4 | it "should have a version" do 5 | _(JavaProperties::VERSION).wont_be_nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | assignees: 9 | - jnbt 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | require 'rake/testtask' 4 | 5 | desc 'Run tests' 6 | task :default => :spec 7 | 8 | Rake::TestTask.new(:spec) do |test| 9 | test.test_files = FileList['spec/**/*_spec.rb'] 10 | test.libs << 'spec' 11 | test.verbose = true 12 | end -------------------------------------------------------------------------------- /spec/fixtures/test_out_skip_unicode.properties: -------------------------------------------------------------------------------- 1 | item0= 2 | item1=item1 3 | item2=item2 4 | item3=item3 5 | it\ em4=item4 6 | it\=em5=item5 7 | it\:em6=item6 8 | item7=line 1 line 2 line 3 9 | item8=line 1 line 2 line 3 10 | item9=line 1 line 2 line 3 11 | item10=test\n\ttestה test\n\ttest test\n\ttestü = test 12 | item11=aäb 𪀯 13 | -------------------------------------------------------------------------------- /spec/fixtures/test_out.properties: -------------------------------------------------------------------------------- 1 | item0= 2 | item1=item1 3 | item2=item2 4 | item3=item3 5 | it\ em4=item4 6 | it\=em5=item5 7 | it\:em6=item6 8 | item7=line 1 line 2 line 3 9 | item8=line 1 line 2 line 3 10 | item9=line 1 line 2 line 3 11 | item10=test\n\ttest\u05d4 test\n\ttest test\n\ttest\u00fc = test 12 | item11=a\u00e4b \ud868\udc2f 13 | -------------------------------------------------------------------------------- /spec/fixtures/test_out_skip_separators.properties: -------------------------------------------------------------------------------- 1 | item0= 2 | item1=item1 3 | item2=item2 4 | item3=item3 5 | it em4=item4 6 | it=em5=item5 7 | it:em6=item6 8 | item7=line 1 line 2 line 3 9 | item8=line 1 line 2 line 3 10 | item9=line 1 line 2 line 3 11 | item10=test\n\ttest\u05d4 test\n\ttest test\n\ttest\u00fc = test 12 | item11=a\u00e4b \ud868\udc2f 13 | -------------------------------------------------------------------------------- /spec/fixtures/test_out_skip_special_chars.properties: -------------------------------------------------------------------------------- 1 | item0= 2 | item1=item1 3 | item2=item2 4 | item3=item3 5 | it\ em4=item4 6 | it\=em5=item5 7 | it\:em6=item6 8 | item7=line 1 line 2 line 3 9 | item8=line 1 line 2 line 3 10 | item9=line 1 line 2 line 3 11 | item10=test 12 | test\u05d4 test 13 | test test 14 | test\u00fc = test 15 | item11=a\u00e4b \ud868\udc2f 16 | -------------------------------------------------------------------------------- /spec/fixtures/test_normalized.properties: -------------------------------------------------------------------------------- 1 | item0 2 | item1=item1 3 | item2=item2 4 | item3=item3 5 | it\ em4=item4 6 | it\=em5=item5 7 | item6=item6 8 | item7=line 1 line 2 line 3 9 | item8=line 1 line 2 line 3 10 | item9=line 1 line 2 line 3 11 | item10=test\n\ttest\u0050 test\n\ttest test\n\ttest = test 12 | item11=a\u00e4b \ud868\udc2f 13 | item12=#no comment 14 | item13=with\\back\\nslash\\t 15 | -------------------------------------------------------------------------------- /spec/java-properties/parsing/normalizer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe JavaProperties::Parsing::Normalizer do 4 | subject { JavaProperties::Parsing::Normalizer } 5 | 6 | it "normalizes by applying all rules" do 7 | content = fixture("test.properties") 8 | normalized = fixture("test_normalized.properties") 9 | subject.normalize! content 10 | _(content).must_equal normalized 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/test.properties: -------------------------------------------------------------------------------- 1 | # Comment 1 2 | ! Comment 2 3 | item0 4 | item1 = item1 5 | item2 : item2 6 | item3 item3 7 | 8 | #Comment 3 9 | ! Comment 4 10 | 11 | it\ em4=item4 12 | it\=em5:item5 13 | item6 item6 14 | 15 | !Comment 4 16 | # Comment 5 17 | 18 | item7 = line 1 \ 19 | line 2 \ 20 | line 3 21 | 22 | item8 : line 1 \ 23 | line 2 \ 24 | line 3 25 | 26 | item9 line 1 \ 27 | line 2 \ 28 | line 3 29 | 30 | item10=test\n\ttest\u0050 \ 31 | test\n\ttest \ 32 | test\n\ttest = test 33 | 34 | item11=a\u00e4b \ud868\udc2f 35 | 36 | item12=#no comment 37 | 38 | item13=with\\back\\nslash\\t 39 | -------------------------------------------------------------------------------- /spec/helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | 3 | SimpleCov.start do 4 | if ENV['CI'] 5 | require 'simplecov-lcov' 6 | 7 | SimpleCov::Formatter::LcovFormatter.config do |c| 8 | c.report_with_single_file = true 9 | c.single_report_path = 'coverage/lcov.info' 10 | end 11 | 12 | formatter SimpleCov::Formatter::LcovFormatter 13 | end 14 | end 15 | 16 | require 'minitest/autorun' 17 | require 'java-properties' 18 | 19 | class Minitest::Spec 20 | 21 | def fixture(*segments) 22 | File.read(fixture_path(*segments)).strip 23 | end 24 | 25 | def fixture_path(*segments) 26 | File.join(File.dirname(__FILE__), "fixtures", *segments) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/java-properties/encoding/separators_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe JavaProperties::Encoding::Separators do 4 | subject{ JavaProperties::Encoding::Separators } 5 | let(:raw) { 'this is some = text : with special \\=separators' } 6 | let(:raw_normalizd) { 'this is some = text : with special =separators' } 7 | let(:encoded){ 'this\\ is\\ some\\ \\=\\ text\\ \\:\\ with\\ special\\ \\=separators' } 8 | 9 | it "encodes separators" do 10 | processed = subject.encode!(raw.dup) 11 | _(processed).must_equal encoded 12 | end 13 | 14 | it "decodes separators" do 15 | processed = subject.decode!(encoded.dup) 16 | _(processed).must_equal raw_normalizd 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | ruby-version: 9 | - "2.4" 10 | - "2.5" 11 | - "2.6" 12 | - "2.7" 13 | - "3.0" 14 | - "3.1" 15 | - "3.2" 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: ${{ matrix.ruby-version }} 21 | bundler-cache: true 22 | - run: bundle exec rake 23 | - uses: coverallsapp/github-action@master 24 | with: 25 | github-token: ${{ secrets.github_token }} 26 | flag-name: run-${{ matrix.ruby-version }} 27 | parallel: true 28 | finish: 29 | needs: build 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: coverallsapp/github-action@master 33 | with: 34 | github-token: ${{ secrets.github_token }} 35 | parallel-finished: true 36 | -------------------------------------------------------------------------------- /spec/java-properties/encoding/special_chars_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe JavaProperties::Encoding::SpecialChars do 4 | subject{ JavaProperties::Encoding::SpecialChars } 5 | let(:raw) { "this\\is some \n text \t wi\\th special\r chars\f" } 6 | let(:encoded){ 'this\\\\is some \n text \t wi\\\\th special\r chars\f' } 7 | 8 | it "encodes special chars" do 9 | processed = subject.encode!(raw.dup) 10 | _(processed).must_equal encoded 11 | end 12 | 13 | it "decodes special chars" do 14 | processed = subject.decode!(encoded.dup) 15 | _(processed).must_equal raw 16 | end 17 | 18 | it "decodes and encodes to the same" do 19 | encoded = subject.encode!(raw.dup) 20 | deconded = subject.decode!(encoded) 21 | _(deconded).must_equal raw 22 | end 23 | 24 | it "handled backslash escpaing" do 25 | decoded = subject.decode!('some\\\\test\\\\') 26 | _(decoded).must_equal 'some\\test\\' 27 | encoded = subject.encode!('some\\test\\') 28 | _(encoded).must_equal 'some\\\\test\\\\' 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Jonas Thiel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/java-properties/parsing/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe JavaProperties::Parsing::Parser do 4 | subject { JavaProperties::Parsing::Parser } 5 | 6 | let(:as_hash) do 7 | { 8 | :item0 => "", 9 | :item1 => "item1", 10 | :item2 => "item2", 11 | :item3 => "item3", 12 | :"it em4" => "item4", 13 | :"it=em5" => "item5", 14 | :item6 => "item6", 15 | :item7 => "line 1 line 2 line 3", 16 | :item8 => "line 1 line 2 line 3", 17 | :item9 => "line 1 line 2 line 3", 18 | :item10 => "test\n\ttestP test\n\ttest test\n\ttest = test", 19 | :item11 => "aäb 𪀯", 20 | :item12 => "#no comment", 21 | :item13 => "with\\back\\nslash\\t", 22 | } 23 | end 24 | 25 | it "parses correctly a properties file content" do 26 | content = fixture("test.properties") 27 | properties = subject.parse(content) 28 | 29 | # don't compare the hashes directly, as this hard to debug 30 | _(properties.keys).must_equal as_hash.keys 31 | properties.each do |key, value| 32 | _(value).must_equal as_hash[key] 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /java-properties.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/java-properties/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'java-properties' 7 | spec.version = JavaProperties::VERSION.dup 8 | spec.authors = ['Jonas Thiel'] 9 | spec.email = ['jonas@thiel.io'] 10 | spec.summary = %q{Loader and writer for *.properties files} 11 | spec.description = %q{Tool for loading and writing Java properties files} 12 | spec.homepage = 'https://github.com/jnbt/java-properties' 13 | spec.license = 'MIT' 14 | 15 | spec.files = %w(LICENSE README.md Rakefile java-properties.gemspec) 16 | spec.files += Dir.glob('lib/**/*.rb') 17 | spec.test_files = Dir.glob('spec/**/*.rb') 18 | spec.test_files = Dir.glob('spec/fixtures/**/*.properties') 19 | 20 | spec.required_rubygems_version = '>= 1.3.5' 21 | spec.required_ruby_version = Gem::Requirement.new(">= 2.4") 22 | 23 | spec.add_development_dependency 'rake', '~> 13.0' 24 | spec.add_development_dependency 'inch', '~> 0.8' 25 | spec.add_development_dependency 'minitest', '~> 5.14' 26 | spec.add_development_dependency 'simplecov', '~> 0.18.0' 27 | spec.add_development_dependency 'simplecov-lcov', '~> 0.8.0' 28 | end 29 | -------------------------------------------------------------------------------- /spec/java-properties_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require 'helper' 3 | 4 | describe JavaProperties do 5 | subject{ JavaProperties } 6 | 7 | it "parses from string" do 8 | _(subject.parse("item1=item1")).must_equal({:item1 => "item1"}) 9 | end 10 | 11 | it "generates from hash" do 12 | _(subject.generate({:item1 => "item1"})).must_equal("item1=item1") 13 | end 14 | 15 | it "loads from file" do 16 | with_temp_file do |file| 17 | file << "item1=item1" 18 | file.flush 19 | 20 | _(subject.load(file.path)).must_equal({:item1 => "item1"}) 21 | end 22 | end 23 | 24 | it "writes to file" do 25 | with_temp_file do |file| 26 | subject.write({:item1 => "item1"}, file.path) 27 | 28 | file.rewind 29 | _(file.read).must_equal "item1=item1" 30 | end 31 | end 32 | 33 | it "loads from data starting with a BOM" do 34 | properties = subject.load(fixture_path("bom.properties")) 35 | expected = { 36 | :pageTitle => "Some ü text", 37 | :tagOther => "Other" 38 | } 39 | _(properties).must_equal(expected) 40 | end 41 | 42 | private 43 | 44 | def with_temp_file(&block) 45 | file = Tempfile.new("java-properties") 46 | block.call(file) 47 | ensure 48 | file.close 49 | file.unlink 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /spec/java-properties/encoding/unicode_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require 'helper' 3 | 4 | describe JavaProperties::Encoding::Unicode do 5 | subject{ JavaProperties::Encoding::Unicode } 6 | 7 | let(:encoded) { 'this is some \u0024 text \U05D4 with unicode \u00fc a\u00e4b \ud868\udc2f' } 8 | let(:encoded_normalized) { 'this is some $ text \u05d4 with unicode \u00fc a\u00e4b \ud868\udc2f' } 9 | let(:decoded) { 'this is some $ text ה with unicode ü aäb 𪀯' } 10 | 11 | it "decodes unicode chars" do 12 | _(subject.decode!(encoded.dup)).must_equal decoded 13 | end 14 | 15 | it "encodes unicode chars" do 16 | _(subject.encode!(decoded.dup)).must_equal encoded_normalized 17 | end 18 | 19 | it "encodes unicode chars but has 2-based hex size, padded to at least 4" do 20 | _(subject.encode!("ü")).must_equal '\u00fc' 21 | _(subject.encode!("ה")).must_equal '\u05d4' 22 | _(subject.encode!("ᘓ")).must_equal '\u1613' 23 | end 24 | 25 | it "decodes unicode chars based on 4 hexdigest" do 26 | _(subject.decode!('a\u00e4b')).must_equal "aäb" 27 | end 28 | 29 | it "decodes unicode chars over multiple chunks" do 30 | _(subject.decode!('\ud868\udc2f')).must_equal "𪀯" 31 | end 32 | 33 | it "decodes and encodes" do 34 | encoded = subject.encode!(decoded.dup) 35 | dec = subject.decode!(encoded.dup) 36 | _(dec).must_equal decoded 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/java-properties.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | require 'java-properties/version' 3 | require 'java-properties/properties' 4 | require 'java-properties/encoding' 5 | require 'java-properties/parsing' 6 | require 'java-properties/generating' 7 | 8 | # A module to read and write Java properties files 9 | module JavaProperties 10 | 11 | # Parses the content of a Java properties file 12 | # @see Parsing::Parser 13 | # @param text [String] 14 | # @return [Properties] 15 | def self.parse(text) 16 | Parsing::Parser.parse(text) 17 | end 18 | 19 | # Generates the content of a Java properties file 20 | # @see Generating::Generator 21 | # @param hash [Hash] 22 | # @param options [Hash] options for the generator 23 | # @return [String] 24 | def self.generate(hash, options = {}) 25 | Generating::Generator.generate(hash, options) 26 | end 27 | 28 | # Loads and parses a Java properties file 29 | # @see Parsing::Parser 30 | # @param path [String] 31 | # @return [Properties] 32 | def self.load(path) 33 | File.open(path, "r:bom|utf-8") do |f| 34 | parse(f.read) 35 | end 36 | end 37 | 38 | # Generates a Java properties file 39 | # @see Generating::Generator 40 | # @param hash [Hash] 41 | # @param path [String] 42 | # @param options [Hash] options for the generator 43 | def self.write(hash, path, options = {}) 44 | File.write(path, generate(hash, options)) 45 | end 46 | 47 | end -------------------------------------------------------------------------------- /lib/java-properties/encoding/special_chars.rb: -------------------------------------------------------------------------------- 1 | module JavaProperties 2 | module Encoding 3 | # Module to escape and unescape special chars 4 | # @see JavaProperties::Encoding 5 | module SpecialChars 6 | 7 | # Lookup table for escaping special chars 8 | # @return [Hash] 9 | ESCAPING = { 10 | "\t" => '\\t', 11 | "\r" => '\\r', 12 | "\n" => '\\n', 13 | "\f" => '\\f', 14 | "\\" => '\\\\', 15 | }.freeze 16 | 17 | # Lookup table to remove escaping from special chars 18 | # @return [Hash] 19 | DESCAPING = ESCAPING.invert.freeze 20 | 21 | # Marks a segment which has is an encoding special char 22 | # @return [Regexp] 23 | DESCAPING_MARKER = /\\./ 24 | 25 | # Encodes the content a text by escaping all special chars 26 | # @param text [String] 27 | # @return [String] The escaped text for chaining 28 | def self.encode!(text) 29 | buffer = StringIO.new 30 | text.each_char do |char| 31 | buffer << ESCAPING.fetch(char, char) 32 | end 33 | text.replace buffer.string 34 | text 35 | end 36 | 37 | # Decodes the content a text by removing all escaping from special chars 38 | # @param text [String] 39 | # @return [String] The unescaped text for chaining 40 | def self.decode!(text) 41 | text.gsub!(DESCAPING_MARKER) do |match| 42 | DESCAPING.fetch(match, match) 43 | end 44 | text 45 | end 46 | 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/java-properties/encoding/separators.rb: -------------------------------------------------------------------------------- 1 | module JavaProperties 2 | module Encoding 3 | # Module to escape separators as : or = 4 | # @see JavaProperties::Encoding 5 | module Separators 6 | 7 | # Marker for all separators 8 | # @return [Regexp] 9 | ENCODE_SEPARATOR_MARKER = /[ :=]/ 10 | 11 | # Marker for already escaped separators 12 | # @return [Regexp] 13 | ESCAPING_MARKER = /\\/ 14 | 15 | # Char to use for escaping 16 | # @return [String] 17 | ESCAPE = "\\" 18 | 19 | # Marker for all escaped separators 20 | # @return [Regexp] 21 | DECODE_SEPARATOR_MARKER = /\\([ :=])/ 22 | 23 | # Escapes all not already escaped separators 24 | # @param text [text] 25 | # @return [String] The escaped text for chaining 26 | def self.encode!(text) 27 | buffer = StringIO.new 28 | last_token = '' 29 | text.each_char do |char| 30 | if char =~ ENCODE_SEPARATOR_MARKER && last_token !~ ESCAPING_MARKER 31 | buffer << ESCAPE 32 | end 33 | buffer << char 34 | last_token = char 35 | end 36 | text.replace buffer.string 37 | text 38 | end 39 | 40 | # Removes escapes from escaped separators 41 | # @param text [text] 42 | # @return [String] The unescaped text for chaining 43 | def self.decode!(text) 44 | text.gsub!(DECODE_SEPARATOR_MARKER) do 45 | $1 46 | end 47 | text 48 | end 49 | 50 | end 51 | end 52 | end -------------------------------------------------------------------------------- /spec/java-properties/generating/generator_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require 'helper' 3 | 4 | describe JavaProperties::Generating::Generator do 5 | subject { JavaProperties::Generating::Generator } 6 | 7 | let(:as_hash) do 8 | { 9 | :item0 => "", 10 | :item1 => "item1".freeze, 11 | :item2 => "item2", 12 | :item3 => "item3", 13 | :"it em4".freeze => "item4", 14 | :"it=em5" => "item5", 15 | :"it:em6" => "item6", 16 | :item7 => "line 1 line 2 line 3", 17 | :item8 => "line 1 line 2 line 3", 18 | :item9 => "line 1 line 2 line 3", 19 | :item10 => "test\n\ttestה test\n\ttest test\n\ttestü = test", 20 | :item11 => "aäb 𪀯" 21 | } 22 | end 23 | 24 | it "generates properties file content" do 25 | expected = fixture("test_out.properties") 26 | content = subject.generate(as_hash) 27 | _(content).must_equal expected 28 | end 29 | 30 | it "generates properties file content but skips unicode encoding" do 31 | expected = fixture("test_out_skip_unicode.properties") 32 | content = subject.generate(as_hash, :skip_encode_unicode => true) 33 | _(content).must_equal expected 34 | end 35 | 36 | it "generates properties file content but skips separators encoding" do 37 | expected = fixture("test_out_skip_separators.properties") 38 | content = subject.generate(as_hash, :skip_encode_separators => true) 39 | _(content).must_equal expected 40 | end 41 | 42 | it "generates properties file content but skips special chars encoding" do 43 | expected = fixture("test_out_skip_special_chars.properties") 44 | content = subject.generate(as_hash, :skip_encode_special_chars => true) 45 | _(content).must_equal expected 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /lib/java-properties/encoding.rb: -------------------------------------------------------------------------------- 1 | require 'java-properties/encoding/special_chars' 2 | require 'java-properties/encoding/separators' 3 | require 'java-properties/encoding/unicode' 4 | 5 | module JavaProperties 6 | # Module to encode and decode 7 | # 8 | # Usage: 9 | # encoded = Encoding.encode!("Some text to be encoded") 10 | # decoded = Encoding.decode!("Some text to be decoded") 11 | # 12 | # You can disable separate encoding (and decoding) steps, 13 | # by passing in additional flags: 14 | # 15 | # * SKIP_SEPARATORS: Do not code the separators (space,:,=) 16 | # * SKIP_UNICODE: Do not code unicode chars 17 | # * SKIP_SPECIAL_CHARS: Do not code newlines, tab stops, ... 18 | # 19 | module Encoding 20 | 21 | # Flag for skipping separators encodings / decoding 22 | # @return [Symbol] 23 | SKIP_SEPARATORS=:skip_separators 24 | 25 | # Flag for skipping separators encodings / decoding 26 | # @return [Symbol] 27 | SKIP_UNICODE=:skip_unicode 28 | 29 | # Flag for skipping separators encodings / decoding 30 | # @return [Symbol] 31 | SKIP_SPECIAL_CHARS=:skip_special_chars 32 | 33 | # Encode a given text in place 34 | # @param text [String] 35 | # @param *flags [Array] Optional flags to skip encoding steps 36 | # @return [String] The encoded text for chaining 37 | def self.encode!(text, *flags) 38 | SpecialChars.encode!(text) unless flags.include?(SKIP_SPECIAL_CHARS) 39 | Separators.encode!(text) unless flags.include?(SKIP_SEPARATORS) 40 | Unicode.encode!(text) unless flags.include?(SKIP_UNICODE) 41 | text 42 | end 43 | 44 | # Decodes a given text in place 45 | # @param text [String] 46 | # @param *flags [Array] Optional flags to skip decoding steps 47 | # @return [String] The decoded text for chaining 48 | def self.decode!(text, *flags) 49 | Unicode.decode!(text) unless flags.include?(SKIP_UNICODE) 50 | Separators.decode!(text) unless flags.include?(SKIP_SEPARATORS) 51 | SpecialChars.decode!(text) unless flags.include?(SKIP_SPECIAL_CHARS) 52 | text 53 | end 54 | 55 | end 56 | end -------------------------------------------------------------------------------- /lib/java-properties/parsing/parser.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | module JavaProperties 3 | module Parsing 4 | # This module allows parsing of a properties file content string 5 | # into a {Properties} object 6 | # 7 | # @example 8 | # Parser.parse("item=something \u05d4") => {:item => "something ה"} 9 | # 10 | module Parser 11 | 12 | # Symbol which separates key from value after normalization 13 | # @return [String] 14 | KEY_VALUE_MARKER = '=' 15 | 16 | # Symbol which escapes a KEY_VALUE_MARKER in the key name 17 | # @return [String] 18 | KEY_ESCAPE = '\\' 19 | 20 | # Marker for a line which only consists of an key w/o value 21 | # @return [Regexp] 22 | KEY_ONLY_MARKER = /^(\S+)$/ 23 | 24 | # Parses a string into a {Properties} object 25 | # @param text [String] 26 | # @return [Properties] 27 | def self.parse(text) 28 | properties = Properties.new 29 | Normalizer.normalize!(text) 30 | text.each_line do |line| 31 | key, value = extract_key_and_value(line.chomp) 32 | append_to_properties(properties, key, value) 33 | end 34 | properties 35 | end 36 | 37 | private 38 | 39 | def self.extract_key_and_value(line) 40 | # A line must be handled char by char to handled escaped '=' chars in the key name 41 | key = StringIO.new 42 | value = StringIO.new 43 | key_complete = false 44 | last_token = '' 45 | line.each_char do |char| 46 | if !key_complete && char == KEY_VALUE_MARKER && last_token != KEY_ESCAPE 47 | key_complete = true 48 | else 49 | (key_complete ? value : key) << char 50 | end 51 | last_token = char 52 | end 53 | [key.string, value.string] 54 | end 55 | 56 | def self.append_to_properties(properties, key, value) 57 | unless key.nil? && value.nil? 58 | properties[Encoding.decode!(key).to_sym] = Encoding.decode!(value, Encoding::SKIP_SEPARATORS) 59 | end 60 | end 61 | 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /lib/java-properties/parsing/normalizer.rb: -------------------------------------------------------------------------------- 1 | module JavaProperties 2 | module Parsing 3 | # Module to normalize the content of a properties file 4 | # 5 | # @example Normalizes: 6 | # # Comment 1 7 | # ! Comment 2 8 | # item0 9 | # item1 = item1 10 | # item2 : item2 11 | # item3=line 1 \ 12 | # line 2 13 | # 14 | # @example Into: 15 | # 16 | # item0 17 | # item1=item1 18 | # item2=item2 19 | # item3=line 1 line 2 20 | # 21 | module Normalizer 22 | 23 | # Describes a single normalization rule by replacing content 24 | class Rule 25 | # Initializes a new rules base on a matching regexp 26 | # and a replacement as substitution 27 | # @param matcher [Regexp] 28 | # @param replacement [String] 29 | def initialize(matcher, replacement = '') 30 | @matcher = matcher 31 | @replacement = replacement 32 | end 33 | 34 | # Apply the substitution to the text in place 35 | # @param text [string] 36 | # @return [String] 37 | def apply!(text) 38 | text.gsub!(@matcher, @replacement) 39 | end 40 | end 41 | 42 | # Collection of ordered rules 43 | RULES = [] 44 | 45 | # Removes comments 46 | RULES << Rule.new(/^\s*[!\#].*$/) 47 | 48 | # Removes leading whitepsace 49 | RULES << Rule.new(/^\s+/) 50 | 51 | # Removes tailing whitepsace 52 | RULES << Rule.new(/\s+$/) 53 | 54 | # Strings ending with \ are concatenated 55 | RULES << Rule.new(/\\\s*$[\n\r]+/) 56 | 57 | # Remove whitespace around delimiters and replace with = 58 | RULES << Rule.new(/^((?:(?:\\[=: \t])|[^=: \t])+)[ \t]*[=: \t][ \t]*/, '\1=') 59 | 60 | RULES.freeze 61 | 62 | # Normalizes the content of a properties file content by applying the RULES 63 | # @param text [String] 64 | # @return [String] 65 | def self.normalize!(text) 66 | RULES.each do |rule| 67 | rule.apply!(text) 68 | end 69 | text 70 | end 71 | 72 | end 73 | end 74 | end -------------------------------------------------------------------------------- /lib/java-properties/generating/generator.rb: -------------------------------------------------------------------------------- 1 | module JavaProperties 2 | module Generating 3 | # This module allows generating the content of a properties file 4 | # base on a {Properties} object (or any other hash like structure) 5 | # 6 | # @example 7 | # Generator.generate({:item => "something ה"}) => "item=something \u05d4" 8 | # 9 | module Generator 10 | # Character used for key-value separation 11 | # @return [String] 12 | KEY_VALUE_SEPARATOR = '=' 13 | 14 | # Default options 15 | # @return [Hash] 16 | DEFAULT_OPTIONS = { 17 | :skip_encode_unicode => false, 18 | :skip_encode_separators => false, 19 | :skip_encode_special_chars => false 20 | }.freeze 21 | 22 | # Generates a properties file content based on a hash 23 | # @param properties [Properties] or simple hash 24 | # @param options [Hash] 25 | # @option options skip_encode_unicode [Boolean] Skip unicode encoding 26 | # @option options skip_encode_separators [Boolean] Skip seperators encoding 27 | # @option options skip_encode_special_chars [Boolean] Skip special char encoding 28 | # @return [String] 29 | def self.generate(properties, options = {}) 30 | options = DEFAULT_OPTIONS.merge(options) 31 | lines = [] 32 | properties.each do |key, value| 33 | lines << build_line(key, value, options) 34 | end 35 | lines.join("\n") 36 | end 37 | 38 | private 39 | 40 | def self.build_line(key, value, options) 41 | encoded_key = Encoding.encode!(key.to_s.dup, *encoding_skips(false, options)) 42 | encoded_value = Encoding.encode!(value.to_s.dup, *encoding_skips(true, options)) 43 | 44 | encoded_key + KEY_VALUE_SEPARATOR + encoded_value 45 | end 46 | 47 | def self.encoding_skips(is_value, options) 48 | skips = [] 49 | skips << Encoding::SKIP_SEPARATORS if is_value || options[:skip_encode_separators] 50 | skips << Encoding::SKIP_UNICODE if options[:skip_encode_unicode] 51 | skips << Encoding::SKIP_SPECIAL_CHARS if options[:skip_encode_special_chars] 52 | skips 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/java-properties/encoding_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require 'helper' 3 | 4 | describe JavaProperties::Encoding do 5 | subject{ JavaProperties::Encoding } 6 | 7 | describe "full" do 8 | let(:decoded){ "this is some \n text = with ה" } 9 | let(:encoded){ 'this\ is\ some\ \n\ text\ \=\ with\ \u05d4' } 10 | 11 | it "encodes correctly" do 12 | input = decoded.dup 13 | processed = subject.encode!(input) 14 | _(processed).must_equal encoded 15 | end 16 | 17 | it "decodes correctly" do 18 | input = encoded.dup 19 | processed = subject.decode!(input) 20 | _(processed).must_equal decoded 21 | end 22 | end 23 | 24 | describe "skip_separators" do 25 | let(:decoded){ "this is some \n text = with ה" } 26 | let(:encoded){ 'this is some \n text = with \u05d4' } 27 | 28 | it "encodes correctly" do 29 | input = decoded.dup 30 | processed = subject.encode!(input, subject::SKIP_SEPARATORS) 31 | _(processed).must_equal encoded 32 | end 33 | 34 | it "decodes correctly" do 35 | input = encoded.dup 36 | processed = subject.decode!(input, subject::SKIP_SEPARATORS) 37 | _(processed).must_equal decoded 38 | end 39 | end 40 | 41 | describe "skip_unicode" do 42 | it "encodes correctly" do 43 | input = "this is some \n text = with ה" 44 | processed = subject.encode!(input, subject::SKIP_UNICODE) 45 | expected = 'this\ is\ some\ \n\ text\ \=\ with\ ה' 46 | _(processed).must_equal expected 47 | end 48 | 49 | it "decodes correctly" do 50 | input = 'this\ is\ some\ \n\ text\ \=\ with\ \u05d4' 51 | processed = subject.decode!(input, subject::SKIP_UNICODE) 52 | expected = "this is some \n text = with " + '\u05d4' 53 | _(processed).must_equal expected 54 | end 55 | end 56 | 57 | describe "skip_special_chars" do 58 | it "encodes correctly" do 59 | input = "this is some \n text = with ה" 60 | processed = subject.encode!(input, subject::SKIP_SPECIAL_CHARS) 61 | expected = 'this\ is\ some\ ' + "\n" + '\ text\ \=\ with\ \u05d4' 62 | _(processed).must_equal expected 63 | end 64 | 65 | it "decodes correctly" do 66 | input = 'this\ is\ some\ \n\ text\ \=\ with\ \u05d4' 67 | processed = subject.decode!(input, subject::SKIP_SPECIAL_CHARS) 68 | expected = 'this is some \n text = with ה' 69 | _(processed).must_equal expected 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/java-properties/encoding/unicode.rb: -------------------------------------------------------------------------------- 1 | module JavaProperties 2 | module Encoding 3 | # Module to encode and decode unicode chars 4 | # This code is highly influced by Florian Frank's JSON gem 5 | # @see https://github.com/flori/json/ 6 | module Unicode 7 | 8 | # @private 9 | MAP = { 10 | "\x0" => '\u0000', 11 | "\x1" => '\u0001', 12 | "\x2" => '\u0002', 13 | "\x3" => '\u0003', 14 | "\x4" => '\u0004', 15 | "\x5" => '\u0005', 16 | "\x6" => '\u0006', 17 | "\x7" => '\u0007', 18 | "\xb" => '\u000b', 19 | "\xe" => '\u000e', 20 | "\xf" => '\u000f', 21 | "\x10" => '\u0010', 22 | "\x11" => '\u0011', 23 | "\x12" => '\u0012', 24 | "\x13" => '\u0013', 25 | "\x14" => '\u0014', 26 | "\x15" => '\u0015', 27 | "\x16" => '\u0016', 28 | "\x17" => '\u0017', 29 | "\x18" => '\u0018', 30 | "\x19" => '\u0019', 31 | "\x1a" => '\u001a', 32 | "\x1b" => '\u001b', 33 | "\x1c" => '\u001c', 34 | "\x1d" => '\u001d', 35 | "\x1e" => '\u001e', 36 | "\x1f" => '\u001f', 37 | } 38 | 39 | # @private 40 | EMPTY_8BIT_STRING = '' 41 | EMPTY_8BIT_STRING.force_encoding(::Encoding::ASCII_8BIT) 42 | 43 | # Decodes all unicode chars from escape sequences in place 44 | # @param text [String] 45 | # @return [String] The encoded text for chaining 46 | def self.decode!(text) 47 | string = text.dup 48 | string = string.gsub(%r((?:\\[uU](?:[A-Fa-f\d]{4}))+)) do |c| 49 | c.downcase! 50 | bytes = EMPTY_8BIT_STRING.dup 51 | i = 0 52 | while c[6 * i] == ?\\ && c[6 * i + 1] == ?u 53 | bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16) 54 | i += 1 55 | end 56 | bytes.encode("utf-8", "utf-16be") 57 | end 58 | string.force_encoding(::Encoding::UTF_8) 59 | 60 | text.replace string 61 | text 62 | end 63 | 64 | # Decodes all unicode chars into escape sequences in place 65 | # @param text [String] 66 | # @return [String] The decoded text for chaining 67 | def self.encode!(text) 68 | string = text.dup 69 | string.force_encoding(::Encoding::ASCII_8BIT) 70 | string.gsub!(/["\\\x0-\x1f]/n) { |c| MAP[c] || c } 71 | string.gsub!(/( 72 | (?: 73 | [\xc2-\xdf][\x80-\xbf] | 74 | [\xe0-\xef][\x80-\xbf]{2} | 75 | [\xf0-\xf4][\x80-\xbf]{3} 76 | )+ | 77 | [\x80-\xc1\xf5-\xff] # invalid 78 | )/nx) { |c| 79 | c.size == 1 and raise "Invalid utf8 byte: '#{c}'" 80 | s = c.encode("utf-16be", "utf-8").unpack('H*')[0] 81 | s.force_encoding(::Encoding::ASCII_8BIT) 82 | s.gsub!(/.{4}/n, '\\\\u\&') 83 | s.force_encoding(::Encoding::UTF_8) 84 | } 85 | string.force_encoding(::Encoding::UTF_8) 86 | text.replace string 87 | text 88 | end 89 | 90 | private 91 | 92 | def self.unicode(code) 93 | code.chr(::Encoding::UTF_8) 94 | end 95 | 96 | def self.hex(codepoint) 97 | hex = codepoint.to_s(16) 98 | size = [4, hex.size].max 99 | target_size = size.even? ? size : size+1 100 | hex.rjust(target_size, '0') 101 | end 102 | 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaProperties 2 | 3 | [![Build Status](https://github.com/jnbt/java-properties/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/jnbt/java-properties/actions?query=branch%3Amaster) 4 | [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/jnbt/java-properties)](https://codeclimate.com/github/jnbt/java-properties) 5 | [![Coverage Status](https://coveralls.io/repos/github/jnbt/java-properties/badge.svg?branch=master)](https://coveralls.io/github/java-properties/linr?branch=master) 6 | [![RubyGems](http://img.shields.io/gem/v/java-properties)](http://rubygems.org/gems/java-properties) 7 | [![Inline docs](http://inch-ci.org/github/jnbt/java-properties.svg?style=shields)](http://inch-ci.org/github/jnbt/java-properties) 8 | 9 | A ruby library to read and write [Java properties files](http://en.wikipedia.org/wiki/.properties). 10 | 11 | ## Installation 12 | 13 | Install via Rubygems 14 | 15 | ```bash 16 | $ gem install java-properties 17 | ``` 18 | 19 | ... or add to your Gemfile 20 | 21 | ```ruby 22 | gem "java-properties" 23 | ``` 24 | 25 | ## Loading files 26 | 27 | You can load a valid Java properties file from the file system using a path: 28 | 29 | ```ruby 30 | properties = JavaProperties.load("path/to/my.properties") 31 | properties[:foo] # => "bar" 32 | ``` 33 | 34 | If have already the content of the properties file at hand than parse the content as: 35 | 36 | ```ruby 37 | properties = JavaProperties.parse("foo=bar") 38 | properties[:foo] # => "bar" 39 | ``` 40 | 41 | ## Writing files 42 | 43 | You can write any Hash-like structure as a properties file: 44 | 45 | ```ruby 46 | hash = {:foo => "bar"} 47 | JavaProperties.write(hash, "path/to/my.properties") 48 | ``` 49 | 50 | Or if you want to omit the file you can receive the content directly: 51 | 52 | ```ruby 53 | hash = {:foo => "bar"} 54 | JavaProperties.generate(hash) # => "foo=bar" 55 | ``` 56 | 57 | ## Encodings and special chars 58 | 59 | As Java properties files normally hold UTF-8 chars in their escaped representation this tool tries to convert them: 60 | 61 | ``` 62 | "ה" <=> "\u05d4" 63 | "𪀯" <=> "\ud868\udc2f" 64 | ``` 65 | 66 | The tool also escaped every '=', ' ' and ':' in the name part of a property line: 67 | 68 | ```ruby 69 | JavaProperties.generate({"i : like=strange" => "bar"}) 70 | # => "i\ \:\ like\=strange=bar" 71 | ``` 72 | 73 | ## Multi line and line breaks 74 | 75 | In Java properties files a string can be multi line but line breaks have to be escaped. 76 | 77 | Assume the following input: 78 | 79 | ```ini 80 | my=This is a multi \ 81 | line content with only \n one line break 82 | ``` 83 | 84 | The parses would read: 85 | 86 | ```ruby 87 | {:my => "This is a multi line content which only \n one line break"} 88 | ``` 89 | 90 | In the opposite direction line breaks will be correctly escaped but the generator will never use multi line values. 91 | 92 | ## Contributing 93 | 94 | 1. [Fork it!](https://github.com/jnbt/java-properties/fork) 95 | 2. Create your feature branch (`git checkout -b my-new-feature`) 96 | 3. Commit your changes (`git commit -am 'Add some feature'`) 97 | 4. Push to the branch (`git push origin my-new-feature`) 98 | 5. Create new Pull Request 99 | 100 | ## Author 101 | 102 | Jonas Thiel (@jonasthiel) 103 | 104 | ## References 105 | 106 | For more information about the properties file format have a look at the [Java Plattform documenation](http://docs.oracle.com/javase/6/docs/api/java/util/Properties.html). 107 | 108 | ## License 109 | 110 | This gem is released under the MIT License. See the LICENSE file for further details. 111 | --------------------------------------------------------------------------------