pax_global_header00006660000000000000000000000064151357447450014531gustar00rootroot0000000000000052 comment=b41b09fc8804e8b52c44559598c0d5ff0d81410c moor-2.10.3/000077500000000000000000000000001513574474500125705ustar00rootroot00000000000000moor-2.10.3/.dockerignore000066400000000000000000000000121513574474500152350ustar00rootroot00000000000000/releases moor-2.10.3/.github/000077500000000000000000000000001513574474500141305ustar00rootroot00000000000000moor-2.10.3/.github/workflows/000077500000000000000000000000001513574474500161655ustar00rootroot00000000000000moor-2.10.3/.github/workflows/deployment.yml000066400000000000000000000004561513574474500210750ustar00rootroot00000000000000name: Continuous Delivery on: release: types: [released] jobs: homebrew: runs-on: ubuntu-latest steps: - name: Bump Homebrew formula uses: dawidd6/action-homebrew-bump-formula@v7 with: token: ${{secrets.JOHAN_GITHUB_API_TOKEN}} formula: moor moor-2.10.3/.github/workflows/linux-ci.yml000066400000000000000000000014541513574474500204440ustar00rootroot00000000000000name: Linux CI on: push: branches: [master] pull_request: jobs: validate: runs-on: ubuntu-latest steps: - name: Check out repository code uses: actions/checkout@v5 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: "go.mod" # golangci-lint is required by test.sh. Latest version here if you want # to bump it, version number is at the end of the "curl | sh" # commandline below: # https://github.com/golangci/golangci-lint/releases/latest - name: Install golangci-lint run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$(go env GOPATH)"/bin v2.1.2 - run: go build ./... - run: ./test.sh - run: GOARCH=386 ./test.sh moor-2.10.3/.github/workflows/windows-ci.yml000066400000000000000000000006071513574474500207760ustar00rootroot00000000000000name: Windows CI on: push: branches: [master] pull_request: jobs: validate: runs-on: windows-latest steps: - name: Check out repository code uses: actions/checkout@v5 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: "go.mod" - run: go build -race ./... - run: go test -race -timeout 60s ./... moor-2.10.3/.gitignore000066400000000000000000000002001513574474500145500ustar00rootroot00000000000000/moor /moor.exe /releases/moor-* # For historical reasons, this old name had to be changed to get into Debian /releases/moar-* moor-2.10.3/.golangci.yaml000066400000000000000000000005471513574474500153230ustar00rootroot00000000000000version: "2" linters: enable: - revive - usestdlibvars settings: revive: rules: - name: "exported" disabled: true staticcheck: checks: - all - -QF1003 - -ST1000 - -ST1003 - -ST1005 - -ST1020 - -ST1021 - -ST1022 formatters: enable: - gofmt moor-2.10.3/.whitesource000066400000000000000000000002111513574474500151240ustar00rootroot00000000000000{ "generalSettings": { "shouldScanRepo": true }, "checkRunSettings": { "vulnerableCheckRunConclusionLevel": "failure" } }moor-2.10.3/Dockerfile-test-386000066400000000000000000000002771513574474500160630ustar00rootroot00000000000000# Run the tests in 32 bit mode: # docker build . -f Dockerfile-test-386 FROM golang:1.20 WORKDIR /moor COPY go.mod go.sum ./ RUN go mod download COPY . . RUN GOARCH=386 go test -v ./... moor-2.10.3/LICENSE000066400000000000000000000027621513574474500136040ustar00rootroot00000000000000Copyright (c) 2013, johan.walles@gmail.com All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. moor-2.10.3/MOUSE.md000066400000000000000000000125401513574474500140040ustar00rootroot00000000000000# Mouse Scrolling vs Selecting and Copying NOTE: `moor` now adapts to your terminal's capabilities in a lot of cases (look for `terminalHasArrowKeysEmulation()`). But if mouse scrolling and / or copying doesn't work, read on! `moor` supports two mouse modes (using the `--mousemode` parameter): - `scroll` makes `moor` process mouse events from your terminal, thus enabling mouse scrolling work, but disabling the ability to select text with mouse in the usual way. Selecting text will require using your terminal's capability to bypass mouse protocol. Most terminals support this capability, see [Selection workarounds for `scroll` mode](#mouse-selection-workarounds-for-scroll-mode) for details. - `select` makes `moor` not process mouse events. This makes selecting and copying text work, but scrolling might not be possible, depending on your terminal and its configuration. - `auto` uses `select` on terminals where we know it won't break scrolling, and `scroll` on all others. [The white list lives in the `terminalHasArrowKeysEmulation()` function in `screen.go`](https://github.com/walles/moor/blob/master/twin/screen.go). The reason these tradeoffs exist is that if `moor` requests mouse events from the terminal, it should process _all_ mouse events, including attempts to select text. This is the case with every console application. However, some terminals can send "fake" arrow key presses to applications which _do not_ request processing mouse events. This means that on those terminals, you will be better off using `--mousemode select` option, given that you also have this feature enabled (it's usually on by default). With this setup, both scrolling and text selecting in the usual way will work. To check whether this could work, simply run `moor` with option `--mousemode select` and see if scrolling still works. ## Mouse Selection Workarounds for `scroll` Mode Most terminals implement a way to suppress mouse events capturing by applications, thus allowing you to select text even in those applications which make use of the mouse. Usually this involves selecting with Shift being held. Often the modifier key is configurable. Some other terminals allow setting options for specific types of mouse events to be reported. While the table below attempts to list the default behaviours of some common terminals, you should consult documentation of the one you're using to get detailed up-to-date information. If your favorite terminal is missing, feel free to add it. > :warning: With some of these, if you made incorrect selection you can cancel it either with an Escape key press or with a mouse > click on text area. You will probably need to still hold the modifier key for this, as hitting Escape without it will likely exit `moor`. | Terminal | Solution | | -------- | -------- | | Contour | [Use Shift](https://github.com/contour-terminal/contour/blob/cf434eaae4b428228413039624231ad0a4e6839b/docs/configuration/advanced/mouse.md) when selecting with mouse.
*Cred to @postsolar for this tip.* | | Foot | [Use Shift](https://codeberg.org/dnkl/foot/wiki#i-can-t-use-the-mouse-to-select-text) when selecting with mouse.
*Cred to @postsolar for this tip.* | | Terminal on macOS | On a laptop: Hold down the fn key when selecting with mouse. | # `less`' screen initialization sequence Recorded using [iTerm's _Automatically log session input to files_ feature](https://iterm2.com/documentation-preferences-profiles-session.html). `less` is version 487 that comes with macOS 11.3 Big Sur. All linebreaks are mine, added for readability. The `^M`s are not. ``` less /etc/passwd ^G[30m(B[m^M [?1049h [?1h =^M ## ``` # `moor`'s screen initialization sequence ``` moor /etc/passwd /Users/johan/src/moor ^G[30m(B[m^M [?1049h [?1006;1000h [?25l [1;1H [m[2m 1 [22m## ``` # Analysis of `less` The line starting with `^G` is probably from from [`fish`](https://fishshell.com/) since it's the same for both `less` and `moor`. `[?1049h` switches to the Alternate Screen Buffer, [search here for `1 0 4 9`](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer) for info. Then `less` does `[?1h`, which apparently is [DECCKM Cursor Keys Mode, send ESC O A for cursor up](https://www.real-world-systems.com/docs/ANSIcode.html), followed by `=`, meaning [DECKPAM - Set keypad to applications mode (ESCape instead of digits)](https://www.real-world-systems.com/docs/ANSIcode.html). **NOTE** that this means that `less` version 487 that comes with macOS 11.3 Big Sur doesn't even try to enable any mouse reporting, but relies on the terminal to convert scroll wheel events into arrow keypresses. # Analysis of `moor` Same as `less` up until the Alternate Screen Buffer is enabled. `[?1006;1000h` enables [SGR Mouse Mode and the X11 xterm mouse protocol (search for `1 0 0 0`)](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html). `[?25l` [hides the cursor](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html). **NOTE** Maybe we don't need this? It might be implicit when we enable the Alternate Screen Buffer. `[1;1H` [moves the cursor to the top left corner](). Then it's the first line with its line number in faint type. moor-2.10.3/README.md000066400000000000000000000232611513574474500140530ustar00rootroot00000000000000Note: :warning: [As of version 2.0.0, `moar` has been renamed to `moor`](https://github.com/walles/moor/releases/tag/v2.0.0), but is otherwise the same tool. [![Linux CI](https://github.com/walles/moor/actions/workflows/linux-ci.yml/badge.svg?branch=master)](https://github.com/walles/moor/actions/workflows/linux-ci.yml?query=branch%3Amaster) [![Windows CI](https://github.com/walles/moor/actions/workflows/windows-ci.yml/badge.svg?branch=master)](https://github.com/walles/moor/actions/workflows/windows-ci.yml?query=branch%3Amaster) Moor is a pager. It reads and displays UTF-8 encoded text from files or pipes. `moor` is designed to just do the right thing without any configuration: ![Moor displaying its own source code](screenshot.png) The intention is that Moor should be trivial to get into if you have previously been using [Less](http://www.greenwoodsoftware.com/less/). If you come from Less and find Moor confusing or hard to migrate to, [please report it](https://github.com/walles/moor/issues)! Doing the right thing includes: - **Syntax highlight** source code by default using [Chroma](https://github.com/alecthomas/chroma) - **Search is incremental** / find-as-you-type just like in [Chrome](http://www.google.com/chrome) or [Emacs](http://www.gnu.org/software/emacs/) - **Filtering is incremental**: Press & to filter the input interactively - Search becomes case sensitive if you add any UPPER CASE characters to your search terms, just like in Emacs - [Regexp](http://en.wikipedia.org/wiki/Regular_expression#Basic_concepts) search if your search string is a valid regexp - Deduplicated search history persists across `moor` invocations - **Snappy UI** even on slow / large input by reading input in the background and using multi-threaded search - Supports displaying ANSI color coded texts (like the output from `git diff` [| `riff`](https://github.com/walles/riff) for example) - Supports UTF-8 input and output - **Transparent decompression** when viewing [compressed text files](https://github.com/walles/moor/issues/97#issuecomment-1191415680) (`.gz`, `.bz2`, `.xz`, `.zst`, `.zstd`) or [streams](https://github.com/walles/moor/issues/261) - The position in the file is always shown - Supports **word wrapping** (on actual word boundaries) if requested using `--wrap` or by pressing w - [**Follows output** as long as you are on the last line](https://github.com/walles/moor/issues/108#issuecomment-1331743242), just like `tail -f` - Renders [terminal hyperlinks](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) properly - **Mouse Scrolling** works out of the box (but [look here for tradeoffs](https://github.com/walles/moor/blob/master/MOUSE.md)) [For compatibility reasons](https://github.com/walles/moor/issues/14), `moor` uses the formats declared in these environment variables if present: - `LESS_TERMCAP_md`: Man page bold - `LESS_TERMCAP_us`: Man page underline - `LESS_TERMCAP_so`: [Status bar and search hits](https://github.com/walles/moor/issues/114) Setting `LESSSECURE` to `1` will prevent `moor` from launching external programs or opening new files [as required by `systemctl(1)`][systemctlLessSecure]. In secure mode, the v command for opening the current file in an editor is disabled. For configurability reasons, `moor` reads extra command line options from the `MOOR` environment variable. Moor is used as the default pager by: - [`px` / `ptop`](https://github.com/walles/px), `ps` and `top` for human beings - [`riff`](https://github.com/walles/riff), a diff filter highlighting which line parts have changed # Installing ## Using [Homebrew](https://brew.sh/) **Both macOS and Linux** users can use Homebrew to install. See below for distro specific instructions. ```sh brew install moor ``` Then whenever you want to upgrade to the latest release: ```sh brew upgrade ``` ## Using [MacPorts](https://www.macports.org/) ```sh sudo port install moor ``` More info [here](https://ports.macports.org/port/moor/). ## Using [Gentoo](https://gentoo.org/) ```sh emerge --ask --verbose sys-apps/moor ``` More info [here](https://packages.gentoo.org/packages/sys-apps/moor). ## Using [Arch Linux](https://archlinux.org/) ```sh pacman -S moor ``` More info [here](https://archlinux.org/packages/extra/x86_64/moor/). ## Debian / Ubuntu In progress: https://ftp-master.debian.org/new.html In the mean time, use Homebrew (see above) or read on for manual install instructions. ## Manual Install ### Using `go` This will [install `moor` into `$GOPATH/bin`](<(https://manpages.debian.org/testing/golang-go/go-install.1.en.html)>) : ```sh go install github.com/walles/moor/v2/cmd/moor@latest ``` NOTE: If you got here because there is no binary for your platform, [please consider packaging `moor`](#packaging). ### Downloading binaries 1. Download `moor` for your platform from 1. `chmod a+x moor-*-*-*` 1. `sudo mv moor-*-*-* /usr/local/bin/moor` And now you can just invoke `moor` from the prompt! Try `moor --help` to see options. # Configuring Do `moor --help` for an up to date list of options. Environment variable `MOOR` can be used to set default options. For example: ```bash export MOOR='--statusbar=bold --no-linenumbers' ``` ## Setting `moor` as your default pager Set it as your default pager by adding... ```bash export PAGER=/usr/local/bin/moor ``` ... to your `.bashrc`. # Issues Issues are tracked [here](https://github.com/walles/moor/issues), or you can send questions to . # Packaging If you package `moor`, do include [the man page](moor.1) in your package. # Embedding `moor` in your app API Reference: https://pkg.go.dev/github.com/walles/moor/v2/pkg/moor For a quick start, first fetch your dependency: ``` go get github.com/walles/moor/v2 ``` Then, here's how you can use the API: ```go package main import ( "github.com/walles/moor/v2/pkg/moor" ) func main() { err := moor.PageFromString("Hello, world!", moor.Options{}) if err != nil { // Handle paging problems panic(err) } } ``` After both `go get` is done and you have calls to `moor` in your code, you may have to: ``` go mod tidy ``` You can also `PageFromStream()` or `PageFromFile()`. # Developing You need the [go tools](https://golang.org/doc/install). Run tests: ```bash ./test.sh ``` Launch the manual test suite: ```bash ./manual-test.sh ``` To run tests in 32 bit mode, either do `GOARCH=386 ./test.sh` if you're on Linux, or `docker build . -f Dockerfile-test-386` (tested on macOS). Run microbenchmarks: ```bash go test -benchmem -run='^$' -bench=. ./... ``` Profiling `BenchmarkPlainTextSearch()`. Try replacing `-alloc_objects` with `-alloc_space` or change the `-focus` function: ```bash go test -memprofilerate=1 -memprofile=profile.out -benchmem -run='^$' -bench '^BenchmarkPlainTextSearch$' ./internal && go tool pprof -alloc_objects -focus findFirstHit -relative_percentages -web profile.out ``` Or to get a CPU profile: ```bash go test -cpuprofile=profile.out -benchmem -run='^$' -bench '^BenchmarkRenderLines$' ./internal && go tool pprof -focus renderLines -relative_percentages -web profile.out ``` Build + run: ```bash ./moor.sh ... ``` Install (into `/usr/local/bin`) from source: ```bash ./install.sh ``` # Making a new Release Make sure that [screenshot.png](screenshot.png) matches moor's current UI. If it doesn't, scale a window to 81x16 characters and make a new one. Execute `release.sh` and follow instructions. # TODO - Enable exiting using ^c (without restoring the screen). - Enable suspending using ^z, followed by resuming using `fg`. - Underline the file name in the status bar while viewing. The point is to make it more obvious where this name ends in case it contains whitespace. - Retain the search string when pressing / to search a second time. ## Done - Add `>` markers at the end of lines being cut because they are too long - Doing moor on an arbitrary binary (like `/bin/ls`) should put all line-continuation markers at the rightmost column. This really means our truncation code must work even with things like tabs and various control characters. - Make sure search hits are highlighted even when we have to scroll right to see them - Change out-of-file visualization to writing `---` after the end of the file and leaving the rest of the screen blank. - Exit search on pressing up / down / pageup / pagedown keys and scroll. I attempted to do that spontaneously, so it's probably a good idea. - Remedy all FIXMEs in this README file - Release the `go` version as the new `moor`, replacing the previous Ruby implementation - Add licensing information (same as for the Ruby branch) - Make sure `git grep` output gets highlighted properly. - Handle all kinds of line endings. - Make sure version information is printed if there are warnings. - Add spinners while file is still loading - Make `tail -f /dev/null` exit properly, fix . - Showing unicode search hits should highlight the correct chars - [Word wrap text rather than character wrap it](m/linewrapper.go). - Arrow keys up / down while in line wrapping mode should scroll by screen line, not by input file line. - Define 'g' to prompt for a line number to go to. - Handle search hits to the right of the right screen edge when searching forwards. Searching forwards now moves first right, then to the left edge and down. - Handle search hits to the right of the right screen edge when searching backwards. Searching backwards should move first left, then up and to the rightmost hit. [systemctlLessSecure]: https://github.com/systemd/systemd/blob/ee3cd7890d81744efa9513b739e5ff03c9e7649b/man/common-variables.xml#L193-L194 moor-2.10.3/build.sh000077500000000000000000000023511513574474500142270ustar00rootroot00000000000000#!/bin/bash set -e -o pipefail if [ -z ${CI+x} ]; then # Local build, not in CI, format source gofmt -s -w . fi VERSION="$(git describe --tags --dirty --always)" BINARY="moor" if [ -n "${GOOS}${GOARCH}" ]; then EXE="" if [ "${GOOS}" = "windows" ]; then EXE=".exe" fi BINARY="releases/${BINARY}-${VERSION}-${GOOS}-${GOARCH}${EXE}" fi # Linker flags version number trick below from here: # https://www.reddit.com/r/golang/comments/4cpi2y/question_where_to_keep_the_version_number_of_a_go/d1kbap7?utm_source=share&utm_medium=web2x # Linker flags -s and -w strips debug data, but keeps whatever is needed for # proper panic backtraces, this makes binaries smaller: # https://boyter.org/posts/trimming-golang-binary-fat/ # This line must be last in the script so that its return code # propagates properly to its caller # # Note that ${RACE} must *not* be quoted, we want it to disappear if empty. # shellcheck disable=SC2086 go build ${RACE} -trimpath -ldflags="-s -w -X main.versionString=${VERSION}" -o "${BINARY}" ./cmd/moor # Alternative build line, if you want to attach to the running process in the Go debugger: # go build -ldflags="-X main.versionString=${VERSION}" -gcflags="all=-N -l" -o "${BINARY}" ./cmd/moor moor-2.10.3/cmd/000077500000000000000000000000001513574474500133335ustar00rootroot00000000000000moor-2.10.3/cmd/moor/000077500000000000000000000000001513574474500143075ustar00rootroot00000000000000moor-2.10.3/cmd/moor/moor.go000066400000000000000000000550351513574474500156220ustar00rootroot00000000000000// Command line launcher for moor package main import ( "flag" "fmt" "io" "os" "runtime" "runtime/debug" "strconv" "strings" "time" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/formatters" "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" log "github.com/sirupsen/logrus" "golang.org/x/term" "github.com/walles/moor/v2/internal" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/internal/util" "github.com/walles/moor/v2/twin" ) var versionString = "" // Which environment variable should we get our config from? // // Prefer MOOR, but if that's not set, look at MOAR as well for backwards // compatibility reasons. func moorEnvVarName() string { moorEnvSet := len(strings.TrimSpace(os.Getenv("MOOR"))) > 0 if moorEnvSet { return "MOOR" } moarEnvSet := len(strings.TrimSpace(os.Getenv("MOAR"))) > 0 if moarEnvSet { // Legacy, keep for backwards compatibility return "MOAR" } // This is the default return "MOOR" } // printProblemsHeader prints bug reporting information to stderr func printProblemsHeader() { fmt.Fprintln(os.Stderr, "Please post the following report at ,") fmt.Fprintln(os.Stderr, "or e-mail it to johan.walles@gmail.com.") fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr, "Version :", getVersion()) fmt.Fprintln(os.Stderr, "LANG :", os.Getenv("LANG")) fmt.Fprintln(os.Stderr, "TERM :", os.Getenv("TERM")) fmt.Fprintln(os.Stderr, "EDITOR :", os.Getenv("EDITOR")) fmt.Fprintln(os.Stderr, "TERM_PROGRAM :", os.Getenv("TERM_PROGRAM")) fmt.Fprintln(os.Stderr) lessenv_section := "" lessTermcapVars := []string{ "LESS_TERMCAP_md", "LESS_TERMCAP_us", "LESS_TERMCAP_so", } for _, varName := range lessTermcapVars { value := os.Getenv(varName) if value == "" { continue } lessenv_section += varName + " : " + strings.ReplaceAll(value, "\x1b", "ESC") + "\n" } if lessenv_section != "" { fmt.Fprintln(os.Stderr, lessenv_section) } fmt.Fprintln(os.Stderr, "GOOS :", runtime.GOOS) fmt.Fprintln(os.Stderr, "GOARCH :", runtime.GOARCH) fmt.Fprintln(os.Stderr, "Compiler:", runtime.Compiler) fmt.Fprintln(os.Stderr, "NumCPU :", runtime.NumCPU()) fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr, "Stdin is a terminal:", term.IsTerminal(int(os.Stdin.Fd()))) fmt.Fprintln(os.Stderr, "Stdout is a terminal:", term.IsTerminal(int(os.Stdout.Fd()))) fmt.Fprintln(os.Stderr) if moorEnvVarName() == "MOAR" { fmt.Fprintln(os.Stderr, "MOAR (legacy):", os.Getenv("MOAR")) } else { fmt.Fprintln(os.Stderr, "MOOR:", os.Getenv("MOOR")) } fmt.Fprintf(os.Stderr, "Commandline: %#v\n", os.Args) } func parseLexerOption(lexerOption string) (chroma.Lexer, error) { byMimeType := lexers.MatchMimeType(lexerOption) if byMimeType != nil { return byMimeType, nil } // Use Chroma's built-in fuzzy lexer picker lexer := lexers.Get(lexerOption) if lexer != nil { return lexer, nil } return nil, fmt.Errorf( "Look here for inspiration: https://github.com/alecthomas/chroma/tree/master/lexers/embedded", ) } func parseStyleOption(styleOption string) (*chroma.Style, error) { style, ok := styles.Registry[styleOption] if !ok { return &chroma.Style{}, fmt.Errorf( "Pick a style from here: https://xyproto.github.io/splash/docs/longer/all.html") } return style, nil } func parseColorsOption(colorsOption string) (twin.ColorCount, error) { if strings.ToLower(colorsOption) == "auto" { colorsOption = "16M" if os.Getenv("COLORTERM") != "truecolor" && strings.Contains(os.Getenv("TERM"), "256") { // Covers "xterm-256color" as used by the macOS Terminal colorsOption = "256" } } switch strings.ToUpper(colorsOption) { case "8": return twin.ColorCount8, nil case "16": return twin.ColorCount16, nil case "256": return twin.ColorCount256, nil case "16M": return twin.ColorCount24bit, nil } var noColor twin.ColorCount return noColor, fmt.Errorf("Valid counts are 8, 16, 256, 16M or auto") } func parseStatusBarStyle(styleOption string) (internal.StatusBarOption, error) { if styleOption == "inverse" { return internal.STATUSBAR_STYLE_INVERSE, nil } if styleOption == "plain" { return internal.STATUSBAR_STYLE_PLAIN, nil } if styleOption == "bold" { return internal.STATUSBAR_STYLE_BOLD, nil } return 0, fmt.Errorf("Good ones are inverse, plain and bold") } func parseUnprintableStyle(styleOption string) (textstyles.UnprintableStyleT, error) { if styleOption == "highlight" { return textstyles.UnprintableStyleHighlight, nil } if styleOption == "whitespace" { return textstyles.UnprintableStyleWhitespace, nil } return 0, fmt.Errorf("Good ones are highlight or whitespace") } func parseScrollHint(scrollHint string) (textstyles.CellWithMetadata, error) { scrollHint = strings.ReplaceAll(scrollHint, "ESC", "\x1b") parsedTokens := textstyles.StyledRunesFromString(twin.StyleDefault, scrollHint, nil, 0).StyledRunes if len(parsedTokens) == 1 { return parsedTokens[0], nil } return textstyles.CellWithMetadata{}, fmt.Errorf("Expected exactly one (optionally highlighted) character. For example: 'ESC[2m…'") } func parseShiftAmount(shiftAmount string) (uint, error) { value, err := strconv.ParseUint(shiftAmount, 10, 32) if err != nil { return 0, err } if value < 1 { return 0, fmt.Errorf("Shift amount must be at least 1") } // Let's add an upper bound as well if / when requested return uint(value), nil } func parseTabAmount(tabAmount string) (uint, error) { value, err := strconv.ParseUint(tabAmount, 10, 32) if err != nil { return 0, err } if value < 1 { return 0, fmt.Errorf("Tab size must be at least 1") } // Let's add an upper bound as well if / when requested return uint(value), nil } func parseMouseMode(mouseMode string) (twin.MouseMode, error) { switch mouseMode { case "auto": return twin.MouseModeAuto, nil case "select", "mark": return twin.MouseModeSelect, nil case "scroll": return twin.MouseModeScroll, nil } return twin.MouseModeAuto, fmt.Errorf("Valid modes are auto, select and scroll") } func pumpToStdout(inputFilenames ...string) error { if len(inputFilenames) > 0 { stdinDone := false // If we get both redirected stdin and an input filenames, should only // copy the files and ignore stdin, because that's how less works. for _, inputFilename := range inputFilenames { if inputFilename == "-" && !term.IsTerminal(int(os.Stdin.Fd())) { // "-" with redirected stdin means "read from stdin" if stdinDone { // stdin already drained, don't do it again continue } _, err := io.Copy(os.Stdout, os.Stdin) if err != nil { return fmt.Errorf("Failed to copy stdin to stdout: %w", err) } stdinDone = true continue } inputFile, _, err := reader.ZOpen(inputFilename) if err != nil { return fmt.Errorf("Failed to open %s: %w", inputFilename, err) } _, err = io.Copy(os.Stdout, inputFile) if err != nil { return fmt.Errorf("Failed to copy %s to stdout: %w", inputFilename, err) } } return nil } // No input filenames, pump stdin to stdout _, err := io.Copy(os.Stdout, os.Stdin) if err != nil { return fmt.Errorf("Failed to copy stdin to stdout: %w", err) } return nil } // Parses an argument like "+123" anywhere on the command line into a one-based // line number, and returns the remaining args. // // Returns nil on no target line number specified. func getTargetLine(args []string) (*linemetadata.Index, []string) { for i, arg := range args { if !strings.HasPrefix(arg, "+") { continue } lineNumber, err := strconv.ParseInt(arg[1:], 10, 32) if err != nil { // Let's pretend this is a file name continue } if lineNumber < 0 { // Pretend this is a file name continue } // Remove the target line number from the args // // Ref: https://stackoverflow.com/a/57213476/473672 remainingArgs := make([]string, 0) remainingArgs = append(remainingArgs, args[:i]...) remainingArgs = append(remainingArgs, args[i+1:]...) if lineNumber == 0 { // Ignore +0 because that's what less does: // https://github.com/walles/moor/issues/316 return nil, remainingArgs } returnMe := linemetadata.IndexFromOneBased(int(lineNumber)) return &returnMe, remainingArgs } return nil, args } func russiaNotSupported() { if !strings.HasPrefix(strings.ToLower(os.Getenv("LANG")), "ru_ru") { // Not russia return } if os.Getenv("CRIMEA") == "Ukraine" { // It is! return } fmt.Fprintln(os.Stderr, "ERROR: russia not supported (but Russian is!)") fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr, "Options for using moor in Russian:") fmt.Fprintln(os.Stderr, "* Change your language setting to ru_UA.UTF-8") fmt.Fprintln(os.Stderr, "* Set CRIMEA=Ukraine in your environment") fmt.Fprintln(os.Stderr, "* russia leaves Ukraine") os.Exit(1) } // For git output and man pages, disable line numbers by default. // // Before paging, "man" first checks the terminal width and formats the man page // to fit that width. Git does that as well for git diff --stat. // // Then, if moor adds line numbers, the rightmost part of the page will scroll // out of view. // // So we try to this, and in that case disable line numbers so that the // rightmost part of the page is visible by default. // // See also internal/haveLoadedManPage(), where we try to detect man pages by // their contents. func noLineNumbersDefault() bool { if os.Getenv("MAN_PN") != "" { // Set by "man" on Ubuntu 22.04.4 when I tested it inside of Docker. log.Debug("MAN_PN is set, skipping line numbers for man page") return true } if os.Getenv("GIT_EXEC_PATH") != "" { // Set by "git". // Neither logs nor diffs are helped by line numbers, turn them off by // default. log.Debug("GIT_EXEC_PATH is set, skipping line numbers when paging git output") return true } // Default to not skipping line numbers return false } // Return complete version when built with build.sh or fallback to module version (i.e. "go install") func getVersion() string { if versionString != "" { return versionString } info, ok := debug.ReadBuildInfo() if ok && info.Main.Version != "" && info.Main.Version != "(devel)" { return info.Main.Version } return "Should be set when building, please use build.sh to build" } // Can return a nil pager on --help or --version, or if pumping to stdout. func pagerFromArgs( args []string, newScreen func(mouseMode twin.MouseMode, terminalColorCount twin.ColorCount) (twin.Screen, error), stdinIsRedirected bool, stdoutIsRedirected bool, ) ( *internal.Pager, twin.Screen, chroma.Style, *chroma.Formatter, bool, error, ) { // FIXME: If we get a CTRL-C, get terminal back into a useful state before terminating flagSet := flag.NewFlagSet("", flag.ContinueOnError, // We want to do our own error handling ) flagSet.SetOutput(io.Discard) // We want to do our own printing printVersion := flagSet.Bool("version", false, "Prints the moor version number") debug := flagSet.Bool("debug", false, "Print debug logs after exiting") trace := flagSet.Bool("trace", false, "Print trace logs after exiting") wrap := flagSet.Bool("wrap", false, "Wrap long lines") follow := flagSet.Bool("follow", false, "Follow piped input just like \"tail -f\"") styleOption := flagSetFunc(flagSet, "style", nil, "Highlighting `style` from https://xyproto.github.io/splash/docs/longer/all.html", parseStyleOption) lexer := flagSetFunc(flagSet, "lang", nil, "File contents, used for highlighting. Mime type or file extension (\"html\"). Default is to guess by filename.", parseLexerOption) terminalFg := flagSet.Bool("terminal-fg", false, "Use terminal foreground color rather than style foreground for plain text") noSearchLineHighlight := flagSet.Bool("no-search-line-highlight", false, "Do not highlight the background of lines with search hits") defaultFormatter, err := parseColorsOption("auto") if err != nil { panic(fmt.Errorf("Failed parsing default formatter: %w", err)) } terminalColorsCount := flagSetFunc(flagSet, "colors", defaultFormatter, "Highlighting palette size: 8, 16, 256, 16M, auto", parseColorsOption) noLineNumbers := flagSet.Bool("no-linenumbers", noLineNumbersDefault(), "Hide line numbers on startup, press left arrow key to show") noStatusBar := flagSet.Bool("no-statusbar", false, "Hide the status bar, toggle with '='") reFormat := flagSet.Bool("reformat", false, "Reformat some input files (JSON)") flagSet.Bool("no-reformat", true, "No effect, kept for compatibility. See --reformat") quitIfOneScreen := flagSet.Bool("quit-if-one-screen", false, "Don't page if contents fits on one screen. Affected by --no-clear-on-exit-margin.") noClearOnExit := flagSet.Bool("no-clear-on-exit", false, "Retain screen contents when exiting moor") noClearOnExitMargin := flagSet.Int("no-clear-on-exit-margin", 1, "Number of lines to leave for your shell prompt, defaults to 1") statusBarStyle := flagSetFunc(flagSet, "statusbar", internal.STATUSBAR_STYLE_INVERSE, "Status bar `style`: inverse, plain or bold", parseStatusBarStyle) unprintableStyle := flagSetFunc(flagSet, "render-unprintable", textstyles.UnprintableStyleHighlight, "How unprintable characters are rendered: highlight or whitespace", parseUnprintableStyle) scrollLeftHint := flagSetFunc(flagSet, "scroll-left-hint", textstyles.CellWithMetadata{Rune: '<', Style: twin.StyleDefault.WithAttr(twin.AttrReverse)}, "Shown when view can scroll left. One character with optional ANSI highlighting.", parseScrollHint) scrollRightHint := flagSetFunc(flagSet, "scroll-right-hint", textstyles.CellWithMetadata{Rune: '>', Style: twin.StyleDefault.WithAttr(twin.AttrReverse)}, "Shown when view can scroll right. One character with optional ANSI highlighting.", parseScrollHint) shift := flagSetFunc(flagSet, "shift", 16, "Horizontal scroll `amount` >=1, defaults to 16", parseShiftAmount) tabSize := flagSetFunc(flagSet, "tab-size", 8, "Number of spaces per tab stop, defaults to 8", parseTabAmount) mouseMode := flagSetFunc( flagSet, "mousemode", twin.MouseModeAuto, "Mouse `mode`: auto, select or scroll: https://github.com/walles/moor/blob/master/MOUSE.md", parseMouseMode, ) // Combine flags from environment and from command line flags := args[1:] moorEnv := strings.TrimSpace(os.Getenv(moorEnvVarName())) if len(moorEnv) > 0 { // FIXME: It would be nice if we could debug log that we're doing this, // but logging is not yet set up and depends on command line parameters. flags = append(strings.Fields(moorEnv), flags...) } targetLine, remainingArgs := getTargetLine(flags) err = flagSet.Parse(remainingArgs) if err == nil { if *noClearOnExitMargin < 0 { err = fmt.Errorf("Invalid --no-clear-on-exit-margin %d, must be 0 or higher", *noClearOnExitMargin) } } if err != nil { if err == flag.ErrHelp { printUsage(flagSet, *terminalColorsCount) return nil, nil, chroma.Style{}, nil, false, nil } errorText := err.Error() if strings.HasPrefix(errorText, "invalid value") { errorText = strings.Replace(errorText, ": ", "\n\n", 1) } boldErrorMessage := "\x1b[1m" + errorText + "\x1b[m" fmt.Fprintln(os.Stderr, "ERROR:", boldErrorMessage) fmt.Fprintln(os.Stderr) printCommandline(os.Stderr) fmt.Fprintln(os.Stderr, "For help, run: \x1b[1mmoor --help\x1b[m") os.Exit(1) } logsRequested := *debug || *trace if *printVersion { fmt.Println(getVersion()) return nil, nil, chroma.Style{}, nil, logsRequested, nil } log.SetLevel(log.InfoLevel) if *trace { log.SetLevel(log.TraceLevel) } else if *debug { log.SetLevel(log.DebugLevel) } log.SetFormatter(&log.TextFormatter{ TimestampFormat: time.StampMicro, }) flagSetArgs := flagSet.Args() if stdinIsRedirected && len(flagSetArgs) == 0 { // "-" is special if stdin is redirected, means "read from stdin" // // Ref: https://github.com/walles/moor/issues/162 flagSetArgs = []string{"-"} } // Check that any input files can be opened for _, inputFilename := range flagSetArgs { if stdinIsRedirected && inputFilename == "-" { // stdin can be opened continue } // Need to check before newScreen() below, otherwise the screen // will be cleared before we print the "No such file" error. err := reader.TryOpen(inputFilename) if err != nil { return nil, nil, chroma.Style{}, nil, logsRequested, err } } if len(flagSetArgs) == 0 && !stdinIsRedirected { fmt.Fprintln(os.Stderr, "ERROR: Filename(s) or input pipe required (\"moor file.txt\")") fmt.Fprintln(os.Stderr) printCommandline(os.Stderr) fmt.Fprintln(os.Stderr, "For help, run: \x1b[1mmoor --help\x1b[m") os.Exit(1) } if stdoutIsRedirected { err := pumpToStdout(flagSetArgs...) if err != nil { return nil, nil, chroma.Style{}, nil, logsRequested, err } return nil, nil, chroma.Style{}, nil, logsRequested, nil } // INVARIANT: At this point, stdout is a terminal and we should proceed with // paging. stdoutIsTerminal := !stdoutIsRedirected if !stdoutIsTerminal { panic("Invariant broken: stdout is not a terminal") } formatter := formatters.TTY256 switch *terminalColorsCount { case twin.ColorCount8: formatter = formatters.TTY8 case twin.ColorCount16: formatter = formatters.TTY16 case twin.ColorCount24bit: formatter = formatters.TTY16m } var readerImpls []*reader.ReaderImpl shouldFormat := *reFormat readerOptions := reader.ReaderOptions{Lexer: *lexer, ShouldFormat: shouldFormat} stdinName := "" if os.Getenv("PAGER_LABEL") != "" { stdinName = os.Getenv("PAGER_LABEL") } else if os.Getenv("MAN_PN") != "" { // MAN_PN is set by GNU man. Example value: "printf(1)" stdinName = os.Getenv("MAN_PN") } // Display the input file(s) contents stdinDone := false for _, inputFilename := range flagSetArgs { var readerImpl *reader.ReaderImpl var err error if stdinIsRedirected && inputFilename == "-" { if stdinDone { // stdin already drained, don't do it again continue } readerImpl, err = reader.NewFromStream(stdinName, os.Stdin, formatter, readerOptions) if err != nil { return nil, nil, chroma.Style{}, nil, logsRequested, err } // If the user is doing "sudo something | moor" we can't show the UI until // we start getting data, otherwise we'll mess up sudo's password prompt. readerImpl.AwaitFirstByte() stdinDone = true } else { readerImpl, err = reader.NewFromFilename(inputFilename, formatter, readerOptions) } if err != nil { return nil, nil, chroma.Style{}, nil, logsRequested, err } readerImpls = append(readerImpls, readerImpl) } // We got the first byte, this means sudo is done (if it was used) and we // can set up the UI. screen, err := newScreen(*mouseMode, *terminalColorsCount) if err != nil { // Ref: https://github.com/walles/moor/issues/149 log.Info("Failed to set up screen for paging, pumping to stdout instead: ", err) for _, readerImpl := range readerImpls { readerImpl.PumpToStdout() } return nil, nil, chroma.Style{}, nil, logsRequested, nil } var style chroma.Style if *styleOption == nil { style = internal.GetStyleForScreen(screen) } else { style = **styleOption } log.Debug("Using style <", style.Name, ">") for _, readerImpl := range readerImpls { readerImpl.SetStyleForHighlighting(style) } pager := internal.NewPager(readerImpls...) pager.WrapLongLines = *wrap pager.ShowLineNumbers = !*noLineNumbers pager.ShowStatusBar = !*noStatusBar pager.DeInit = !*noClearOnExit pager.DeInitFalseMargin = *noClearOnExitMargin pager.QuitIfOneScreen = *quitIfOneScreen pager.StatusBarStyle = *statusBarStyle pager.UnprintableStyle = *unprintableStyle pager.WithTerminalFg = *terminalFg pager.ScrollLeftHint = *scrollLeftHint pager.ScrollRightHint = *scrollRightHint pager.SideScrollAmount = int(*shift) pager.TabSize = int(*tabSize) pager.WithSearchHitLineBackground = !*noSearchLineHighlight pager.TargetLine = targetLine if *follow && pager.TargetLine == nil { reallyHigh := linemetadata.IndexMax() pager.TargetLine = &reallyHigh } return pager, screen, style, &formatter, logsRequested, nil } func main() { var loglines internal.LogWriter logsRequested := false log.SetOutput(&loglines) twin.SetLogger(&util.TwinLogger{}) russiaNotSupported() defer func() { err := recover() haveLogsToShow := len(loglines.String()) > 0 && logsRequested if err == nil && !haveLogsToShow { // No problems return } printProblemsHeader() if len(loglines.String()) > 0 { fmt.Fprintln(os.Stderr) // Consider not printing duplicate log messages more than once fmt.Fprintf(os.Stderr, "%s", loglines.String()) } if err != nil { fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr, "Panic recovery timestamp:", time.Now().String()) fmt.Fprintln(os.Stderr) panic(err) } // We were asked to print logs, and we did. Success! os.Exit(0) }() stdinIsRedirected := !term.IsTerminal(int(os.Stdin.Fd())) stdoutIsRedirected := !term.IsTerminal(int(os.Stdout.Fd())) pager, screen, style, formatter, _logsRequested, err := pagerFromArgs( os.Args, twin.NewScreenWithMouseModeAndColorCount, stdinIsRedirected, stdoutIsRedirected, ) logsRequested = _logsRequested if err != nil { fmt.Fprintln(os.Stderr, "ERROR:", err) os.Exit(1) } if pager == nil { // No pager, we're done return } startPaging(pager, screen, &style, formatter) } // Define a generic flag with specified name, default value, and usage string. // The return value is the address of a variable that stores the parsed value of // the flag. func flagSetFunc[T any](flagSet *flag.FlagSet, name string, defaultValue T, usage string, parser func(valueString string) (T, error)) *T { parsed := defaultValue flagSet.Func(name, usage, func(valueString string) error { parseResult, err := parser(valueString) if err != nil { return err } parsed = parseResult return nil }) return &parsed } func startPaging(pager *internal.Pager, screen twin.Screen, chromaStyle *chroma.Style, chromaFormatter *chroma.Formatter) { defer func() { panicMessage := recover() if panicMessage != nil { // Clarify that any screen shutdown logs are from panic handling, // not something the user or some external thing did. log.Info("Panic detected, closing screen before informing the user...") } // Restore screen... screen.Close() // ... before printing any panic() output, otherwise the output will // have broken linefeeds and be hard to follow. if panicMessage != nil { panic(panicMessage) } if !pager.DeInit { pager.ReprintAfterExit() } if pager.AfterExit != nil { err := pager.AfterExit() if err != nil { log.Error("Failed running AfterExit hook: ", err) } } }() pager.StartPaging(screen, chromaStyle, chromaFormatter) } moor-2.10.3/cmd/moor/moor_test.go000066400000000000000000000027361513574474500166610ustar00rootroot00000000000000package main import ( "testing" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) func TestParseScrollHint(t *testing.T) { token, err := parseScrollHint("ESC[7m>") assert.NilError(t, err) assert.Equal(t, token, textstyles.CellWithMetadata{ Rune: '>', Style: twin.StyleDefault.WithAttr(twin.AttrReverse), }) } func TestPageOneInputFile(t *testing.T) { pager, screen, _, formatter, _, err := pagerFromArgs( []string{"", "moor_test.go"}, func(_ twin.MouseMode, _ twin.ColorCount) (twin.Screen, error) { return twin.NewFakeScreen(80, 24), nil }, false, // stdin is redirected false, // stdout is redirected ) assert.NilError(t, err) assert.Assert(t, pager != nil) assert.Assert(t, screen != nil) assert.Assert(t, formatter != nil) } func TestGetTargetLine(t *testing.T) { index, remaining := getTargetLine([]string{}) assert.Assert(t, index == nil) assert.DeepEqual(t, remaining, []string{}) index, remaining = getTargetLine([]string{"+"}) assert.Assert(t, index == nil) assert.DeepEqual(t, remaining, []string{"+"}) // Ref: https://github.com/walles/moor/issues/316 index, remaining = getTargetLine([]string{"+0"}) assert.Assert(t, index == nil) assert.DeepEqual(t, remaining, []string{}) index, remaining = getTargetLine([]string{"+1"}) assert.Equal(t, *index, linemetadata.IndexFromOneBased(1)) assert.DeepEqual(t, remaining, []string{}) } moor-2.10.3/cmd/moor/usage.go000066400000000000000000000211151513574474500157420ustar00rootroot00000000000000package main import ( "flag" "fmt" "io" "os" "os/exec" "path/filepath" "sort" "strings" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal" "github.com/walles/moor/v2/twin" ) func renderLessTermcapEnvVar(envVarName string, description string, colors twin.ColorCount) string { value := os.Getenv(envVarName) if len(value) == 0 { return "" } style, err := internal.TermcapToStyle(value) if err != nil { bold := twin.StyleDefault.WithAttr(twin.AttrBold).RenderUpdateFrom(twin.StyleDefault, colors) notBold := twin.StyleDefault.RenderUpdateFrom(twin.StyleDefault.WithAttr(twin.AttrBold), colors) return fmt.Sprintf(" %s (%s): %s %s<- Error: %v%s\n", envVarName, description, strings.ReplaceAll(value, "\x1b", "ESC"), bold, err, notBold, ) } prefix := style.RenderUpdateFrom(twin.StyleDefault, colors) suffix := twin.StyleDefault.RenderUpdateFrom(style, colors) return fmt.Sprintf(" %s (%s): %s\n", envVarName, description, prefix+strings.ReplaceAll(value, "\x1b", "ESC")+suffix, ) } func renderPagerEnvVar(name string, colors twin.ColorCount) string { bold := twin.StyleDefault.WithAttr(twin.AttrBold).RenderUpdateFrom(twin.StyleDefault, colors) notBold := twin.StyleDefault.RenderUpdateFrom(twin.StyleDefault.WithAttr(twin.AttrBold), colors) value, isSet := os.LookupEnv(name) if value == "" { what := "unset" if isSet { what = "empty" } return fmt.Sprintf(" %s is %s %s<- Should be %s%s\n", name, what, bold, getMoorPath(), notBold, ) } absMoorPath, err := absLookPath(os.Args[0]) if err != nil { log.Warn("Unable to find absolute moor path: ", err) return "" } // You can actually have command line arguments in these variables (like // MANPAGER="moor --reformat"), so we want to look at only the first word to // find out whether this PAGER variable is pointing to the right binary. firstWord := strings.Fields(value)[0] absEnvValue, err := absLookPath(firstWord) if err != nil { // This can happen if this is set to some outdated value absEnvValue = value } if absEnvValue == absMoorPath { return fmt.Sprintf(" %s=%s\n", name, value) } return fmt.Sprintf(" %s=%s %s<- Should be %s%s\n", name, value, bold, getMoorPath(), notBold, ) } // If the environment variable is set, render it as APA=bepa indented two // spaces, plus a newline at the end. Otherwise, return an empty string. func renderPlainEnvVar(envVarName string) string { value := os.Getenv(envVarName) if value == "" { return "" } return fmt.Sprintf(" %s=%s\n", envVarName, value) } func printCommandline(output io.Writer) { envVarName := moorEnvVarName() envVarDescription := envVarName if envVarName != "MOOR" { bold := twin.StyleDefault.WithAttr(twin.AttrBold).RenderUpdateFrom(twin.StyleDefault, twin.ColorCount256) notBold := twin.StyleDefault.RenderUpdateFrom(twin.StyleDefault.WithAttr(twin.AttrBold), twin.ColorCount256) envVarDescription = envVarName + " (" + bold + "legacy, please use MOOR instead!" + notBold + ")" } fmt.Fprintln(output, "Commandline: moor", strings.Join(os.Args[1:], " ")) //nolint:errcheck fmt.Fprintf(output, "Environment: %s=\"%v\"\n", envVarDescription, os.Getenv(envVarName)) //nolint:errcheck fmt.Fprintln(output) //nolint:errcheck } func heading(text string, colors twin.ColorCount) string { style := twin.StyleDefault.WithAttr(twin.AttrItalic) prefix := style.RenderUpdateFrom(twin.StyleDefault, colors) suffix := twin.StyleDefault.RenderUpdateFrom(style, colors) return prefix + text + suffix } func printUsage(flagSet *flag.FlagSet, colors twin.ColorCount) { // This controls where PrintDefaults() prints, see below flagSet.SetOutput(os.Stdout) // FIXME: Log if any printouts fail? fmt.Println(heading("Usage", colors)) fmt.Println(" moor [options] ...") fmt.Println(" ... | moor") fmt.Println(" moor < file") fmt.Println() fmt.Println("Shows file contents. Compressed files will be transparently decompressed.") fmt.Println("Input is expected to be (possibly compressed) UTF-8 encoded text. Invalid /") fmt.Println("non-printable characters are by default rendered as '?'. Press : inside of") fmt.Println("moor to switch between files.") fmt.Println() fmt.Println("More information + source code:") fmt.Println(" ") fmt.Println() fmt.Println(heading("Environment", colors)) envVarName := moorEnvVarName() envVarValue := os.Getenv(envVarName) if len(envVarValue) == 0 { fmt.Println(" Additional options are read from the MOOR environment variable if set.") fmt.Println(" But currently, the MOOR environment variable is not set.") } else { fmt.Printf(" Additional options are read from the %s environment variable.\n", envVarName) if envVarName != "MOOR" { bold := twin.StyleDefault.WithAttr(twin.AttrBold).RenderUpdateFrom(twin.StyleDefault, colors) notBold := twin.StyleDefault.RenderUpdateFrom(twin.StyleDefault.WithAttr(twin.AttrBold), colors) fmt.Printf( " But that is going away, %splease use the MOOR environment variable instead%s!\n", bold, notBold) } fmt.Printf(" Current setting: %s=\"%s\"\n", envVarName, envVarValue) } envSection := "" envSection += renderLessTermcapEnvVar("LESS_TERMCAP_md", "man page bold style", colors) envSection += renderLessTermcapEnvVar("LESS_TERMCAP_us", "man page underline style", colors) envSection += renderLessTermcapEnvVar("LESS_TERMCAP_so", "search hits and footer style", colors) envSection += renderPagerEnvVar("PAGER", colors) envVars := os.Environ() sort.Strings(envVars) for _, env := range envVars { split := strings.SplitN(env, "=", 2) if len(split) != 2 { continue } name := split[0] if name == "PAGER" { // Already done above continue } if !strings.HasSuffix(name, "PAGER") { continue } envSection += renderPagerEnvVar(name, colors) } envSection += renderPlainEnvVar("TERM") envSection += renderPlainEnvVar("TERM_PROGRAM") envSection += renderPlainEnvVar("COLORTERM") // Requested here: https://github.com/walles/moor/issues/170#issuecomment-1891154661 envSection += renderPlainEnvVar("MANROFFOPT") if envSection != "" { fmt.Println() // Not Println since the section already ends with a newline fmt.Print(envSection) } printSetDefaultPagerHelp(colors) fmt.Println() fmt.Println(heading("Options", colors)) flagSet.PrintDefaults() fmt.Println(" +1234") fmt.Println(" \tImmediately scroll to line 1234") } // If $PAGER isn't pointing to us, print a help text on how to set it. func printSetDefaultPagerHelp(colors twin.ColorCount) { absMoorPath, err := absLookPath(os.Args[0]) if err != nil { log.Warn("Unable to find moor binary ", err) return } absPagerValue, err := absLookPath(os.Getenv("PAGER")) if err != nil { absPagerValue = "" } if absPagerValue == absMoorPath { // We're already the default pager return } fmt.Println() fmt.Println(heading("Making moor Your Default Pager", colors)) shellIsFish := strings.HasSuffix(os.Getenv("SHELL"), "fish") shellIsPowershell := len(os.Getenv("PSModulePath")) > 0 if shellIsFish { fmt.Println(" Write this command at your prompt:") fmt.Println() fmt.Printf(" set -Ux PAGER %s\n", getMoorPath()) } else if shellIsPowershell { fmt.Println(" Put the following line in your $PROFILE file (\"echo $PROFILE\" to find it)") fmt.Println(" and moor will be used as the default pager in all new terminal windows:") fmt.Println() fmt.Printf(" $env:PAGER = \"%s\"\n", getMoorPath()) } else { // I don't know how to identify bash / zsh, put generic instructions here fmt.Println(" Put the following line in your ~/.bashrc, ~/.bash_profile or ~/.zshrc") fmt.Println(" and moor will be used as the default pager in all new terminal windows:") fmt.Println() fmt.Printf(" export PAGER=%s\n", getMoorPath()) } } // "moor" if we're in the $PATH, otherwise an absolute path func getMoorPath() string { moorPath := os.Args[0] if filepath.IsAbs(moorPath) { return moorPath } if strings.Contains(moorPath, string(os.PathSeparator)) { // Relative path moorPath, err := filepath.Abs(moorPath) if err != nil { panic(err) } return moorPath } // Neither absolute nor relative, try PATH _, err := exec.LookPath(moorPath) if err != nil { panic("Unable to find in $PATH: " + moorPath) } return moorPath } func absLookPath(path string) (string, error) { lookedPath, err := exec.LookPath(path) if err != nil { return "", err } absLookedPath, err := filepath.Abs(lookedPath) if err != nil { return "", err } return absLookedPath, err } moor-2.10.3/go.mod000066400000000000000000000011361513574474500136770ustar00rootroot00000000000000module github.com/walles/moor/v2 go 1.23.12 toolchain go1.24.4 require ( github.com/adrg/xdg v0.5.3 github.com/alecthomas/chroma/v2 v2.22.0 github.com/charlievieth/strcase v0.0.5 github.com/davecgh/go-spew v1.1.1 github.com/google/go-cmp v0.5.9 github.com/klauspost/compress v1.17.4 github.com/rivo/uniseg v0.4.7 github.com/sirupsen/logrus v1.8.3 github.com/ulikunitz/xz v0.5.15 golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc golang.org/x/sys v0.26.0 golang.org/x/term v0.0.0-20210503060354-a79de5458b56 gotest.tools/v3 v3.3.0 ) require github.com/dlclark/regexp2 v1.11.5 // indirect moor-2.10.3/go.sum000066400000000000000000000146701513574474500137330ustar00rootroot00000000000000github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.22.0 h1:PqEhf+ezz5F5owoDeOUKFzW+W3ZJDShNCaHg4sZuItI= github.com/alecthomas/chroma/v2 v2.22.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/charlievieth/strcase v0.0.5 h1:gV4iXVyD6eI5KdfOV+/vIVCKXZwtCWOmDMcu7Uy00Rs= github.com/charlievieth/strcase v0.0.5/go.mod h1:FIOYY1aDBMSIOFqmVomHBpoK+bteGlESRsgsdWjrhx8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/sirupsen/logrus v1.8.3 h1:DBBfY8eMYazKEJHb3JKpSPfpgd2mBCoNFlQx6C5fftU= github.com/sirupsen/logrus v1.8.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= moor-2.10.3/install.sh000077500000000000000000000002711513574474500145750ustar00rootroot00000000000000#!/bin/bash set -e -o pipefail ./test.sh echo echo 'Installing into /usr/local/bin...' cp moor /usr/local/bin/moor echo echo 'Installed, try "moor moor.go" to see moor in action!' moor-2.10.3/internal/000077500000000000000000000000001513574474500144045ustar00rootroot00000000000000moor-2.10.3/internal/detectManPage.go000066400000000000000000000005171513574474500174370ustar00rootroot00000000000000package internal import ( "github.com/walles/moor/v2/internal/linemetadata" ) func (p *Pager) haveLoadedManPage() bool { reader := p.Reader() if reader == nil { return false } for _, line := range reader.GetLines(linemetadata.Index{}, 10).Lines { if line.Line.HasManPageFormatting() { return true } } return false } moor-2.10.3/internal/editor.go000066400000000000000000000125501513574474500162240ustar00rootroot00000000000000package internal import ( "fmt" "math" "os" "os/exec" "runtime" "strings" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/reader" ) // Dump the reader lines into a read-only temp file and return the absolute file // name. func dumpToTempFile(reader *reader.ReaderImpl) (string, error) { tempFile, err := os.CreateTemp("", "moor-contents-") if err != nil { return "", err } defer func() { err = tempFile.Close() if err != nil { log.Warn("Failed to close temp file: ", err) } }() log.Debug("Dumping contents into: ", tempFile.Name()) lines := reader.GetLines(linemetadata.Index{}, math.MaxInt) for _, line := range lines.Lines { toWrite := line.Plain() _, err := tempFile.WriteString(toWrite + "\n") if err != nil { return "", err } } // Ref: https://pkg.go.dev/os#Chmod err = os.Chmod(tempFile.Name(), 0400) if err != nil { // Doesn't matter that much, but if it fails we should at least log it log.Debug("Failed to make temp file ", tempFile.Name(), " read-only: ", err) } return tempFile.Name(), nil } // Check that the editor is executable func errUnlessExecutable(file string) error { stat, err := os.Stat(file) if err != nil { return fmt.Errorf("Failed to stat %s: %w", file, err) } if runtime.GOOS == "windows" && strings.HasSuffix(strings.ToLower(file), ".exe") { log.Debug(".exe file on Windows, assuming executable: ", file) return nil } if stat.Mode()&0111 != 0 { // Note that this check isn't perfect, it could still be executable but // not by us. Corner case, let's just fail later in that case. return nil } return fmt.Errorf("Not executable: %s", file) } func pickAnEditor() (string, string, error) { // Get an editor setting from either VISUAL or EDITOR editorEnv := "VISUAL" editor := strings.TrimSpace(os.Getenv(editorEnv)) if editor == "" { editorEnv := "EDITOR" editor = strings.TrimSpace(os.Getenv(editorEnv)) } if editor != "" { return editor, editorEnv, nil } candidates := []string{ "vim", // This is a sucky default, but let's have it for compatibility with less "nano", "vi", } for _, candidate := range candidates { fullPath, err := exec.LookPath(candidate) log.Trace("Problem finding ", candidate, ": ", err) if err != nil { continue } err = errUnlessExecutable(fullPath) log.Trace("Problem with executability of ", fullPath, ": ", err) if err != nil { continue } return candidate, "fallback list", nil } return "", "", fmt.Errorf("No editor found, tried: $VISUAL, $EDITOR, %s", strings.Join(candidates, ", ")) } func handleEditingRequest(p *Pager) { if os.Getenv("LESSSECURE") == "1" { p.mode = &PagerModeInfo{ Pager: p, Text: "Not launching editor since LESSSECURE=1 is set in the environment", } return } editor, editorEnv, err := pickAnEditor() if err != nil { log.Warn("Failed to find an editor: ", err) return } // Tyre kicking check that we can find the editor either in the PATH or as // an absolute path firstWord := strings.Fields(editor)[0] editorPath, err := exec.LookPath(firstWord) if err != nil { // FIXME: Show a message in the status bar instead? Nothing wrong with // moor here. log.Warn("Failed to find editor "+firstWord+" from $"+editorEnv+": ", err) return } // Check that the editor is executable err = errUnlessExecutable(editorPath) if err != nil { // FIXME: Show a message in the status bar instead? Nothing wrong with // moor here. log.Warn("Editor from {} not executable: {}", editorEnv, err) return } canOpenFile := p.readers[p.currentReader].FileName != nil if p.readers[p.currentReader].FileName != nil { // Verify that the file exists and is readable err = reader.TryOpen(*p.readers[p.currentReader].FileName) if err != nil { canOpenFile = false log.Info("File to edit is not readable: ", err) } } var fileToEdit string if canOpenFile { fileToEdit = *p.readers[p.currentReader].FileName } else { // NOTE: Let's not wait for the stream to finish, just dump whatever we // have and open the editor on that. The user just asked for it, if they // wanted to wait, they should have done that themselves. // Create a temp file based on reader contents fileToEdit, err = dumpToTempFile(p.readers[p.currentReader]) if err != nil { log.Warn("Failed to create temp file to edit: ", err) return } } p.AfterExit = func() error { // NOTE: If you do any changes here, make sure they work with both "nano" // and "code -w" (VSCode). commandWithArgs := strings.Fields(editor) commandWithArgs = append(commandWithArgs, fileToEdit) log.Info("'v' pressed, launching editor: ", commandWithArgs) command := exec.Command(commandWithArgs[0], commandWithArgs[1:]...) if runtime.GOOS == "windows" { // Don't touch command.Stdin on Windows: // https://github.com/walles/moor/issues/281#issuecomment-2953384726 } else { // Since os.Stdin might come from a pipe, we can't trust that. Instead, // we tell the editor to read from os.Stdout, which points to the // terminal as well. // // Tested on macOS and Linux, works like a charm. command.Stdin = os.Stdout // <- YES, WE SHOULD ASSIGN STDOUT TO STDIN } command.Stdout = os.Stdout command.Stderr = os.Stderr err := command.Run() if err == nil { log.Info("Editor exited successfully: ", commandWithArgs) } return err } p.Quit() } moor-2.10.3/internal/file-switching.go000066400000000000000000000016641513574474500176560ustar00rootroot00000000000000package internal import ( log "github.com/sirupsen/logrus" ) func (p *Pager) previousFile() { p.readerLock.Lock() defer p.readerLock.Unlock() newIndex := p.currentReader - 1 if newIndex < 0 { newIndex = 0 } p.currentReader = newIndex log.Tracef("Switched to previous file, index %d", p.currentReader) select { case p.readerSwitched <- struct{}{}: default: } } func (p *Pager) nextFile() { p.readerLock.Lock() defer p.readerLock.Unlock() newIndex := p.currentReader + 1 if newIndex >= len(p.readers) { newIndex = len(p.readers) - 1 } p.currentReader = newIndex log.Tracef("Switched to next file, index %d", p.currentReader) select { case p.readerSwitched <- struct{}{}: default: } } func (p *Pager) firstFile() { p.readerLock.Lock() defer p.readerLock.Unlock() p.currentReader = 0 log.Tracef("Switched to first file, index %d", p.currentReader) select { case p.readerSwitched <- struct{}{}: default: } } moor-2.10.3/internal/filteringReader.go000066400000000000000000000145741513574474500200540ustar00rootroot00000000000000package internal import ( "fmt" "math" "sync" "time" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/internal/search" ) // Filters lines based on the search query from the pager. type FilteringReader struct { BackingReader reader.Reader // This is a reference so that we can track changes to the original pattern, // including if it is set to nil. Filter *search.Search // Protects filteredLinesCache, unfilteredLineCountWhenCaching, and // filterPatternWhenCaching. lock sync.Mutex // nil means no filtering has happened yet filteredLinesCache *[]reader.NumberedLine // This is what the reader's line count was when we filtered. If the // reader's current line count doesn't match, then our cache needs to be // rebuilt. unfilteredLineCountWhenCaching int // This is the pattern that was used when we cached the lines. If it // doesn't match the current pattern, then our cache needs to be rebuilt. filterWhenCaching search.Search } // Please hold the lock when calling this method. func (f *FilteringReader) rebuildCache() { t0 := time.Now() cache := make([]reader.NumberedLine, 0) filter := *f.Filter // Mark cache base conditions f.unfilteredLineCountWhenCaching = f.BackingReader.GetLineCount() f.filterWhenCaching = filter // Repopulate the cache allBaseLines := f.BackingReader.GetLines(linemetadata.Index{}, math.MaxInt) resultIndex := 0 for _, line := range allBaseLines.Lines { if filter.Active() && !filter.Matches(line.Line.Plain(line.Index)) { // We have a pattern but it doesn't match continue } cache = append(cache, reader.NumberedLine{ Line: line.Line, Index: linemetadata.IndexFromZeroBased(resultIndex), Number: line.Number, }) resultIndex++ } f.filteredLinesCache = &cache log.Debugf("Filtered out %d/%d lines in %s", len(allBaseLines.Lines)-len(cache), len(allBaseLines.Lines), time.Since(t0)) } func (f *FilteringReader) getAllLines() []reader.NumberedLine { f.lock.Lock() defer f.lock.Unlock() if f.filteredLinesCache == nil { f.rebuildCache() return *f.filteredLinesCache } if f.unfilteredLineCountWhenCaching != f.BackingReader.GetLineCount() { f.rebuildCache() return *f.filteredLinesCache } var currentFilterPattern search.Search if (*f).Filter.Active() { currentFilterPattern = *f.Filter } var cacheFilterPattern search.Search if f.filterWhenCaching.Active() { cacheFilterPattern = f.filterWhenCaching } if !currentFilterPattern.Equals(cacheFilterPattern) { f.rebuildCache() return *f.filteredLinesCache } return *f.filteredLinesCache } func (f *FilteringReader) shouldPassThrough() bool { f.lock.Lock() defer f.lock.Unlock() if f.Filter == nil || f.Filter.Inactive() { // Cache is not needed f.filteredLinesCache = nil // No filtering, so pass through all return true } return false } func (f *FilteringReader) GetLineCount() int { if f.shouldPassThrough() { return f.BackingReader.GetLineCount() } return len(f.getAllLines()) } func (f *FilteringReader) ShouldShowLineCount() bool { panic("Unexpected call to FilteringReader.ShouldShowLineCount()") } func (f *FilteringReader) GetLine(index linemetadata.Index) *reader.NumberedLine { if f.shouldPassThrough() { return f.BackingReader.GetLine(index) } allLines := f.getAllLines() if index.Index() < 0 || index.Index() >= len(allLines) { return nil } return &allLines[index.Index()] } func (f *FilteringReader) GetLines(firstLine linemetadata.Index, wantedLineCount int) reader.InputLines { if f.shouldPassThrough() { return f.BackingReader.GetLines(firstLine, wantedLineCount) } acceptedLines := f.getAllLines() if len(acceptedLines) == 0 || wantedLineCount == 0 { return reader.InputLines{ StatusText: f.createStatus(nil), } } lastLine := firstLine.NonWrappingAdd(wantedLineCount - 1) // Prevent reading past the end of the available lines maxLineIndex := *linemetadata.IndexFromLength(len(acceptedLines)) if lastLine.IsAfter(maxLineIndex) { lastLine = maxLineIndex // If one line was requested, then first and last should be exactly the // same, and we would get there by adding zero. firstLine = lastLine.NonWrappingAdd(1 - wantedLineCount) return f.GetLines(firstLine, firstLine.CountLinesTo(lastLine)) } return reader.InputLines{ Lines: acceptedLines[firstLine.Index() : firstLine.Index()+wantedLineCount], StatusText: f.createStatus(&lastLine), } } func (f *FilteringReader) GetLinesPreallocated(firstLine linemetadata.Index, resultLines *[]reader.NumberedLine) (string, string) { if f.shouldPassThrough() { return f.BackingReader.GetLinesPreallocated(firstLine, resultLines) } lines := f.GetLines(firstLine, cap(*resultLines)) *resultLines = lines.Lines return lines.FilenameText, lines.StatusText } // In the general case, this will return a text like this: // "Filtered: 1234/5678 lines 22%" func (f *FilteringReader) createStatus(lastLine *linemetadata.Index) string { baseCount := f.BackingReader.GetLineCount() if baseCount == 0 { return "Filtered: No input lines" } baseCountString := "/" + linemetadata.IndexFromLength(baseCount).Format() if !f.BackingReader.ShouldShowLineCount() { baseCountString = "" } if lastLine == nil { // 100% because we're showing all 0 lines return "Filtered: 0" + baseCountString + " lines 100%" } acceptedCount := f.GetLineCount() acceptedCountString := linemetadata.IndexFromLength(acceptedCount).Format() percent := int(math.Floor(100 * float64(lastLine.Index()+1) / float64(acceptedCount))) lineString := "line" if (len(baseCountString) > 0 && baseCount != 1) || (len(baseCountString) == 0 && acceptedCount != 1) { lineString += "s" } return fmt.Sprintf("Filtered: %s%s %s %d%%", acceptedCountString, baseCountString, lineString, percent) } // SetBackingReader switches the underlying reader while holding the lock and // clears all cached state so that subsequent calls will rebuild using the new // reader. // // This avoids data races that would occur if the entire FilteringReader struct // (and its mutex) were reassigned concurrently. func (f *FilteringReader) SetBackingReader(r reader.Reader) { f.lock.Lock() defer f.lock.Unlock() f.BackingReader = r // Invalidate caches so they will be rebuilt lazily on next access. f.filteredLinesCache = nil f.unfilteredLineCountWhenCaching = -1 f.filterWhenCaching = search.Search{} } moor-2.10.3/internal/inputbox.go000066400000000000000000000144071513574474500166110ustar00rootroot00000000000000package internal import ( "unicode" "github.com/walles/moor/v2/twin" ) type InputBoxOnTextChanged func(text string) type AcceptMode int const ( INPUTBOX_ACCEPT_ALL AcceptMode = iota INPUTBOX_ACCEPT_POSITIVE_NUMBERS ) type InputBox struct { text string // accept controls what input is accepted. Use the INPUTBOX_ACCEPT_* // constants defined above. accept AcceptMode // cursorPos is the insertion point in runes (0 == before first rune, // len(runes) == after last rune). cursorPos int // onTextChanged is an optional callback which is triggered when the text // of the InputBox changes. onTextChanged InputBoxOnTextChanged } // draw renders the input box at the bottom line of the screen, showing a // simple prompt and the current text with a reverse attribute cursor. func (b *InputBox) draw(screen twin.Screen, keys_help string, prompt string) { width, height := screen.Size() pos := 0 // Draw the prompt first for _, ch := range prompt { pos += screen.SetCell(pos, height-1, twin.NewStyledRune(ch, twin.StyleDefault)) } // Work with runes for cursor correctness textRunes := []rune(b.text) if b.cursorPos < 0 { b.cursorPos = 0 } if b.cursorPos > len(textRunes) { b.cursorPos = len(textRunes) } // Draw left side (before cursor) for i, ch := range textRunes { if i == b.cursorPos { break } pos += screen.SetCell(pos, height-1, twin.NewStyledRune(ch, twin.StyleDefault)) } // If cursor is on a rune, invert that rune. If cursor is at the end, // show an inverted blank cell. if b.cursorPos < len(textRunes) { pos += screen.SetCell(pos, height-1, twin.NewStyledRune(textRunes[b.cursorPos], twin.StyleDefault.WithAttr(twin.AttrReverse))) // Draw right side after the cursor rune for i := b.cursorPos + 1; i < len(textRunes); i++ { pos += screen.SetCell(pos, height-1, twin.NewStyledRune(textRunes[i], twin.StyleDefault)) } } else { // Cursor at end -> reverse blank pos += screen.SetCell(pos, height-1, twin.NewStyledRune(' ', twin.StyleDefault.WithAttr(twin.AttrReverse))) } afterTextPos := pos // Clear the rest of the line for pos < width { pos += screen.SetCell(pos, height-1, twin.NewStyledRune(' ', twin.StyleDefault)) } // Draw help on the right if len(keys_help) > 0 { renderedHelp := renderHelpText(keys_help) helpStart := width - len(renderedHelp) if helpStart > afterTextPos { // Draw the help text pos = width - len(renderedHelp) for _, cell := range renderedHelp { pos += screen.SetCell(pos, height-1, cell) } screen.SetCell(pos, height-1, twin.NewStyledRune(' ', statusbarStyle)) } } } func (b *InputBox) setText(text string) { b.text = text b.moveCursorEnd() if b.onTextChanged != nil { b.onTextChanged(b.text) } } // handleRune appends runes to the text of the InputBox and returns if those have been processed. // (Some keyboards send 0x08 instead of backspace, so we support it here too). func (b *InputBox) handleRune(char rune) bool { if char == '\x08' { b.backspace() return true } if char == '\x01' { // Ctrl-A, move cursor to start b.moveCursorHome() return true } if char == '\x05' { // Ctrl-E, move cursor to end b.moveCursorEnd() return true } if char == '\x02' { // Ctrl-B, move cursor left b.moveCursorLeft() return true } if char == '\x06' { // Ctrl-F, move cursor right b.moveCursorRight() return true } if char == '\x0b' { // Ctrl-K, delete to end of line b.deleteToEnd() return true } if char == '\x15' { // Ctrl-U, delete to start of line b.deleteToStart() return true } // If configured to accept numbers only, drop any non-digit rune. if b.accept == INPUTBOX_ACCEPT_POSITIVE_NUMBERS { if !unicode.IsDigit(char) { return false } } // Insert at cursor position runes := []rune(b.text) if b.cursorPos < 0 { b.cursorPos = 0 } if b.cursorPos > len(runes) { b.cursorPos = len(runes) } // Build a new rune slice with the inserted rune newRunes := make([]rune, 0, len(runes)+1) newRunes = append(newRunes, runes[:b.cursorPos]...) newRunes = append(newRunes, char) if b.cursorPos < len(runes) { newRunes = append(newRunes, runes[b.cursorPos:]...) } b.text = string(newRunes) b.cursorPos++ // finally let's tell someone that the text has changed if b.onTextChanged != nil { b.onTextChanged(b.text) } return true } // handleKey processes special keys like backspace, delete, arrow keys, home and end. // Returns true if the key was processed, false otherwise. func (b *InputBox) handleKey(key twin.KeyCode) bool { switch key { case twin.KeyLeft: b.moveCursorLeft() return true case twin.KeyRight: b.moveCursorRight() return true case twin.KeyHome: b.moveCursorHome() return true case twin.KeyEnd: b.moveCursorEnd() return true case twin.KeyBackspace: b.backspace() return true case twin.KeyDelete: b.delete() return true } return false } // moveCursorLeft moves the cursor one rune to the left. func (b *InputBox) moveCursorLeft() { if b.cursorPos > 0 { b.cursorPos-- } } // moveCursorRight moves the cursor one rune to the right. func (b *InputBox) moveCursorRight() { if b.cursorPos < len([]rune(b.text)) { b.cursorPos++ } } // moveCursorHome moves the cursor to the start of the text. func (b *InputBox) moveCursorHome() { b.cursorPos = 0 } // moveCursorEnd moves the cursor to the end of the text. func (b *InputBox) moveCursorEnd() { b.cursorPos = len([]rune(b.text)) } func (b *InputBox) deleteToEnd() { b.text = string([]rune(b.text)[:b.cursorPos]) if b.onTextChanged != nil { b.onTextChanged(b.text) } } func (b *InputBox) deleteToStart() { b.text = string([]rune(b.text)[b.cursorPos:]) b.cursorPos = 0 if b.onTextChanged != nil { b.onTextChanged(b.text) } } // backspace removes the rune before the cursor and moves the cursor left. func (b *InputBox) backspace() { runes := []rune(b.text) if b.cursorPos > 0 && len(runes) > 0 { runes = append(runes[:b.cursorPos-1], runes[b.cursorPos:]...) b.cursorPos-- b.text = string(runes) if b.onTextChanged != nil { b.onTextChanged(b.text) } } } // delete removes the rune at the cursor. func (b *InputBox) delete() { runes := []rune(b.text) if b.cursorPos < len(runes) { runes = append(runes[:b.cursorPos], runes[b.cursorPos+1:]...) b.text = string(runes) if b.onTextChanged != nil { b.onTextChanged(b.text) } } } moor-2.10.3/internal/inputbox_test.go000066400000000000000000000043071513574474500176460ustar00rootroot00000000000000package internal import ( "testing" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) func TestInsertAndBackspace(t *testing.T) { screen := twin.NewFakeScreen(40, 2) b := &InputBox{accept: INPUTBOX_ACCEPT_ALL} assert.Assert(t, b.handleRune('a')) assert.Assert(t, b.handleRune('b')) assert.Assert(t, b.handleRune('c')) assert.Equal(t, "abc", b.text) // Backspace b.backspace() assert.Equal(t, "ab", b.text) // Draw and inspect status line b.draw(screen, "", "P: ") row := rowToString(screen.GetRow(1)) assert.Equal(t, "P: ab", row) } func TestCursorMovementAndInsertDelete(t *testing.T) { screen := twin.NewFakeScreen(80, 2) b := &InputBox{accept: INPUTBOX_ACCEPT_ALL} b.handleRune('a') b.handleRune('b') b.handleRune('c') assert.Equal(t, "abc", b.text) // Move left twice, insert 'X' between a and b b.moveCursorLeft() b.moveCursorLeft() assert.Assert(t, b.handleRune('X')) assert.Equal(t, "aXbc", b.text) // Delete at cursor (cursor is after X) b.delete() assert.Equal(t, "aXc", b.text) // Move home and insert b.moveCursorHome() assert.Assert(t, b.handleRune('S')) assert.Equal(t, "SaXc", b.text) // Move end and append b.moveCursorEnd() assert.Assert(t, b.handleRune('E')) assert.Equal(t, "SaXcE", b.text) b.draw(screen, "", "G: ") row := rowToString(screen.GetRow(1)) assert.Equal(t, "G: SaXcE", row) } func TestAcceptPositiveNumbers(t *testing.T) { b := &InputBox{accept: INPUTBOX_ACCEPT_POSITIVE_NUMBERS} assert.Assert(t, b.handleRune('1')) assert.Assert(t, !b.handleRune('a')) assert.Assert(t, b.handleRune('2')) assert.Equal(t, "12", b.text) } func TestUnicodeRunes(t *testing.T) { screen := twin.NewFakeScreen(80, 2) b := &InputBox{accept: INPUTBOX_ACCEPT_ALL} // Insert a CJK char and an emoji assert.Assert(t, b.handleRune('åˆ')) assert.Assert(t, b.handleRune('ðŸ§')) assert.Equal(t, "åˆðŸ§", b.text) // Backspace should remove the emoji b.backspace() assert.Equal(t, "åˆ", b.text) // Insert another wide char at start b.moveCursorHome() assert.Assert(t, b.handleRune('ä½ ')) assert.Equal(t, "ä½ åˆ", b.text) b.draw(screen, "", "U: ") row := rowToString(screen.GetRow(1)) // We expect prompt + two runes assert.Equal(t, "U: ä½ åˆ", row) } moor-2.10.3/internal/linemetadata/000077500000000000000000000000001513574474500170345ustar00rootroot00000000000000moor-2.10.3/internal/linemetadata/index.go000066400000000000000000000040651513574474500204770ustar00rootroot00000000000000package linemetadata import ( "fmt" "math" "github.com/walles/moor/v2/internal/util" ) // This represents a (zero based) index into an array of lines type Index struct { index int } func (i Index) Index() int { return i.index } func IndexFromZeroBased(zeroBased int) Index { if zeroBased < 0 { panic(fmt.Errorf("zero-based line indices must be at least 0, got %d", zeroBased)) } return Index{index: zeroBased} } func IndexFromOneBased(oneBased int) Index { if oneBased < 1 { panic(fmt.Errorf("one-based line indices must be at least 1, got %d", oneBased)) } return Index{index: oneBased - 1} } // The highest possible line index func IndexMax() Index { return Index{index: math.MaxInt} } // Set the line index to the last line of a file with the given number of lines // in it. Or nil if the line count is 0. func IndexFromLength(length int) *Index { if length == 0 { return nil } if length < 0 { panic(fmt.Errorf("line count must be at least 0, got %d", length)) } return &Index{index: length - 1} } // Returns a new index between 0 and math.MaxInt func (i Index) NonWrappingAdd(offset int) Index { if offset > 0 { if i.index > math.MaxInt-offset { return Index{index: math.MaxInt} } } else { if i.index < -offset { return Index{index: 0} } } return Index{index: i.index + offset} } // First line will be formatted as "1". func (i Index) Format() string { return util.FormatInt(i.index + 1) } func (i Index) IsBefore(other Index) bool { return i.index < other.index } func (i Index) IsAfter(other Index) bool { return i.index > other.index } // If both indices are the same this method will return 1. func (i Index) CountLinesTo(next Index) int { if i.index > next.index { panic(fmt.Errorf("line indices must be ordered, got %s-%s", i.Format(), next.Format())) } return 1 + next.index - i.index } func (i Index) IsZero() bool { return i.index == 0 } func (i Index) IsWithinLength(length int) bool { if length < 0 { panic(fmt.Errorf("line count must be at least 0, got %d", length)) } return i.index >= 0 && i.index < length } moor-2.10.3/internal/linemetadata/number.go000066400000000000000000000042031513574474500206520ustar00rootroot00000000000000package linemetadata import ( "fmt" "math" "github.com/walles/moor/v2/internal/util" ) // This represents a line number in an input stream type Number struct { number int } func (l Number) AsOneBased() int { if l.number == math.MaxInt { return math.MaxInt } return l.number + 1 } // FIXME: Maybe drop this in favor of some array access method(s)? func (l Number) AsZeroBased() int { return l.number } func NumberFromOneBased(oneBased int) Number { if oneBased < 1 { panic(fmt.Errorf("one-based line numbers must be at least 1, got %d", oneBased)) } return Number{number: oneBased - 1} } func NumberFromZeroBased(zeroBased int) Number { if zeroBased < 0 { panic(fmt.Errorf("zero-based line numbers must be at least 0, got %d", zeroBased)) } return Number{number: zeroBased} } // The highest possible line number func NumberMax() Number { return Number{number: math.MaxInt} } // Set the line number to the last line of a file with the given number of lines // in it. Or nil if the line count is 0. func NumberFromLength(length int) *Number { if length == 0 { return nil } if length < 0 { panic(fmt.Errorf("line count must be at least 0, got %d", length)) } return &Number{number: length - 1} } func (l Number) NonWrappingAdd(offset int) Number { if offset > 0 { if l.AsZeroBased() > math.MaxInt-offset { return NumberFromZeroBased(math.MaxInt) } } else { if l.AsZeroBased() < -offset { return NumberFromZeroBased(0) } } return NumberFromZeroBased(l.number + offset) } func (l Number) Format() string { return util.FormatInt(l.AsOneBased()) } // If both lines are the same this method will return 1. func (l Number) CountLinesTo(next Number) int { if l.number > next.number { panic(fmt.Errorf("line numbers must be ordered, got %s-%s", l.Format(), next.Format())) } return 1 + next.AsZeroBased() - l.AsZeroBased() } // Is this the lowest possible line number? func (l Number) IsZero() bool { return l.AsZeroBased() == 0 } func (l Number) IsBefore(other Number) bool { return l.AsZeroBased() < other.AsZeroBased() } func (l Number) IsAfter(other Number) bool { return l.AsZeroBased() > other.AsZeroBased() } moor-2.10.3/internal/linemetadata/number_test.go000066400000000000000000000037741513574474500217250ustar00rootroot00000000000000package linemetadata import ( "math" "testing" "gotest.tools/v3/assert" ) func TestNonWrappingAddPositive(t *testing.T) { base := NumberFromZeroBased(math.MaxInt - 2) assert.Equal(t, base.NonWrappingAdd(1), NumberFromZeroBased(math.MaxInt-1)) assert.Equal(t, base.NonWrappingAdd(2), NumberFromZeroBased(math.MaxInt)) assert.Equal(t, base.NonWrappingAdd(3), NumberFromZeroBased(math.MaxInt)) } func TestNonWrappingAddNegative(t *testing.T) { base := NumberFromZeroBased(2) assert.Equal(t, base.NonWrappingAdd(-1), NumberFromZeroBased(1)) assert.Equal(t, base.NonWrappingAdd(-2), NumberFromZeroBased(0)) assert.Equal(t, base.NonWrappingAdd(-3), NumberFromZeroBased(0)) } func TestLineNumberFormatting(t *testing.T) { assert.Equal(t, "1", NumberFromOneBased(1).Format()) assert.Equal(t, "10", NumberFromOneBased(10).Format()) assert.Equal(t, "100", NumberFromOneBased(100).Format()) // Ref: // https://en.wikipedia.org/wiki/Decimal_separator#Exceptions_to_digit_grouping assert.Equal(t, "1000", NumberFromOneBased(1000).Format()) assert.Equal(t, "10_000", NumberFromOneBased(10000).Format()) assert.Equal(t, "100_000", NumberFromOneBased(100000).Format()) assert.Equal(t, "1_000_000", NumberFromOneBased(1000000).Format()) assert.Equal(t, "10_000_000", NumberFromOneBased(10000000).Format()) } func TestNumberFromLength(t *testing.T) { // If the file has one line then the last zero based line number is 0. fromLength := NumberFromLength(1) assert.Equal(t, *fromLength, NumberFromZeroBased(0)) } func TestLineNumberEquality(t *testing.T) { assert.Equal(t, NumberFromZeroBased(1), NumberFromOneBased(2), "Two different ways of representing the same line number should be equal") } func TestLineNumberCountLinesTo(t *testing.T) { assert.Equal(t, NumberFromZeroBased(0).CountLinesTo(NumberFromZeroBased(0)), 1, // Count is inclusive, so countint from 0 to 0 is 1 ) assert.Equal(t, NumberFromZeroBased(0).CountLinesTo(NumberFromZeroBased(1)), 2, // Count is inclusive, so countint from 0 to 1 is 2 ) } moor-2.10.3/internal/linewrapper.go000066400000000000000000000100621513574474500172620ustar00rootroot00000000000000package internal import ( "unicode" "github.com/walles/moor/v2/internal/textstyles" ) // From: https://www.compart.com/en/unicode/U+00A0 // //revive:disable-next-line:var-naming const NO_BREAK_SPACE = '\xa0' // Given some text and a maximum width in screen cells, find the best point at // which to wrap the text. Return value is in number of runes. func getWrapCount(line []textstyles.CellWithMetadata, maxScreenCellsCount int) int { screenCells := 0 bestCutPoint := maxScreenCellsCount inLeadingWhitespace := true for cutBeforeThisIndex := 0; cutBeforeThisIndex <= maxScreenCellsCount; cutBeforeThisIndex++ { canBreakHere := false char := line[cutBeforeThisIndex].Rune onBreakableSpace := unicode.IsSpace(char) && char != NO_BREAK_SPACE if onBreakableSpace && !inLeadingWhitespace { // Break-OK whitespace, cut before this one! canBreakHere = true } if !onBreakableSpace { inLeadingWhitespace = false } // Accept cutting inside "]("" in Markdown links: [home](http://127.0.0.1) if cutBeforeThisIndex > 0 { previousChar := line[cutBeforeThisIndex-1].Rune if previousChar == ']' && char == '(' { canBreakHere = true } } // Break after single slashes, this is to enable breaking inside URLs / paths if cutBeforeThisIndex > 1 { beforeSlash := line[cutBeforeThisIndex-2].Rune slash := line[cutBeforeThisIndex-1].Rune afterSlash := char if beforeSlash != '/' && slash == '/' && afterSlash != '/' { canBreakHere = true } } if cutBeforeThisIndex > 0 { // Break after a hyphen / dash. That's something people do. previousChar := line[cutBeforeThisIndex-1].Rune if previousChar == '-' && char != '-' { canBreakHere = true } } if canBreakHere { bestCutPoint = cutBeforeThisIndex } screenCells += line[cutBeforeThisIndex].Width() if screenCells > maxScreenCellsCount { // We went too far if bestCutPoint > cutBeforeThisIndex { // We have to cut here bestCutPoint = cutBeforeThisIndex } break } } return bestCutPoint } // How many screen cells wide will this line be? func getScreenCellCount(runes []textstyles.CellWithMetadata) int { cellCount := 0 for _, rune := range runes { cellCount += rune.Width() } return cellCount } // Wrap one line of text to a maximum width. // // The return value will not contain any trailers, but the ContainsSearchHit // field will be correctly set for sub-lines with search hits. func wrapLine(width int, line textstyles.CellWithMetadataSlice) []textstyles.StyledRunesWithTrailer { // Trailing space risks showing up by itself on a line, which would just // look weird. line = line.WithoutSpaceRight() screenCellCount := getScreenCellCount(line) if screenCellCount == 0 { return []textstyles.StyledRunesWithTrailer{{}} } wrapped := make([]textstyles.StyledRunesWithTrailer, 0, len(line)/width) for screenCellCount > width { wrapWidth := getWrapCount(line, width) firstPart := line[:wrapWidth] isOnFirstLine := len(wrapped) == 0 if !isOnFirstLine { // Leading whitespace on wrapped lines would just look like // indentation, which would be weird for wrapped text. firstPart = firstPart.WithoutSpaceLeft() } wrapped = append(wrapped, textstyles.StyledRunesWithTrailer{ StyledRunes: firstPart.WithoutSpaceRight(), ContainsSearchHit: firstPart.ContainsSearchHit(), }, ) // These runes still need processing remaining := line[wrapWidth:].WithoutSpaceLeft() // Track how many screen cells are left to handle handledCount := len(line) - len(remaining) screenCellCount -= getScreenCellCount(line[:handledCount]) // Prepare for the next iteration line = remaining } isOnFirstLine := len(wrapped) == 0 if !isOnFirstLine { // Leading whitespace on wrapped lines would just look like // indentation, which would be weird for wrapped text. line = line.WithoutSpaceLeft() } line = line.WithoutSpaceRight() if len(line) > 0 { wrapped = append(wrapped, textstyles.StyledRunesWithTrailer{ StyledRunes: line, ContainsSearchHit: line.ContainsSearchHit(), }, ) } return wrapped } moor-2.10.3/internal/linewrapper_test.go000066400000000000000000000117271513574474500203320ustar00rootroot00000000000000package internal import ( "testing" "gotest.tools/v3/assert" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/twin" log "github.com/sirupsen/logrus" ) func tokenize(input string) []textstyles.CellWithMetadata { return textstyles.StyledRunesFromString(twin.StyleDefault, input, nil, 0).StyledRunes } func rowsToString(cellLines []textstyles.CellWithMetadataSlice) string { returnMe := "" for _, cellLine := range cellLines { lineString := "" for _, cell := range cellLine { lineString += string(cell.Rune) } if len(returnMe) > 0 { returnMe += "\n" } returnMe += "<" + lineString + ">" } return returnMe } func assertWrap(t *testing.T, input string, widthInScreenCells int, wrappedLines ...string) { toWrap := tokenize(input) wrapped := wrapLine(widthInScreenCells, toWrap) actual := make([]textstyles.CellWithMetadataSlice, len(wrapped)) for i, w := range wrapped { actual[i] = w.StyledRunes } expected := []textstyles.CellWithMetadataSlice{} for _, wrappedLine := range wrappedLines { expected = append(expected, tokenize(wrappedLine)) } equal := true if len(actual) != len(expected) { equal = false } else { for i := range actual { if !actual[i].Equal(expected[i]) { equal = false break } } } if equal { return } t.Errorf("When wrapping <%s> at cell count %d:\n--Expected--\n%s\n\n--Actual--\n%s", input, widthInScreenCells, rowsToString(expected), rowsToString(actual)) } func TestEnoughRoomNoWrapping(t *testing.T) { assertWrap(t, "This is a test", 20, "This is a test") } func TestWrapBlank(t *testing.T) { assertWrap(t, " ", 4, "") assertWrap(t, " ", 2, "") assertWrap(t, "", 20, "") } func TestWordLongerThanLine(t *testing.T) { assertWrap(t, "intermediary", 6, "interm", "ediary") } func TestLeadingSpaceNoWrap(t *testing.T) { assertWrap(t, " abc", 20, " abc") } func TestLeadingSpaceWithWrap(t *testing.T) { assertWrap(t, " abc", 2, " a", "bc") } func TestLeadingWrappedSpace(t *testing.T) { assertWrap(t, "ab cd", 2, "ab", "cd") } func TestWordWrap(t *testing.T) { assertWrap(t, "abc 123", 8, "abc 123") assertWrap(t, "abc 123", 7, "abc 123") assertWrap(t, "abc 123", 6, "abc", "123") assertWrap(t, "abc 123", 5, "abc", "123") assertWrap(t, "abc 123", 4, "abc", "123") assertWrap(t, "abc 123", 3, "abc", "123") assertWrap(t, "abc 123", 2, "ab", "c", "12", "3") assertWrap(t, "here's the last line", 10, "here's the", "last line") } func TestWordWrapUrl(t *testing.T) { assertWrap(t, "http://apa/bepa/", 17, "http://apa/bepa/") assertWrap(t, "http://apa/bepa/", 16, "http://apa/bepa/") assertWrap(t, "http://apa/bepa/", 15, "http://apa/", "bepa/") assertWrap(t, "http://apa/bepa/", 14, "http://apa/", "bepa/") assertWrap(t, "http://apa/bepa/", 13, "http://apa/", "bepa/") assertWrap(t, "http://apa/bepa/", 12, "http://apa/", "bepa/") assertWrap(t, "http://apa/bepa/", 11, "http://apa/", "bepa/") assertWrap(t, "http://apa/bepa/", 10, "http://apa", "/bepa/") assertWrap(t, "http://apa/bepa/", 9, "http://ap", "a/bepa/") assertWrap(t, "http://apa/bepa/", 8, "http://a", "pa/bepa/") assertWrap(t, "http://apa/bepa/", 7, "http://", "apa/", "bepa/") assertWrap(t, "http://apa/bepa/", 6, "http:/", "/apa/", "bepa/") assertWrap(t, "http://apa/bepa/", 5, "http:", "//apa", "/bepa", "/") assertWrap(t, "http://apa/bepa/", 4, "http", "://a", "pa/", "bepa", "/") assertWrap(t, "http://apa/bepa/", 3, "htt", "p:/", "/ap", "a/", "bep", "a/") } func TestWordWrapMarkdownLink(t *testing.T) { assertWrap(t, "[something](http://apa/bepa)", 13, "[something]", "(http://apa/", "bepa)") assertWrap(t, "[something](http://apa/bepa)", 12, "[something]", "(http://apa/", "bepa)") assertWrap(t, "[something](http://apa/bepa)", 11, "[something]", "(http://apa", "/bepa)") // This doesn't look great, room for tuning! assertWrap(t, "[something](http://apa/bepa)", 10, "[something", "]", "(http://ap", "a/bepa)") } func TestWordWrapWideChars(t *testing.T) { // The width is in cells, and there are wide chars in here using multiple cells. assertWrap(t, "x上åˆy", 6, "x上åˆy") assertWrap(t, "x上åˆy", 5, "x上åˆ", "y") assertWrap(t, "x上åˆy", 4, "x上", "åˆy") assertWrap(t, "x上åˆy", 3, "x上", "åˆy") assertWrap(t, "x上åˆy", 2, "x", "上", "åˆ", "y") } func TestGetWrapCountWideChars(t *testing.T) { line := tokenize("x上åˆy") assert.Equal(t, getWrapCount(line, 5), 3) assert.Equal(t, getWrapCount(line, 4), 2) assert.Equal(t, getWrapCount(line, 3), 2) assert.Equal(t, getWrapCount(line, 2), 1) assert.Equal(t, getWrapCount(line, 1), 1) } func BenchmarkWrapLine(b *testing.B) { log.SetLevel(log.WarnLevel) // Stop info logs from polluting benchmark output words := "Here are some words of different lengths, some of which are very long, and some of which are short. " lineLen := 60_000 line := "" for len(line) < lineLen { line += words } line = line[:lineLen] styledRunes := tokenize(line) b.ResetTimer() for i := 0; i < b.N; i++ { _ = wrapLine(73, styledRunes) } b.StopTimer() } moor-2.10.3/internal/logwriter.go000066400000000000000000000006571513574474500167610ustar00rootroot00000000000000package internal import ( "strings" "sync" ) // Capture log lines and support returning all logged lines as one string type LogWriter struct { lock sync.Mutex buffer strings.Builder } func (lw *LogWriter) Write(p []byte) (n int, err error) { lw.lock.Lock() defer lw.lock.Unlock() return lw.buffer.Write(p) } func (lw *LogWriter) String() string { lw.lock.Lock() defer lw.lock.Unlock() return lw.buffer.String() } moor-2.10.3/internal/pager-search.go000066400000000000000000000355611513574474500173060ustar00rootroot00000000000000package internal import ( "fmt" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/linemetadata" ) // Scroll to the next search hit, while the user is typing the search string. func (p *Pager) scrollToSearchHits() { if p.search.Inactive() { // This is not a search return } if p.searchHitIsVisible() { // Already on-screen return } if p.scrollRightToSearchHits() { // Found it to the right, done! return } lineIndex := p.scrollPosition.lineIndex(p) if lineIndex == nil { // No lines to search return } firstHitIndex := FindFirstHit(p.Reader(), p.search, *lineIndex, nil, SearchDirectionForward) if firstHitIndex == nil { alreadyAtTheTop := (*lineIndex == linemetadata.Index{}) if alreadyAtTheTop { // No match, can't wrap, give up return } // Try again from the top firstHitIndex = FindFirstHit(p.Reader(), p.search, linemetadata.Index{}, lineIndex, SearchDirectionForward) } if firstHitIndex == nil { // No match, give up return } // Found a match on some line p.scrollPosition = NewScrollPositionFromIndex(*firstHitIndex, "scrollToSearchHits") p.leftColumnZeroBased = 0 p.showLineNumbers = p.ShowLineNumbers if !p.searchHitIsVisible() { p.scrollRightToSearchHits() } p.centerSearchHitsVertically() } // Scroll to the next search hit, when the user presses 'n'. func (p *Pager) scrollToNextSearchHit() { if p.search.Inactive() { // Nothing to search for, never mind return } if p.Reader().GetLineCount() == 0 { // Nothing to search in, never mind return } if p.scrollRightToSearchHits() { // Found it to the right, done! return } if p.isViewing() && p.isScrolledToEnd() { p.mode = PagerModeNotFound{pager: p} return } var firstSearchIndex linemetadata.Index switch { case p.isViewing(): // Start searching on the first line below the bottom of the screen position := p.getLastVisiblePosition().NextLine(1) firstSearchIndex = *position.lineIndex(p) case p.isNotFound(): // Restart searching from the top p.mode = PagerModeViewing{pager: p} firstSearchIndex = linemetadata.Index{} default: panic(fmt.Sprint("Unknown search mode when finding next: ", p.mode)) } firstHitIndex := FindFirstHit(p.Reader(), p.search, firstSearchIndex, nil, SearchDirectionForward) if firstHitIndex == nil { p.mode = PagerModeNotFound{pager: p} return } p.scrollPosition = NewScrollPositionFromIndex(*firstHitIndex, "scrollToNextSearchHit") // Don't let any search hit scroll out of sight p.setTargetLine(nil) p.leftColumnZeroBased = 0 p.showLineNumbers = p.ShowLineNumbers if !p.searchHitIsVisible() { p.scrollRightToSearchHits() } p.centerSearchHitsVertically() } // Scroll backwards to the previous search hit, while the user is typing the // search string. func (p *Pager) scrollToSearchHitsBackwards() { if p.search.Inactive() { // This is not a search return } if p.searchHitIsVisible() { // Already on-screen return } if p.scrollLeftToSearchHits() { // Found it to the left, done! return } // Start at the top visible line lineIndex := p.scrollPosition.lineIndex(p) firstHitIndex := FindFirstHit(p.Reader(), p.search, *lineIndex, nil, SearchDirectionBackward) if firstHitIndex == nil { lastReaderLineIndex := linemetadata.IndexFromLength(p.Reader().GetLineCount()) if lastReaderLineIndex == nil { // In the first part of the search we had some lines to search. // Lines should never go away, so this should never happen. log.Error("Wrapped backwards search had no lines to search") return } lastVisibleLineIndex := p.getLastVisiblePosition().lineIndex(p) canWrap := (*lineIndex != *lastVisibleLineIndex) if !canWrap { // No match, can't wrap, give up return } // Try again from the bottom firstHitIndex = FindFirstHit(p.Reader(), p.search, *lastReaderLineIndex, lineIndex, SearchDirectionBackward) } if firstHitIndex == nil { // No match, give up return } hitPosition := NewScrollPositionFromIndex(*firstHitIndex, "scrollToSearchHitsBackwards") // Scroll so that the first hit is at the bottom of the screen. If the // visible height is 1, we should scroll 0 steps. p.scrollPosition = hitPosition.PreviousLine(p.visibleHeight() - 1) p.scrollMaxRight() if !p.searchHitIsVisible() { p.scrollLeftToSearchHits() } p.centerSearchHitsVertically() } // Scroll backwards to the previous search hit, when the user presses 'N'. func (p *Pager) scrollToPreviousSearchHit() { if p.search.Inactive() { // Nothing to search for, never mind return } if p.Reader().GetLineCount() == 0 { // Nothing to search in, never mind return } if p.scrollLeftToSearchHits() { // Found it to the left, done! return } var firstSearchIndex linemetadata.Index switch { case p.isViewing(): if p.scrollPosition.lineIndex(p).Index() == 0 { // Already at the top, can't go further up p.mode = PagerModeNotFound{pager: p} return } // Start searching on the first line above the top of the screen position := p.scrollPosition.PreviousLine(1) firstSearchIndex = *position.lineIndex(p) case p.isNotFound(): // Restart searching from the bottom p.mode = PagerModeViewing{pager: p} firstSearchIndex = *linemetadata.IndexFromLength(p.Reader().GetLineCount()) default: panic(fmt.Sprint("Unknown search mode when finding previous: ", p.mode)) } hitIndex := FindFirstHit(p.Reader(), p.search, firstSearchIndex, nil, SearchDirectionBackward) if hitIndex == nil { p.mode = PagerModeNotFound{pager: p} return } p.scrollPosition = *scrollPositionFromIndex("scrollToPreviousSearchHit", *hitIndex) // Don't let any search hit scroll out of sight p.setTargetLine(nil) // Prefer hits to the right p.scrollMaxRight() if !p.searchHitIsVisible() { p.scrollLeftToSearchHits() } p.centerSearchHitsVertically() } // Return true if any search hit is currently visible on screen. // // A search hit is considered visible if the first character of the hit is // visible. This means that if the hit is longer than one character, the rest of // it may be off-screen to the right. If that happens, the user can scroll right // manually to see the rest of the hit. func (p *Pager) searchHitIsVisible() bool { for _, row := range p.renderLines().lines { for _, cell := range row.cells { if cell.StartsSearchHit { // Found a search hit on screen! return true } } } // No search hits found return false } func (p *Pager) centerSearchHitsVertically() { if p.WrapLongLines { // FIXME: Centering is not supported when wrapping, future improvement! return } for { rendered := p.renderLines() firstHitRow := -1 lastHitRow := -1 for rowIndex, row := range rendered.inputLines { if !p.search.Matches(row.Plain()) { continue } if firstHitRow == -1 { firstHitRow = rowIndex } lastHitRow = rowIndex } if firstHitRow == -1 || lastHitRow == -1 { log.Warn("No hits found while centering, how did we get here?") return } // If the visible height is 1, the center screen row is 0. centerScreenRowDoubled := p.visibleHeight() - 1 centerHitRowDoubled := firstHitRow + lastHitRow // Divide by 2 here to get the amount of rows we need to scroll. We // postponed the division by 2 until now to avoid rounding errors. // // If the center screen row is 1 (3 lines visible), and the center hit // row is 2 (last screen line), we need to arrow down once. deltaRows := (centerHitRowDoubled - centerScreenRowDoubled) / 2 newScrollPosition := p.scrollPosition.NextLine(deltaRows) if p.ScrollPositionsEqual(p.scrollPosition, newScrollPosition) { // No change, done! return } p.scrollPosition = newScrollPosition } } // If we are alredy too far right when you call this method, it will scroll // left. func (p *Pager) scrollMaxRight() { if p.WrapLongLines { // No horizontal scrolling when wrapping return } // First, render a screen scrolled to the far left so we know how much space // line numbers take. p.leftColumnZeroBased = 0 p.showLineNumbers = p.ShowLineNumbers rendered := p.renderLines() // Find the widest line, in screen cells. Some runes are double-width. widestLineWidth := 0 for _, inputLine := range rendered.inputLines { lineLength := inputLine.DisplayWidth() if lineLength > widestLineWidth { widestLineWidth = lineLength } } screenWidth, _ := p.screen.Size() availableWidth := screenWidth - rendered.numberPrefixWidth if widestLineWidth <= availableWidth { // All lines fit on screen, this means we're now max scrolled right return } p.showLineNumbers = false availableWidth += rendered.numberPrefixWidth if widestLineWidth <= availableWidth { // All lines fit on screen with line numbers off, this means we're now // max scrolled right return } // If the line width is 10 and the available width is also 10, we should // start at column 0. p.leftColumnZeroBased = widestLineWidth - availableWidth } // Scroll right looking for search hits. Return true if we found any. func (p *Pager) scrollRightToSearchHits() bool { if p.WrapLongLines { // No horizontal scrolling when wrapping return false } restoreShowLineNumbers := p.showLineNumbers restoreLeftColumn := p.leftColumnZeroBased // Check how far right we can scroll at most. Factors involved: // - Screen width // - Length of longest visible line screenWidth, _ := p.screen.Size() widestLineWidth := 0 // In screen cells, some runes are double-width rendered := p.renderLines() for _, inputLine := range rendered.inputLines { lineLength := inputLine.DisplayWidth() if lineLength > widestLineWidth { widestLineWidth = lineLength } } // With a 10 wide screen and a 15 wide line (max index 14), the leftmost // screen column can at most be 5: // // Screen column: 0123456789 // Line column: 5678901234 maxLeftmostColumn := widestLineWidth - screenWidth // If we have line numbers and disable them, do any new hits appear? if rendered.numberPrefixWidth > 0 { // If the number prefix width is 4, and the screen width is 10, then 6 should be // the first newly revealed column index (10-4): // // Screen column: 0123456789 // New cells: ______1234 // // But since the rightmost column can be covered by scroll-right we need to subtract // one more and get to 5. firstJustRevealedColumn := screenWidth - rendered.numberPrefixWidth - 1 if firstJustRevealedColumn <= 0 { log.Info("Screen too narrow ({}) to disable line numbers for search hits, skipping", screenWidth) return false } p.showLineNumbers = false for _, row := range p.renderLines().lines { for column := firstJustRevealedColumn; column < len(row.cells); column++ { if row.cells[column].StartsSearchHit { // Found a search hit on screen! return true } } } p.showLineNumbers = restoreShowLineNumbers } for p.leftColumnZeroBased < maxLeftmostColumn { // FIXME: Rather than scrolling right one screen at a time, we should // consider scanning all lines for search hits and scrolling directly to the // first one that is off-screen to the right. // If the screen width is 1, and we have no line numbers, the answer // could be 1. But since the last column could be covered by scroll-right // markers, we'll say 0. firstNotVisibleColumn := p.leftColumnZeroBased + screenWidth - rendered.numberPrefixWidth - 1 if firstNotVisibleColumn < 1 { log.Info("Screen is narrower than number prefix length, not scrolling right for search hits") p.showLineNumbers = restoreShowLineNumbers p.leftColumnZeroBased = restoreLeftColumn return false } // Minus one to account for the scroll-left marker that will cover the // first column after scrolling. scrollToColumn := firstNotVisibleColumn - 1 p.showLineNumbers = false p.leftColumnZeroBased = scrollToColumn if p.searchHitIsVisible() { // A new hit showed up! if p.leftColumnZeroBased > maxLeftmostColumn { // Scrolled beyond max, adjust p.leftColumnZeroBased = maxLeftmostColumn } return true } } // Can't scroll right, pretend nothing happened p.showLineNumbers = restoreShowLineNumbers p.leftColumnZeroBased = restoreLeftColumn return false } // Scroll left looking for search hits. Return true if we found any. func (p *Pager) scrollLeftToSearchHits() bool { if p.WrapLongLines { // No horizontal scrolling when wrapping return false } restoreLeftColumn := p.leftColumnZeroBased restoreShowLineNumbers := p.showLineNumbers screenWidth, _ := p.screen.Size() // If we go max left, which column will be the rightmost visible one? var fullLeftRightmostVisibleColumn int { p.showLineNumbers = p.ShowLineNumbers p.leftColumnZeroBased = 0 rendered := p.renderLines() // If the screen width is 2, we have columns 0 and 1. The rightmost column can be covered by // scroll-right markers, so the first not-visible column when fully scrolled left is 0, or // "2 - 2". fullLeftRightmostVisibleColumn = screenWidth - 2 - rendered.numberPrefixWidth p.leftColumnZeroBased = restoreLeftColumn p.showLineNumbers = restoreShowLineNumbers } if fullLeftRightmostVisibleColumn < 0 { log.Info("Screen too narrow ({}) to scroll left for search hits, skipping", screenWidth) return false } // Keep scrolling left until we either find a search hit, or reach the // leftmost column with line numbers shown or not based on the user's // preference. for p.leftColumnZeroBased > 0 || (p.showLineNumbers != p.ShowLineNumbers) { // FIXME: Rather than scrolling left one screen at a time, we should // consider scanning all lines for search hits and scrolling directly to the // first one that is off-screen to the left. // Pretend the current leftmost column is not visible, since it could be // covered by scroll-left markers. lastNotVisibleColumn := p.leftColumnZeroBased // Go left if lastNotVisibleColumn <= fullLeftRightmostVisibleColumn { // Going max left will show the column we want p.showLineNumbers = p.ShowLineNumbers p.leftColumnZeroBased = 0 } else { // Scroll left one screen. // // If the screen width is 3, and we want column 5 to be visible, and // there can be both scroll-left and scroll-right markers, we should // start at colum 4 (covered by a scroll-left marker), so that // column 5 is visible next to it. // // Set the leftmost column to 4, which is "5 - 3 + 2". scrollToColumn := lastNotVisibleColumn - screenWidth + 2 if scrollToColumn < 0 { scrollToColumn = 0 } p.leftColumnZeroBased = scrollToColumn // If showing line numbers was possible we should have ended up in // the other if branch ^ p.showLineNumbers = false } if p.searchHitIsVisible() { // Found it! return true } } // Scrolling left didn't find anything, pretend nothing happened p.showLineNumbers = restoreShowLineNumbers p.leftColumnZeroBased = restoreLeftColumn return false } func (p *Pager) isViewing() bool { _, isViewing := p.mode.(PagerModeViewing) return isViewing } func (p *Pager) isNotFound() bool { _, isNotFound := p.mode.(PagerModeNotFound) return isNotFound } moor-2.10.3/internal/pager-search_test.go000066400000000000000000000261171513574474500203420ustar00rootroot00000000000000package internal import ( "strings" "testing" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) func TestScrollMaxRight_AllLinesFitWithLineNumbers(t *testing.T) { // Case 2: All lines fit with line numbers screenWidth := 20 widestLineWidth := 16 // Just below available width line := strings.Repeat("x", widestLineWidth) reader := reader.NewFromTextForTesting("test", line) screen := twin.NewFakeScreen(screenWidth, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = true pager.WrapLongLines = false pager.scrollMaxRight() assert.Equal(t, 0, pager.leftColumnZeroBased) assert.Equal(t, true, pager.showLineNumbers) } func TestScrollMaxRight_AllLinesFitWithoutLineNumbers1(t *testing.T) { // Case 2: All lines fit with line numbers screenWidth := 20 widestLineWidth := 17 // Just above available width with line numbers line := strings.Repeat("x", widestLineWidth) reader := reader.NewFromTextForTesting("test", line) screen := twin.NewFakeScreen(screenWidth, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = true pager.WrapLongLines = false pager.scrollMaxRight() assert.Equal(t, 0, pager.leftColumnZeroBased) assert.Equal(t, false, pager.showLineNumbers) } func TestScrollMaxRight_AllLinesFitWithoutLineNumbers2(t *testing.T) { // Case 3: All lines fit only if line numbers are hidden, just at the edge screenWidth := 20 widestLineWidth := 20 // Above available with line numbers, just below without line := strings.Repeat("x", widestLineWidth) reader := reader.NewFromTextForTesting("test", line) screen := twin.NewFakeScreen(screenWidth, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = true pager.WrapLongLines = false pager.scrollMaxRight() assert.Equal(t, 0, pager.leftColumnZeroBased) assert.Equal(t, false, pager.showLineNumbers) } func TestScrollMaxRight_WidestLineExceedsScreenWidth_Edge(t *testing.T) { // Case 4: Widest line just exceeds available width even without line numbers screenWidth := 20 widestLineWidth := 21 // Just above available width without line numbers line := strings.Repeat("x", widestLineWidth) reader := reader.NewFromTextForTesting("test", line) screen := twin.NewFakeScreen(screenWidth, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = true pager.WrapLongLines = false pager.scrollMaxRight() assert.Equal(t, 1, pager.leftColumnZeroBased) assert.Equal(t, false, pager.showLineNumbers) } func modeName(pager *Pager) string { switch pager.mode.(type) { case PagerModeViewing: return "Viewing" case PagerModeNotFound: return "NotFound" case *PagerModeSearch: return "Search" case *PagerModeGotoLine: return "GotoLine" default: panic("Unknown pager mode") } } // Create a pager with three screen lines reading from a six lines stream func createThreeLinesPager(t *testing.T) *Pager { reader := reader.NewFromTextForTesting("", "a\nb\nc\nd\ne\nf\n") screen := twin.NewFakeScreen(20, 3) pager := NewPager(reader) pager.screen = screen assert.Equal(t, "Viewing", modeName(pager), "Initial pager state") return pager } func TestScrollToNextSearchHit_StartAtBottom(t *testing.T) { // Create a pager scrolled to the last line pager := createThreeLinesPager(t) pager.scrollToEnd() // Set the search to something that doesn't exist in this pager pager.search.For("xxx") // Scroll to the next search hit pager.scrollToNextSearchHit() assert.Equal(t, "NotFound", modeName(pager)) } func TestScrollToNextSearchHit_StartAtTop(t *testing.T) { // Create a pager scrolled to the first line pager := createThreeLinesPager(t) // Set the search to something that doesn't exist in this pager pager.search.For("xxx") // Scroll to the next search hit pager.scrollToNextSearchHit() assert.Equal(t, "NotFound", modeName(pager)) } func TestScrollToNextSearchHit_WrapAfterNotFound(t *testing.T) { // Create a pager scrolled to the last line pager := createThreeLinesPager(t) pager.scrollToEnd() // Search for "a", it's on the first line (ref createThreeLinesPager()) pager.search.For("a") // Scroll to the next search hit, this should take us into _NotFound pager.scrollToNextSearchHit() assert.Equal(t, "NotFound", modeName(pager)) // Scroll to the next search hit, this should wrap the search and take us to // the top pager.scrollToNextSearchHit() assert.Equal(t, "Viewing", modeName(pager)) assert.Assert(t, pager.lineIndex().IsZero()) } func TestScrollToNextSearchHit_WrapAfterFound(t *testing.T) { // Create a pager scrolled to the last line pager := createThreeLinesPager(t) pager.scrollToEnd() // Search for "f", it's on the last line (ref createThreeLinesPager()) pager.search.For("f") // Scroll to the next search hit, this should take us into _NotFound pager.scrollToNextSearchHit() assert.Equal(t, "NotFound", modeName(pager)) // Scroll to the next search hit, this should wrap the search and take us // back to the bottom again pager.scrollToNextSearchHit() assert.Equal(t, "Viewing", modeName(pager)) assert.Equal(t, 4, pager.lineIndex().Index()) } // Ref: https://github.com/walles/moor/issues/152 func Test152(t *testing.T) { // Show a pager on a five lines terminal reader := reader.NewFromTextForTesting("", "a\nab\nabc\nabcd\nabcde\nabcdef\n") screen := twin.NewFakeScreen(20, 5) pager := NewPager(reader) pager.screen = screen assert.Equal(t, "Viewing", modeName(pager), "Initial pager state") searchMode := NewPagerModeSearch(pager, SearchDirectionForward, pager.scrollPosition) pager.mode = searchMode // Search for the first not-visible hit searchMode.inputBox.setText("abcde") assert.Equal(t, "Search", modeName(pager)) assert.Equal(t, 2, pager.lineIndex().Index()) } func TestScrollLeftToSearchHits_NoLineNumbers(t *testing.T) { reader := reader.NewFromTextForTesting("", "a234567890") screen := twin.NewFakeScreen(10, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = false pager.showLineNumbers = false pager.search.For("a") pager.leftColumnZeroBased = 1 assert.Equal(t, true, pager.scrollLeftToSearchHits()) assert.Equal(t, 0, pager.leftColumnZeroBased) assert.Equal(t, false, pager.showLineNumbers) } func TestScrollLeftToSearchHits_WithLineNumbers(t *testing.T) { reader := reader.NewFromTextForTesting("", "a234567890") screen := twin.NewFakeScreen(10, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = true pager.showLineNumbers = false pager.search.For("a") pager.leftColumnZeroBased = 1 assert.Equal(t, true, pager.scrollLeftToSearchHits()) assert.Equal(t, 0, pager.leftColumnZeroBased) assert.Equal(t, true, pager.showLineNumbers) } func TestScrollLeftToSearchHits_ScrollOneScreen(t *testing.T) { reader := reader.NewFromTextForTesting("", "01234567890a234567890123456789") screen := twin.NewFakeScreen(10, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = true pager.showLineNumbers = false pager.search.For("a") pager.leftColumnZeroBased = 20 assert.Equal(t, true, pager.scrollLeftToSearchHits()) assert.Equal(t, 4, pager.leftColumnZeroBased, "We started at 20, screen is 10 wide, each scroll moves 8 to compensate for scroll markers, and 20-8-8=4") assert.Equal(t, false, pager.showLineNumbers) } // If the screen is too narrow for line numbers, there's no point in scrolling. // This test has provoked some panics. func TestScrollRightToSearchHits_NarrowScreen(t *testing.T) { reader := reader.NewFromTextForTesting("", "abcdefg") screen := twin.NewFakeScreen(1, 5) pager := NewPager(reader) pager.screen = screen pager.showLineNumbers = false assert.Equal(t, pager.scrollRightToSearchHits(), false) pager.showLineNumbers = true assert.Equal(t, pager.scrollRightToSearchHits(), false) } func TestScrollRightToSearchHits_DisableLineNumbersToSeeHit0(t *testing.T) { reader := reader.NewFromTextForTesting("", "12345a") screen := twin.NewFakeScreen(10, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = true pager.showLineNumbers = true pager.search.For("a") pager.leftColumnZeroBased = 0 assert.Equal(t, true, pager.scrollRightToSearchHits()) assert.Equal(t, 0, pager.leftColumnZeroBased, "Should scroll right to bring 'a' into view") assert.Equal(t, false, pager.showLineNumbers, "Should disable line numbers to fit search hit") } func TestScrollRightToSearchHits_DisableLineNumbersToSeeHit(t *testing.T) { reader := reader.NewFromTextForTesting("", "123456789a") screen := twin.NewFakeScreen(10, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = true pager.showLineNumbers = true pager.search.For("a") pager.leftColumnZeroBased = 0 assert.Equal(t, true, pager.scrollRightToSearchHits()) assert.Equal(t, 0, pager.leftColumnZeroBased, "Should scroll right to bring 'a' into view") assert.Equal(t, false, pager.showLineNumbers, "Should disable line numbers to fit search hit") } func TestScrollRightToSearchHits_HiddenByScrollMarker(t *testing.T) { reader := reader.NewFromTextForTesting("", "123456789a234567890") screen := twin.NewFakeScreen(10, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = false pager.showLineNumbers = false pager.search.For("a") pager.leftColumnZeroBased = 0 assert.Equal(t, true, pager.scrollRightToSearchHits()) assert.Equal(t, 8, pager.leftColumnZeroBased, "Should scroll right to bring 'a' into view from behind scroll marker") } // Repro case for https://github.com/walles/moor/issues/337. func TestScrollRightToSearchHits_Issue337(t *testing.T) { reader := reader.NewFromTextForTesting("", "123456a89012345") screen := twin.NewFakeScreen(10, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = false pager.showLineNumbers = false pager.search.For("a") pager.leftColumnZeroBased = 0 assert.Equal(t, false, pager.scrollRightToSearchHits(), "Search hit was already visible, should not have scrolled") assert.Equal(t, 0, pager.leftColumnZeroBased, "Should not have scrolled") } func TestScrollRightToSearchHits_LastCharHit(t *testing.T) { const line = "x0123456789a" reader := reader.NewFromTextForTesting("", line) screen := twin.NewFakeScreen(10, 5) pager := NewPager(reader) pager.screen = screen pager.ShowLineNumbers = false pager.showLineNumbers = false pager.search.For("a") pager.leftColumnZeroBased = 0 assert.Equal(t, true, pager.scrollRightToSearchHits()) width, _ := screen.Size() lastCol := pager.leftColumnZeroBased + width - 1 assert.Equal(t, strings.Index(line, "a"), lastCol, "Search hit should be in the last screen column") } func TestScrollRightToSearchHits_OnlyStartOfHitTriggers(t *testing.T) { // Arrange: create a line with a multi-rune search hit line := "abcDEFGHIJKLMNOPQRSTUVWXYZ" readerImpl := reader.NewFromTextForTesting("test", line) screen := twin.NewFakeScreen(5, 2) // Narrow screen to force scrolling pager := NewPager(readerImpl) pager.search.For("DEFGHIJ") // Match starts at index 3 pager.screen = screen pager.WrapLongLines = false pager.ShowLineNumbers = false pager.showLineNumbers = false assert.Assert(t, !pager.scrollRightToSearchHits(), "No more search hit starts to the right, should not scroll") } moor-2.10.3/internal/pager.go000066400000000000000000000532521513574474500160400ustar00rootroot00000000000000package internal import ( "fmt" "math" "runtime/debug" "sync" "time" "github.com/alecthomas/chroma/v2" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/internal/search" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/twin" ) type PagerMode interface { onKey(key twin.KeyCode) onRune(char rune) drawFooter(filenameText string, statusText string, spinner string) } type StatusBarOption int const ( //revive:disable-next-line:var-naming STATUSBAR_STYLE_INVERSE StatusBarOption = iota //revive:disable-next-line:var-naming STATUSBAR_STYLE_PLAIN //revive:disable-next-line:var-naming STATUSBAR_STYLE_BOLD ) type eventSpinnerUpdate struct { spinner string } type eventMoreLinesAvailable struct{} // Either reading, highlighting or both are done. Check reader.Done() and // reader.HighlightingDone() for details. type eventMaybeDone struct{} // Pager is the main on-screen pager type Pager struct { readers []*reader.ReaderImpl // Immutable since startup, no locking needed currentReader int // Index into the readers slice readerLock sync.Mutex // Protects currentReader readerSwitched chan struct{} // A view of the current reader, possibly filtered filteringReader FilteringReader screen twin.Screen quit bool scrollPosition scrollPosition leftColumnZeroBased int // Maybe this should be renamed to "controller"? Because it controls the UI? // But since we replace it in a lot of places based on the UI mode, maybe // mode is better? mode PagerMode search search.Search // This should never be null while paging. Configured in NewPager(). searchHistory *SearchHistory filter search.Search // We used to have a "Following" field here. If you want to follow, set // TargetLineNumber to linemetadata.IndexMax() instead, see below. isShowingHelp bool preHelpState *_PreHelpState // User preference ShowLineNumbers bool // Current state, initialized in StartPaging() showLineNumbers bool StatusBarStyle StatusBarOption ShowStatusBar bool UnprintableStyle textstyles.UnprintableStyleT WrapLongLines bool // Ref: https://github.com/walles/moor/issues/113 QuitIfOneScreen bool // Ref: https://github.com/walles/moor/issues/94 ScrollLeftHint textstyles.CellWithMetadata ScrollRightHint textstyles.CellWithMetadata SideScrollAmount int // Left / right arrow keys scroll amount TabSize int // Number of spaces per tab, default 8, should be positive // If non-nil, scroll to this line as soon as possible. Set this value to // IndexMax() to follow the end of the input (tail). // // NOTE: Always use setTargetLine() to keep the reader in sync with the // pager! TargetLine *linemetadata.Index // If true, pager will clear the screen on return. If false, pager will // clear the last line, and show the cursor. DeInit bool // If DeInit is false, leave this number of lines for the shell prompt after // exiting DeInitFalseMargin int WithTerminalFg bool // If true, don't set linePrefix // If false, don't highlight lines with search hits (but still highlight the // actual hits) WithSearchHitLineBackground bool // Length of the longest line displayed. This is used for limiting scrolling // to the right. longestLineLength int // Bookmarks that you can come back to. // // Ref: https://github.com/walles/moor/issues/175 bookmarks map[rune]scrollPosition AfterExit func() error } type _PreHelpState struct { scrollPosition scrollPosition leftColumnZeroBased int targetLine *linemetadata.Index } var _HelpReader = reader.NewFromTextForTesting("Help", ` Welcome to Moor, the nice pager! Miscellaneous ------------- * Press 'q' or 'ESC' to quit * Press 'w' to toggle wrapping of long lines * Press '=' to toggle showing the status bar at the bottom * Press 'v' to edit the file in your favorite editor * Press CTRL-t to change the tab size Moving around ------------- * Arrow keys * Alt key plus left / right arrow steps one column at a time * Left / right can be used to hide / show line numbers * Home and End for start / end of the document * 'g' for going to a specific line number * 'm' sets a mark, you will be asked for a letter to label it with * ' (single quote) jumps to the mark * CTRL-p moves to the previous line * CTRL-n moves to the next line * PageUp / 'b' and PageDown / 'f' * SPACE moves down a page * < / 'gg' to go to the start of the document * > / 'G' to go to the end of the document * Half page 'u'p / 'd'own, or CTRL-u / CTRL-d * CTRL-a moves to the leftmost position * RETURN moves down one line Switching files (if you opened multiple files) ---------------------------------------------- * Press ':' to enter file switching mode Filtering --------- Type '&' to start filtering, then type your filter expression. While filtering, arrow keys, PageUp, PageDown, Home and End work as usual. Press 'ESC' or RETURN to exit filtering mode. Searching --------- * Type / to start searching, then type what you want to find * Type ? to search backwards, then type what you want to find * Type RETURN to stop searching, or ESC to skip back to where the search started * Press up / down arrows while searching to access search history * Find next by typing 'n' (for "next") * Find previous by typing SHIFT-N or 'p' (for "previous") * Search is case sensitive if it contains any UPPER CASE CHARACTERS * Search is interpreted as a regexp if it is a valid one Reporting bugs -------------- File issues at https://github.com/walles/moor/issues, or post questions to johan.walles@gmail.com. Installing Moor as your default pager ------------------------------------- Put the following line in your ~/.bashrc, ~/.bash_profile or ~/.zshrc: export PAGER=moor Source Code ----------- Available at https://github.com/walles/moor/. `) // NewPager creates a new Pager with default settings func NewPager(readers ...*reader.ReaderImpl) *Pager { if len(readers) == 0 { panic("NewPager() needs at least one reader") } var name string if readers[0] == nil || readers[0].DisplayName == nil || len(*readers[0].DisplayName) == 0 { name = "Pager" } else { name = "Pager " + *readers[0].DisplayName } pager := Pager{ readers: readers, currentReader: 0, readerSwitched: make(chan struct{}, 1), quit: false, ShowLineNumbers: true, // Constant throghout the lifetime of the pager showLineNumbers: true, // Will be updated over time ShowStatusBar: true, DeInit: true, SideScrollAmount: 16, TabSize: 8, // This is what less defaults to ScrollLeftHint: textstyles.CellWithMetadata{Rune: '<', Style: twin.StyleDefault.WithAttr(twin.AttrReverse)}, ScrollRightHint: textstyles.CellWithMetadata{Rune: '>', Style: twin.StyleDefault.WithAttr(twin.AttrReverse)}, scrollPosition: newScrollPosition(name), WithSearchHitLineBackground: true, } pager.mode = PagerModeViewing{pager: &pager} pager.filteringReader = FilteringReader{ BackingReader: readers[0], // Always start with the first reader Filter: &pager.filter, } searchHistory := BootSearchHistory("") pager.searchHistory = &searchHistory return &pager } // How many lines are visible on screen? Depends on screen height and whether or // not the status bar is visible. func (p *Pager) visibleHeight() int { _, height := p.screen.Size() // Only the viewing mode can be without status bar hasStatusBar := p.ShowStatusBar || !p.isViewing() if hasStatusBar { return height - 1 } return height } // How many cells are needed for this line number? Includes padding. // // Returns 0 if line numbers are disabled. func (p *Pager) getLineNumberPrefixLength(lineNumber linemetadata.Number) int { if !p.showLineNumbers { return 0 } length := len(lineNumber.Format()) + 1 // +1 for the space after the line number if length < 4 { // 4 = space for 3 digits followed by one whitespace // // https://github.com/walles/moor/issues/38 return 4 } return length } // Single quoted parts of the help text will be highlighted relative to the // status bar style. func renderHelpText(help string) []twin.StyledRune { var result []twin.StyledRune shortcutHighlight := twin.AttrBold if statusbarStyle.HasAttr(shortcutHighlight) { shortcutHighlight = twin.AttrUnderline } if statusbarStyle.HasAttr(shortcutHighlight) { shortcutHighlight = twin.AttrReverse } style := statusbarStyle for _, token := range help { if token == '\'' { // Highlight things within single quotes if style == statusbarStyle { style = statusbarStyle.WithAttr(shortcutHighlight) } else { style = statusbarStyle } continue } result = append(result, twin.NewStyledRune(token, style)) } return result } // Draw the footer string at the bottom using the status bar style. // // Single quoted parts of the help text will be bolded. // // prefix example value: "[1/3] " // filename example value: "file.txt" // status example value: ": 123 lines 0%" // help example value: "Press 'h' for help, 'q' to quit" func (p *Pager) setFooter(prefix string, filename string, status string, help string) { width, height := p.screen.Size() pos := 0 // Prefix (multiple open files) for _, token := range prefix { pos += p.screen.SetCell(pos, height-1, twin.NewStyledRune(token, statusbarStyle)) } // File name for _, token := range filename { pos += p.screen.SetCell(pos, height-1, twin.NewStyledRune(token, statusbarFileStyle)) } // percentage, for _, token := range status + " " { pos += p.screen.SetCell(pos, height-1, twin.NewStyledRune(token, statusbarStyle)) } // Help text, highlight keyboard shortcuts for _, cell := range renderHelpText(help) { pos += p.screen.SetCell(pos, height-1, cell) } for pos < width { pos += p.screen.SetCell(pos, height-1, twin.NewStyledRune(' ', statusbarStyle)) } } // Quit leaves the help screen or quits the pager func (p *Pager) Quit() { if !p.isShowingHelp { p.quit = true return } // Reset help p.isShowingHelp = false p.scrollPosition = p.preHelpState.scrollPosition p.leftColumnZeroBased = p.preHelpState.leftColumnZeroBased p.setTargetLine(p.preHelpState.targetLine) p.preHelpState = nil } // Negative deltas move left instead func (p *Pager) moveRight(delta int) { if p.showLineNumbers && delta > 0 { p.showLineNumbers = false return } if p.leftColumnZeroBased == 0 && delta < 0 { p.showLineNumbers = true return } result := p.leftColumnZeroBased + delta if result < 0 { p.leftColumnZeroBased = 0 } else { p.leftColumnZeroBased = result } // If we try to move past the characters when moving right, stop scrolling to // avoid moving infinitely into the void. if p.leftColumnZeroBased > p.longestLineLength { p.leftColumnZeroBased = p.longestLineLength } } func (p *Pager) Reader() reader.Reader { if p.isShowingHelp { return _HelpReader } return &p.filteringReader } func (p *Pager) handleScrolledUp() { p.setTargetLine(nil) } func (p *Pager) handleScrolledDown() { if p.isScrolledToEnd() { // Follow output reallyHigh := linemetadata.IndexMax() p.setTargetLine(&reallyHigh) } else { p.setTargetLine(nil) } } func (p *Pager) handleMoreLinesAvailable() { // Without the isViewing() check, following will continue while // searching, and I prefer it to stop so people can see what they // are searching in. if !p.isViewing() || p.TargetLine == nil { return } // The user wants to scroll down to a specific line number lineCount := p.Reader().GetLineCount() if lineCount == 0 { // No lines yet, keep waiting return } if linemetadata.IndexFromLength(lineCount).IsBefore(*p.TargetLine) { // Not there yet, keep scrolling p.scrollToEnd() return } // We see the target, scroll to it p.scrollPosition = NewScrollPositionFromIndex(*p.TargetLine, "goToTargetLine") p.setTargetLine(nil) } // Except for setting TargetLine, this method also syncs with the reader so that // the reader knows how many lines it needs to fetch. func (p *Pager) setTargetLine(targetLine *linemetadata.Index) { p.readerLock.Lock() r := p.readers[p.currentReader] p.readerLock.Unlock() log.Trace("Pager: Setting target line to ", targetLine, "...") p.TargetLine = targetLine if targetLine == nil { // No target, just do your thing r.SetPauseAfterLines(reader.DEFAULT_PAUSE_AFTER_LINES) return } // Set the target with some lookahead to avoid fetching too few lines. targetValue := targetLine.Index() + reader.DEFAULT_PAUSE_AFTER_LINES/2 if targetValue < targetLine.Index() { // Overflow detected, clip to max int targetValue = math.MaxInt } if targetValue < reader.DEFAULT_PAUSE_AFTER_LINES { targetValue = reader.DEFAULT_PAUSE_AFTER_LINES } r.SetPauseAfterLines(targetValue) } // StartPaging brings up the pager on screen func (p *Pager) StartPaging(screen twin.Screen, chromaStyle *chroma.Style, chromaFormatter *chroma.Formatter) { log.Info("Pager starting") defer func() { p.readerLock.Lock() r := p.readers[p.currentReader] p.readerLock.Unlock() if r.Err != nil { log.Warnf("Reader reported an error: %s", r.Err.Error()) } }() p.showLineNumbers = p.ShowLineNumbers textstyles.UnprintableStyle = p.UnprintableStyle if p.TabSize > 0 { // "0" = unset, stay at the default. If the tab size is negative, just // ignoring it seems like the right move. textstyles.TabSize = p.TabSize } consumeLessTermcapEnvs(screen.TerminalBackground(), chromaStyle, chromaFormatter) styleUI(screen.TerminalBackground(), chromaStyle, chromaFormatter, p.StatusBarStyle, p.WithTerminalFg, p.WithSearchHitLineBackground) p.screen = screen p.mode = PagerModeViewing{pager: p} p.bookmarks = make(map[rune]scrollPosition) // Make sure the reader knows how many lines we want p.setTargetLine(p.TargetLine) go func() { defer func() { PanicHandler("StartPaging()/goroutine", recover(), debug.Stack()) }() spinnerFrames := [...]string{"/.\\", "-o-", "\\O/", "| |"} spinnerIndex := 0 spinnerTicker := time.NewTicker(200 * time.Millisecond) lastSpinnerFrame := "UNSET" // Track the last spinner frame to avoid unnecessary redraws // Support throttling of more-lines-available reads, see below p.readerLock.Lock() throttledMoreLines := p.readers[p.currentReader].MoreLinesAdded p.readerLock.Unlock() var reenable <-chan time.Time for { p.readerLock.Lock() r := p.readers[p.currentReader] p.readerLock.Unlock() select { case <-p.readerSwitched: // A different reader is now active p.filter = search.Search{} p.readerLock.Lock() r = p.readers[p.currentReader] p.filteringReader.SetBackingReader(r) p.readerLock.Unlock() // Look in the right place for more lines throttledMoreLines = r.MoreLinesAdded reenable = nil // Reset spinner for new reader so that we show it again if needed lastSpinnerFrame = "UNSET" // Tell the viewer to replace the view screen.Events() <- eventMoreLinesAvailable{} case <-throttledMoreLines: screen.Events() <- eventMoreLinesAvailable{} // Disable further receives for 200ms. This avoids flooding the // event loop if a lot of lines are added in a short time. throttledMoreLines = nil reenable = time.After(200 * time.Millisecond) case <-reenable: // Re-enable channel throttledMoreLines = r.MoreLinesAdded reenable = nil case <-spinnerTicker.C: currentSpinnerFrame := spinnerFrames[spinnerIndex] if r.ReadingDone.Load() { // We're done, clear the spinner currentSpinnerFrame = "" } spinnerIndex++ if spinnerIndex >= len(spinnerFrames) { spinnerIndex = 0 } if currentSpinnerFrame == lastSpinnerFrame { // Prevent unnecessary redraws continue } screen.Events() <- eventSpinnerUpdate{currentSpinnerFrame} lastSpinnerFrame = currentSpinnerFrame case <-r.MaybeDone: screen.Events() <- eventMaybeDone{} } } }() log.Info("Entering pager main loop...") // Main loop spinner := "" for !p.quit { if len(screen.Events()) == 0 { // Nothing more to process for now, redraw the screen p.redraw(spinner) p.readerLock.Lock() r := p.readers[p.currentReader] p.readerLock.Unlock() // Ref: // https://github.com/gwsw/less/blob/ff8869aa0485f7188d942723c9fb50afb1892e62/command.c#L828-L831 // // Note that we do the slow (atomic) checks only if the fast ones // (no locking required) passed. // // Also, we only do this if we have exactly one reader, because // that's what less does. if len(p.readers) == 1 && p.QuitIfOneScreen && !p.isShowingHelp && r.ReadingDone.Load() && r.HighlightingDone.Load() { if p.fitsOnOneScreen() { // Ref: // https://github.com/walles/moor/issues/113#issuecomment-1368294132 p.showLineNumbers = false // Requires a redraw to take effect, see below p.DeInit = false p.quit = true // Without this the line numbers setting ^ won't take effect p.redraw(spinner) log.Info("Exiting because of --quit-if-one-screen, everything fit on one screen and we're done") break } } } event := <-screen.Events() switch event := event.(type) { case twin.EventKeyCode: log.Tracef("Handling key event %d...", event.KeyCode()) p.mode.onKey(event.KeyCode()) case twin.EventRune: log.Tracef("Handling rune event '%c'/0x%04x...", event.Rune(), event.Rune()) p.mode.onRune(event.Rune()) case twin.EventMouse: log.Tracef("Handling mouse event %d...", event.Buttons()) switch event.Buttons() { case twin.MouseWheelUp: // Clipping is done in _Redraw() p.scrollPosition = p.scrollPosition.PreviousLine(1) case twin.MouseWheelDown: // Clipping is done in _Redraw() p.scrollPosition = p.scrollPosition.NextLine(1) case twin.MouseWheelLeft: p.moveRight(-p.SideScrollAmount) case twin.MouseWheelRight: p.moveRight(p.SideScrollAmount) } case twin.EventResize: // We'll be implicitly redrawn just by taking another lap in the loop case twin.EventExit: log.Info("Got a Twin exit event, exiting") return case eventMoreLinesAvailable: p.handleMoreLinesAvailable() case eventMaybeDone: // Man pages come pre-formatted for the screen width, and line // numbers will mess that up. So we disable line numbers if we // detect a man page by its contents. // // See also noLineNumbersDefault() where we use environment // variables to try to detect man paging. if p.haveLoadedManPage() && len(p.readers) == 1 { p.ShowLineNumbers = false p.showLineNumbers = false log.Info("man page detected by contents, disabling line numbers") } case eventSpinnerUpdate: spinner = event.spinner default: log.Warnf("Unhandled event type: %v", event) } } log.Info("Pager main loop done") } // The height parameter is the terminal height minus the height of the user's // shell prompt. // // This way nothing gets scrolled off screen after we exit. func (p *Pager) fitsOnOneScreenWrapped() bool { if len(p.readers) != 1 { // At most one screen will fit on one screen... return false } // Create a fake screen of height + 1 lines width, height := p.screen.Size() // If the screen height is one, and the prompt height is zero, then the last // line number will be zero. But since we want one extra line to check for // overflow, we now want the last line number to be one. // // So if the initial height is 1, we want the last line number to be 1. // Which is what we get from here. lastScreenRow := height - p.DeInitFalseMargin // If the last screen row is supposed to be one, we need to set the height // to two. So we add one here. testScreenHeight := lastScreenRow + 1 testScreen := twin.NewFakeScreen(width, testScreenHeight) // Create a fake pager for that screen, with no status bar, and matching // line number settings + tab size and wrap settings p.readerLock.Lock() fakePager := NewPager(p.readers[0]) p.readerLock.Unlock() fakePager.screen = testScreen // If we drop out because of quit-if-one-screen, we will not print any line numbers fakePager.showLineNumbers = false fakePager.WrapLongLines = p.WrapLongLines fakePager.ShowStatusBar = false // We are only interested in content lines fakePager.TabSize = p.TabSize // Render on our test screen rendered := fakePager.renderLines() return len(rendered.lines) < testScreenHeight } func (p *Pager) fitsOnOneScreen() bool { if len(p.readers) != 1 { // At most one screen will fit on one screen... return false } if p.WrapLongLines { return p.fitsOnOneScreenWrapped() } width, height := p.screen.Size() height -= p.DeInitFalseMargin p.readerLock.Lock() reader := p.readers[0] p.readerLock.Unlock() if reader.GetLineCount() > height { return false } lines := reader.GetLines(linemetadata.Index{}, reader.GetLineCount()) for _, line := range lines.Lines { rendered := line.HighlightedTokens(twin.StyleDefault, twin.StyleDefault, search.Search{}, width+1).StyledRunes if len(rendered) > width { // This line is too long to fit on one screen line, no fit return false } } return true } // After the pager has exited and the normal screen has been restored, you can // call this method to print the pager contents to screen again, faking // "leaving" pager contents on screen after exit. func (p *Pager) ReprintAfterExit() { // Figure out how many screen lines are used by pager contents renderedScreen := p.renderLines() screenLinesCount := len(renderedScreen.lines) _, screenHeight := p.screen.Size() screenHeightWithoutFooter := screenHeight - p.DeInitFalseMargin if screenLinesCount > screenHeightWithoutFooter { screenLinesCount = screenHeightWithoutFooter } if screenLinesCount > 0 { p.screen.ShowNLines(screenLinesCount) fmt.Println() } } moor-2.10.3/internal/pager_test.go000066400000000000000000000547711513574474500171060ustar00rootroot00000000000000package internal import ( "fmt" "os" "path" "runtime" "strings" "testing" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/formatters" "github.com/alecthomas/chroma/v2/styles" "github.com/google/go-cmp/cmp" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) // NOTE: You can find related tests in screenLines_test.go. const blueBackgroundClearToEol0 = "\x1b[44m\x1b[0K" // With 0 before the K, should clear to EOL const blueBackgroundClearToEol = "\x1b[44m\x1b[K" // No 0 before the K, should also clear to EOL const samplesDir = "../sample-files" func TestUnicodeRendering(t *testing.T) { reader := reader.NewFromTextForTesting("", "åäö") var answers = []twin.StyledRune{ twin.NewStyledRune('Ã¥', twin.StyleDefault), twin.NewStyledRune('ä', twin.StyleDefault), twin.NewStyledRune('ö', twin.StyleDefault), } contents := startPaging(t, reader).GetRow(0) for pos, expected := range answers { assertRunesEqual(t, expected, contents[pos]) } } func assertRunesEqual(t *testing.T, expected twin.StyledRune, actual twin.StyledRune) { t.Helper() if actual.Rune == expected.Rune && actual.Style == expected.Style { return } t.Errorf("Expected %v, got %v", expected, actual) } func TestFgColorRendering(t *testing.T) { reader := reader.NewFromTextForTesting("", "\x1b[30ma\x1b[31mb\x1b[32mc\x1b[33md\x1b[34me\x1b[35mf\x1b[36mg\x1b[37mh\x1b[0mi") var answers = []twin.StyledRune{ twin.NewStyledRune('a', twin.StyleDefault.WithForeground(twin.NewColor16(0))), twin.NewStyledRune('b', twin.StyleDefault.WithForeground(twin.NewColor16(1))), twin.NewStyledRune('c', twin.StyleDefault.WithForeground(twin.NewColor16(2))), twin.NewStyledRune('d', twin.StyleDefault.WithForeground(twin.NewColor16(3))), twin.NewStyledRune('e', twin.StyleDefault.WithForeground(twin.NewColor16(4))), twin.NewStyledRune('f', twin.StyleDefault.WithForeground(twin.NewColor16(5))), twin.NewStyledRune('g', twin.StyleDefault.WithForeground(twin.NewColor16(6))), twin.NewStyledRune('h', twin.StyleDefault.WithForeground(twin.NewColor16(7))), twin.NewStyledRune('i', twin.StyleDefault), } contents := startPaging(t, reader).GetRow(0) for pos, expected := range answers { assertRunesEqual(t, expected, contents[pos]) } } func TestPageEmpty(t *testing.T) { // "---" is the eofSpinner of pager.go assert.Equal(t, "---", renderTextLine("")) } func TestBrokenUtf8(t *testing.T) { // The broken UTF8 character in the middle is based on "©" = 0xc2a9 reader := reader.NewFromTextForTesting("", "abc\xc2def") var answers = []twin.StyledRune{ twin.NewStyledRune('a', twin.StyleDefault), twin.NewStyledRune('b', twin.StyleDefault), twin.NewStyledRune('c', twin.StyleDefault), twin.NewStyledRune('?', twin.StyleDefault.WithForeground(twin.NewColor16(7)).WithBackground(twin.NewColor16(1))), twin.NewStyledRune('d', twin.StyleDefault), twin.NewStyledRune('e', twin.StyleDefault), twin.NewStyledRune('f', twin.StyleDefault), } contents := startPaging(t, reader).GetRow(0) for pos, expected := range answers { assertRunesEqual(t, expected, contents[pos]) } } func startPaging(t *testing.T, reader *reader.ReaderImpl) *twin.FakeScreen { // 0 means default tab size. Defaults to 8 to be like less. return startPagingWithTabSizeAndScreen(t, 0, twin.NewFakeScreen(20, 10), reader) } func startPagingWithTabSize(t *testing.T, tabSize int, reader *reader.ReaderImpl) *twin.FakeScreen { return startPagingWithTabSizeAndScreen(t, tabSize, twin.NewFakeScreen(20, 10), reader) } func startPagingWithScreen(t *testing.T, screen *twin.FakeScreen, reader *reader.ReaderImpl) *twin.FakeScreen { // 0 means default tab size. Defaults to 8 to be like less. return startPagingWithTabSizeAndScreen(t, 0, screen, reader) } func startPagingWithTabSizeAndScreen(t *testing.T, tabSize int, screen *twin.FakeScreen, reader *reader.ReaderImpl) *twin.FakeScreen { err := reader.Wait() if err != nil { t.Fatalf("Failed waiting for reader: %v", err) } pager := NewPager(reader) pager.TabSize = tabSize pager.ShowLineNumbers = false pager.showLineNumbers = false // Tell our Pager to quit immediately pager.Quit() // Except for just quitting, this also associates our FakeScreen with the Pager pager.StartPaging(screen, nil, nil) // This makes sure at least one frame gets rendered pager.redraw("") return screen } // Set style to "native" and use the TTY16m formatter func startPagingWithTerminalFg(t *testing.T, reader *reader.ReaderImpl, withTerminalFg bool) *twin.FakeScreen { err := reader.Wait() if err != nil { t.Fatalf("Failed waiting for reader: %v", err) } screen := twin.NewFakeScreen(20, 10) pager := NewPager(reader) pager.ShowLineNumbers = false pager.showLineNumbers = false pager.WithTerminalFg = withTerminalFg // Tell our Pager to quit immediately pager.Quit() // Except for just quitting, this also associates our FakeScreen with the Pager pager.StartPaging(screen, styles.Get("native"), &formatters.TTY16m) // This makes sure at least one frame gets rendered pager.redraw("") return screen } // assertIndexOfFirstX verifies the (zero-based) index of the first 'x' func assertIndexOfFirstX(t *testing.T, tabSize int, s string, expectedIndex int) { reader := reader.NewFromTextForTesting("", s) contents := startPagingWithTabSize(t, tabSize, reader).GetRow(0) for pos, cell := range contents { if cell.Rune != 'x' { continue } if pos == expectedIndex { // Success! return } t.Errorf("Expected first 'x' with tab size %d to be at (zero-based) index %d, but was at %d: \"%s\"", tabSize, expectedIndex, pos, strings.ReplaceAll(s, "\x09", "")) return } panic("No 'x' found") } func TestTabHandling(t *testing.T) { assertIndexOfFirstX(t, 4, "x", 0) assertIndexOfFirstX(t, 4, "\x09x", 4) assertIndexOfFirstX(t, 4, "\x09\x09x", 8) assertIndexOfFirstX(t, 4, "J\x09x", 4) assertIndexOfFirstX(t, 4, "Jo\x09x", 4) assertIndexOfFirstX(t, 4, "Joh\x09x", 4) assertIndexOfFirstX(t, 4, "Joha\x09x", 8) assertIndexOfFirstX(t, 4, "Johan\x09x", 8) assertIndexOfFirstX(t, 4, "\x09J\x09x", 8) assertIndexOfFirstX(t, 4, "\x09Jo\x09x", 8) assertIndexOfFirstX(t, 4, "\x09Joh\x09x", 8) assertIndexOfFirstX(t, 4, "\x09Joha\x09x", 12) assertIndexOfFirstX(t, 4, "\x09Johan\x09x", 12) } func TestTabHandling_TabSize8(t *testing.T) { assertIndexOfFirstX(t, 8, "\x09x", 8) } func TestCodeHighlighting(t *testing.T) { // From: https://coderwall.com/p/_fmbug/go-get-path-to-current-file _, filename, _, ok := runtime.Caller(0) if !ok { panic("Getting current filename failed") } reader, err := reader.NewFromFilename(filename, formatters.TTY16m, reader.ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, reader.Wait()) packageKeywordStyle := twin.StyleDefault.WithAttr(twin.AttrBold).WithForeground(twin.NewColorHex(0x6AB825)) packageSpaceStyle := twin.StyleDefault.WithForeground(twin.NewColorHex(0x666666)) packageNameStyle := twin.StyleDefault.WithForeground(twin.NewColorHex(0xD0D0D0)) var answers = []twin.StyledRune{ twin.NewStyledRune('p', packageKeywordStyle), twin.NewStyledRune('a', packageKeywordStyle), twin.NewStyledRune('c', packageKeywordStyle), twin.NewStyledRune('k', packageKeywordStyle), twin.NewStyledRune('a', packageKeywordStyle), twin.NewStyledRune('g', packageKeywordStyle), twin.NewStyledRune('e', packageKeywordStyle), twin.NewStyledRune(' ', packageSpaceStyle), twin.NewStyledRune('i', packageNameStyle), twin.NewStyledRune('n', packageNameStyle), twin.NewStyledRune('t', packageNameStyle), twin.NewStyledRune('e', packageNameStyle), twin.NewStyledRune('r', packageNameStyle), twin.NewStyledRune('n', packageNameStyle), twin.NewStyledRune('a', packageNameStyle), twin.NewStyledRune('l', packageNameStyle), } contents := startPaging(t, reader).GetRow(0) for pos, expected := range answers { assertRunesEqual(t, expected, contents[pos]) } } func TestCodeHighlight_compressed(t *testing.T) { // Same as TestCodeHighlighting but with "compressed-markdown.md.gz" reader, err := reader.NewFromFilename("../sample-files/compressed-markdown.md.gz", formatters.TTY16m, reader.ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, reader.Wait()) markdownHeading1Style := twin.StyleDefault.WithAttr(twin.AttrBold).WithForeground(twin.NewColorHex(0xffffff)) var answers = []twin.StyledRune{ twin.NewStyledRune('#', markdownHeading1Style), twin.NewStyledRune(' ', markdownHeading1Style), twin.NewStyledRune('M', markdownHeading1Style), twin.NewStyledRune('a', markdownHeading1Style), twin.NewStyledRune('r', markdownHeading1Style), twin.NewStyledRune('k', markdownHeading1Style), twin.NewStyledRune('d', markdownHeading1Style), twin.NewStyledRune('o', markdownHeading1Style), twin.NewStyledRune('w', markdownHeading1Style), twin.NewStyledRune('n', markdownHeading1Style), } contents := startPaging(t, reader).GetRow(0) for pos, expected := range answers { assertRunesEqual(t, expected, contents[pos]) } } // Regression test for: // https://github.com/walles/moor/issues/236#issuecomment-2282677792 // // Sample file sysctl.h from: // https://github.com/fastfetch-cli/fastfetch/blob/f9597eba39d6afd278eeca2f2972f73a7e54f111/src/common/sysctl.h func TestCodeHighlightingIncludes(t *testing.T) { reader, err := reader.NewFromFilename("../sample-files/sysctl.h", formatters.TTY16m, reader.ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, reader.Wait()) screen := startPaging(t, reader) firstIncludeLine := screen.GetRow(2) secondIncludeLine := screen.GetRow(3) // Both should start with "#include" colored the same way assertRunesEqual(t, firstIncludeLine[0], secondIncludeLine[0]) } func TestUnicodePrivateUse(t *testing.T) { // This character lives in a Private Use Area: // https://codepoints.net/U+f244 // // It's used by Font Awesome as "fa-battery-empty": // https://fontawesome.com/v4/icon/battery-empty char := '\uf244' reader := reader.NewFromTextForTesting("hello", string(char)) renderedRune := startPaging(t, reader).GetRow(0)[0] // Make sure we display this character unmodified assertRunesEqual(t, twin.NewStyledRune(char, twin.StyleDefault), renderedRune) } func resetManPageFormat() { textstyles.ManPageBold = twin.StyleDefault.WithAttr(twin.AttrBold) textstyles.ManPageUnderline = twin.StyleDefault.WithAttr(twin.AttrUnderline) } func testManPageFormatting(t *testing.T, input string, expected twin.StyledRune) { t.Helper() reader := reader.NewFromTextForTesting("", input) // Without these lines the man page tests will fail if either of these // environment variables are set when the tests are run. assert.NilError(t, os.Setenv("LESS_TERMCAP_md", "")) assert.NilError(t, os.Setenv("LESS_TERMCAP_us", "")) assert.NilError(t, os.Setenv("LESS_TERMCAP_so", "")) resetManPageFormat() contents := startPaging(t, reader).GetRow(0) assertRunesEqual(t, expected, contents[0]) assert.Equal(t, contents[1].Rune, ' ') } func TestManPageFormatting(t *testing.T) { testManPageFormatting(t, "n\x08n", twin.NewStyledRune('n', twin.StyleDefault.WithAttr(twin.AttrBold))) testManPageFormatting(t, "_\x08x", twin.NewStyledRune('x', twin.StyleDefault.WithAttr(twin.AttrUnderline))) // Non-breaking space UTF-8 encoded (0xc2a0) should render as a non-breaking unicode space (0xa0) testManPageFormatting(t, string([]byte{0xc2, 0xa0}), twin.NewStyledRune(rune(0xa0), twin.StyleDefault)) // Corner cases testManPageFormatting(t, "\x08", twin.NewStyledRune('<', twin.StyleDefault.WithForeground(twin.NewColor16(7)).WithBackground(twin.NewColor16(1)))) // FIXME: Test two consecutive backspaces // FIXME: Test backspace between two uncombinable characters } func TestScrollToBottomWrapNextToLastLine(t *testing.T) { reader := reader.NewFromTextForTesting("", "first line\nline two will be wrapped\nhere's the last line") // Heigh 3 = two lines of contents + one footer screen := twin.NewFakeScreen(10, 3) pager := NewPager(reader) pager.WrapLongLines = true pager.ShowLineNumbers = false pager.showLineNumbers = false pager.screen = screen assert.NilError(t, pager.readers[pager.currentReader].Wait()) // This is what we're testing really pager.scrollToEnd() // Exit immediately pager.Quit() // Get contents onto our fake screen pager.StartPaging(screen, nil, nil) pager.redraw("") actual := strings.Join([]string{ rowToString(screen.GetRow(0)), rowToString(screen.GetRow(1)), rowToString(screen.GetRow(2)), }, "\n") expected := strings.Join([]string{ "here's the", "last line", "3 lines 1", // "3 lines 100%" clipped after 10 characters (screen width) }, "\n") assert.Equal(t, actual, expected) } // Repro for https://github.com/walles/moor/issues/105 func TestScrollToEndLongInput(t *testing.T) { const lineCount = 10100 // At least five digits // "X" marks the spot reader := reader.NewFromTextForTesting("test", strings.Repeat(".\n", lineCount-1)+"X") pager := NewPager(reader) pager.ShowLineNumbers = true pager.showLineNumbers = true // Tell our Pager to quit immediately pager.Quit() // Connect the pager with a screen const screenHeight = 10 screen := twin.NewFakeScreen(20, screenHeight) pager.StartPaging(screen, nil, nil) // This is what we're really testing pager.scrollToEnd() // This makes sure at least one frame gets rendered pager.redraw("") // The last screen line holds the status field, and the next to last screen // line holds the last contents line. lastContentsLine := screen.GetRow(screenHeight - 2) firstContentsColumn := len("10_100 ") assertRunesEqual(t, twin.NewStyledRune('X', twin.StyleDefault), lastContentsLine[firstContentsColumn]) } func TestIsScrolledToEnd_LongFile(t *testing.T) { // Six lines of contents reader := reader.NewFromTextForTesting("Testing", "a\nb\nc\nd\ne\nf\n") // Three lines screen screen := twin.NewFakeScreen(20, 3) // Create the pager pager := NewPager(reader) pager.screen = screen assert.Equal(t, false, pager.isScrolledToEnd()) pager.scrollToEnd() assert.Equal(t, true, pager.isScrolledToEnd()) } func TestIsScrolledToEnd_ShortFile(t *testing.T) { // Three lines of contents reader := reader.NewFromTextForTesting("Testing", "a\nb\nc") // Six lines screen screen := twin.NewFakeScreen(20, 6) // Create the pager pager := NewPager(reader) pager.screen = screen assert.Equal(t, true, pager.isScrolledToEnd()) pager.scrollToEnd() assert.Equal(t, true, pager.isScrolledToEnd()) } func TestIsScrolledToEnd_ExactFile(t *testing.T) { // Three lines of contents reader := reader.NewFromTextForTesting("Testing", "a\nb\nc") // Three lines screen screen := twin.NewFakeScreen(20, 3) // Create the pager pager := NewPager(reader) pager.screen = screen pager.ShowStatusBar = false assert.Equal(t, true, pager.isScrolledToEnd()) pager.scrollToEnd() assert.Equal(t, true, pager.isScrolledToEnd()) } func TestIsScrolledToEnd_WrappedLastLine(t *testing.T) { // Three lines of contents reader := reader.NewFromTextForTesting("Testing", "a\nb\nc d e f g h i j k l m n") // Three lines screen screen := twin.NewFakeScreen(5, 3) // Create the pager pager := NewPager(reader) pager.screen = screen pager.WrapLongLines = true assert.Equal(t, false, pager.isScrolledToEnd()) pager.scrollToEnd() assert.Equal(t, true, pager.isScrolledToEnd()) pager.mode.onKey(twin.KeyUp) pager.redraw("XXX") assert.Equal(t, false, pager.isScrolledToEnd()) } func TestIsScrolledToEnd_EmptyFile(t *testing.T) { // No contents reader := reader.NewFromTextForTesting("Testing", "") // Three lines screen screen := twin.NewFakeScreen(20, 3) // Create the pager pager := NewPager(reader) pager.screen = screen assert.Equal(t, true, pager.isScrolledToEnd()) pager.scrollToEnd() assert.Equal(t, true, pager.isScrolledToEnd()) } func getTestFiles(t *testing.T) []string { files, err := os.ReadDir(samplesDir) assert.NilError(t, err) var filenames []string for _, file := range files { filenames = append(filenames, path.Join(samplesDir, file.Name())) } return filenames } // Verify that we can page all files in ../sample-files/* without crashing func TestPageSamples(t *testing.T) { for _, fileName := range getTestFiles(t) { t.Run(fileName, func(t *testing.T) { file, err := os.Open(fileName) if err != nil { t.Errorf("Error opening file <%s>: %s", fileName, err.Error()) return } defer func() { if err := file.Close(); err != nil { panic(err) } }() myReader, err := reader.NewFromStream(fileName, file, nil, reader.ReaderOptions{Style: &chroma.Style{}}) assert.NilError(t, err) assert.NilError(t, myReader.Wait()) pager := NewPager(myReader) pager.WrapLongLines = false pager.ShowLineNumbers = false pager.showLineNumbers = false // Heigh 3 = two lines of contents + one footer screen := twin.NewFakeScreen(10, 3) // Exit immediately pager.Quit() // Get contents onto our fake screen pager.StartPaging(screen, nil, nil) pager.redraw("") firstReaderLine := myReader.GetLine(linemetadata.Index{}) if firstReaderLine == nil { return } firstPagerLine := rowToString(screen.GetRow(0)) // Handle the case when first line is chopped off to the right firstPagerLine = strings.TrimSuffix(firstPagerLine, ">") assert.Assert(t, strings.HasPrefix(firstReaderLine.Plain(), firstPagerLine), "\nreader line = <%s>\npager line = <%s>", firstReaderLine.Plain(), firstPagerLine, ) }) } } // Validate rendering of https://en.wikipedia.org/wiki/ANSI_escape_code#EL func TestClearToEndOfLine_ClearFromStart(t *testing.T) { screen := startPaging(t, reader.NewFromTextForTesting("TestClearToEol", blueBackgroundClearToEol)) screenWidth, _ := screen.Size() var expected []twin.StyledRune for len(expected) < screenWidth { expected = append(expected, twin.NewStyledRune(' ', twin.StyleDefault.WithBackground(twin.NewColor16(4))), ) } actual := screen.GetRow(0) assert.DeepEqual(t, actual, expected, cmp.AllowUnexported(twin.Style{})) } // Validate rendering of https://en.wikipedia.org/wiki/ANSI_escape_code#EL func TestClearToEndOfLine_ClearFromNotStart(t *testing.T) { screen := startPaging(t, reader.NewFromTextForTesting("TestClearToEol", "a"+blueBackgroundClearToEol)) screenWidth, _ := screen.Size() expected := []twin.StyledRune{ twin.NewStyledRune('a', twin.StyleDefault), } for len(expected) < screenWidth { expected = append(expected, twin.NewStyledRune(' ', twin.StyleDefault.WithBackground(twin.NewColor16(4))), ) } actual := screen.GetRow(0) assert.DeepEqual(t, actual, expected, cmp.AllowUnexported(twin.Style{})) } // Validate rendering of https://en.wikipedia.org/wiki/ANSI_escape_code#EL func TestClearToEndOfLine_ClearFromStartScrolledRight(t *testing.T) { pager := NewPager(reader.NewFromTextForTesting("TestClearToEol", blueBackgroundClearToEol0)) pager.ShowLineNumbers = false pager.showLineNumbers = false // Tell our Pager to quit immediately pager.Quit() // Except for just quitting, this also associates a FakeScreen with the Pager screen := twin.NewFakeScreen(3, 10) pager.StartPaging(screen, nil, nil) // Scroll right, this is what we're testing pager.leftColumnZeroBased = 44 // This makes sure at least one frame gets rendered pager.redraw("") screenWidth, _ := screen.Size() var expected []twin.StyledRune for len(expected) < screenWidth { expected = append(expected, twin.NewStyledRune(' ', twin.StyleDefault.WithBackground(twin.NewColor16(4))), ) } actual := screen.GetRow(0) assert.DeepEqual(t, actual, expected, cmp.AllowUnexported(twin.Style{})) } // Render a line of text on our 20 cell wide screen func renderTextLine(text string) string { reader := reader.NewFromTextForTesting("renderTextLine", text) screen := startPaging(nil, reader) return rowToString(screen.GetRow(0)) } // Ref: https://github.com/walles/moor/issues/243 func TestPageWideChars(t *testing.T) { // Both of these characters are 2 cells wide on a terminal const monospaced4cells = "上åˆ" const monospaced8cells = monospaced4cells + monospaced4cells const monospaced16cells = monospaced8cells + monospaced8cells const monospaced20cells = monospaced16cells + monospaced4cells const monospaced24cells = monospaced16cells + monospaced8cells // Cut the line in the middle of a wide character const monospaced18cells = monospaced16cells + "上" assert.Equal(t, monospaced18cells+" >", renderTextLine(monospaced24cells)) // Just the right length, no cutting assert.Equal(t, monospaced20cells, renderTextLine(monospaced20cells)) // Cut this line after a whide character assert.Equal(t, "x"+monospaced18cells+">", renderTextLine("x"+monospaced24cells)) } func TestTerminalFg(t *testing.T) { reader := reader.NewFromTextForTesting("", "x") var styleAnswer = twin.NewStyledRune('x', twin.StyleDefault.WithForeground(twin.NewColor24Bit(0xd0, 0xd0, 0xd0))) var terminalAnswer = twin.NewStyledRune('x', twin.StyleDefault) assertRunesEqual(t, styleAnswer, startPagingWithTerminalFg(t, reader, false).GetRow(0)[0]) assertRunesEqual(t, terminalAnswer, startPagingWithTerminalFg(t, reader, true).GetRow(0)[0]) } func testFooter(t *testing.T, filename string, contents string, expectedFooter string) { reader := reader.NewFromTextForTesting(filename, contents) screen := startPagingWithScreen(t, twin.NewFakeScreen(999, 10), reader) footer := rowToString(screen.GetRow(9)) assert.Equal(t, expectedFooter, footer, fmt.Sprintf("filename='%s', contents='%s'", filename, contents)) } func TestFooter(t *testing.T) { help := "Press ESC / q to exit, / to search, & to filter, h for help" testFooter(t, "filename", "", "filename: "+help) testFooter(t, "", "", " "+help) testFooter(t, "", "text", "1 line 100% "+help) testFooter(t, "filename", "text", "filename: 1 line 100% "+help) testFooter(t, "", "line 1\nline 2", "2 lines 100% "+help) } // Regression test for crash when following an empty file. // Before the fix, IndexFromLength(0) would return nil, and calling .IsBefore() // on nil would crash. func TestHandleMoreLinesAvailableWithEmptyFile(t *testing.T) { // Create a pager with an empty reader emptyReader := reader.NewFromTextForTesting("empty", "") pager := NewPager(emptyReader) // Simulate --follow mode by setting target to max targetLine := linemetadata.IndexMax() pager.TargetLine = &targetLine // This should not crash when lineCount is 0 pager.handleMoreLinesAvailable() // Verify target line is still set (we're still waiting for lines) if pager.TargetLine == nil { t.Error("Expected TargetLine to remain set when no lines available") } } moor-2.10.3/internal/pagermode-colon-command.go000066400000000000000000000025321513574474500214240ustar00rootroot00000000000000package internal import ( log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/twin" ) type PagerModeColonCommand struct { pager *Pager } func (m *PagerModeColonCommand) drawFooter(_ string, _ string, _ string) { p := m.pager _, height := p.screen.Size() pos := 0 for _, token := range "Go to [n]ext, [p]revious or first [x] file: " { pos += p.screen.SetCell(pos, height-1, twin.NewStyledRune(token, twin.StyleDefault)) } // Add a cursor p.screen.SetCell(pos, height-1, twin.NewStyledRune(' ', twin.StyleDefault.WithAttr(twin.AttrReverse))) } func (m *PagerModeColonCommand) onKey(key twin.KeyCode) { p := m.pager switch key { case twin.KeyEscape: p.mode = PagerModeViewing{pager: p} default: log.Tracef("Unhandled colon command event %v, treating as a viewing key event", key) p.mode = PagerModeViewing{pager: p} p.mode.onKey(key) } } func (m *PagerModeColonCommand) onRune(char rune) { p := m.pager if char == 'q' { // Back to viewing mode, just like ESC p.mode = PagerModeViewing{pager: p} } if char == 'p' { p.mode = PagerModeViewing{pager: p} p.previousFile() return } if char == 'n' { p.mode = PagerModeViewing{pager: p} p.nextFile() return } if char == 'x' { p.mode = PagerModeViewing{pager: p} p.firstFile() return } log.Debugf("Unhandled colon command rune %q, ignoring it", char) } moor-2.10.3/internal/pagermode-filter.go000066400000000000000000000024521513574474500201640ustar00rootroot00000000000000package internal import ( log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/search" "github.com/walles/moor/v2/twin" ) type PagerModeFilter struct { pager *Pager inputBox *InputBox } func NewPagerModeFilter(p *Pager) *PagerModeFilter { m := &PagerModeFilter{ pager: p, } m.inputBox = &InputBox{ accept: INPUTBOX_ACCEPT_ALL, onTextChanged: func(text string) { m.updateFilterPattern(text) }, } return m } func (m PagerModeFilter) drawFooter(_ string, _ string, _ string) { m.inputBox.draw(m.pager.screen, "Type to filter, 'ENTER' submits, 'ESC' cancels", "Filter: ") } func (m *PagerModeFilter) updateFilterPattern(text string) { m.pager.filter.For(text) m.pager.search.For(text) } func (m *PagerModeFilter) onKey(key twin.KeyCode) { if m.inputBox.handleKey(key) { return } switch key { case twin.KeyEnter: m.pager.mode = PagerModeViewing{pager: m.pager} case twin.KeyEscape: m.pager.mode = PagerModeViewing{pager: m.pager} m.pager.filter = search.Search{} m.pager.search.Clear() case twin.KeyUp, twin.KeyDown, twin.KeyPgUp, twin.KeyPgDown: viewing := PagerModeViewing{pager: m.pager} viewing.onKey(key) default: log.Debugf("Unhandled filter key event %v", key) } } func (m *PagerModeFilter) onRune(char rune) { m.inputBox.handleRune(char) } moor-2.10.3/internal/pagermode-go-to-line.go000066400000000000000000000035271513574474500206550ustar00rootroot00000000000000package internal import ( "strconv" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/twin" ) type PagerModeGotoLine struct { pager *Pager inputBox InputBox } func NewPagerModeGotoLine(p *Pager) *PagerModeGotoLine { m := &PagerModeGotoLine{ pager: p, inputBox: InputBox{ accept: INPUTBOX_ACCEPT_POSITIVE_NUMBERS, onTextChanged: nil, }, } return m } func (m *PagerModeGotoLine) drawFooter(_ string, _ string, _ string) { m.inputBox.draw(m.pager.screen, "'ENTER' submits, 'ESC' cancels", "Go to line number: ") } func (m *PagerModeGotoLine) updateLineNumber(text string) { newLineNumber, err := strconv.Atoi(text) if err != nil { log.Debugf("Got non-number goto text '%s'", text) return } if newLineNumber < 1 { log.Debugf("Got non-positive goto line number: %d", newLineNumber) return } targetIndex := linemetadata.IndexFromOneBased(newLineNumber) m.pager.scrollPosition = NewScrollPositionFromIndex( targetIndex, "onGotoLineKey", ) m.pager.setTargetLine(&targetIndex) } func (m *PagerModeGotoLine) onKey(key twin.KeyCode) { if m.inputBox.handleKey(key) { return } switch key { case twin.KeyEnter: m.updateLineNumber(m.inputBox.text) m.pager.mode = PagerModeViewing{pager: m.pager} case twin.KeyEscape: m.pager.mode = PagerModeViewing{pager: m.pager} default: log.Tracef("Unhandled goto key event %v, treating as a viewing key event", key) m.pager.mode = PagerModeViewing{pager: m.pager} m.pager.mode.onKey(key) } } func (m *PagerModeGotoLine) onRune(char rune) { p := m.pager if char == 'q' { p.mode = PagerModeViewing{pager: p} return } if char == 'g' { p.scrollPosition = newScrollPosition("Pager scroll position") p.handleScrolledUp() p.mode = PagerModeViewing{pager: p} return } m.inputBox.handleRune(char) } moor-2.10.3/internal/pagermode-info.go000066400000000000000000000012501513574474500176250ustar00rootroot00000000000000// Show some info to the user. Fall back to viewing on any input. package internal import ( log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/twin" ) type PagerModeInfo struct { Pager *Pager Text string logged bool } func (m *PagerModeInfo) drawFooter(_ string, _ string, _ string) { if !m.logged { log.Infof("Displaying info message to user: %q", m.Text) m.logged = true } m.Pager.setFooter(m.Text, "", "", "") } func (m *PagerModeInfo) onKey(key twin.KeyCode) { m.Pager.mode = PagerModeViewing{m.Pager} m.Pager.mode.onKey(key) } func (m *PagerModeInfo) onRune(char rune) { m.Pager.mode = PagerModeViewing{m.Pager} m.Pager.mode.onRune(char) } moor-2.10.3/internal/pagermode-jump-to-mark.go000066400000000000000000000030531513574474500212200ustar00rootroot00000000000000package internal import ( "sort" "github.com/walles/moor/v2/twin" "golang.org/x/exp/maps" ) type PagerModeJumpToMark struct { pager *Pager } func (m PagerModeJumpToMark) drawFooter(_ string, _ string, _ string) { p := m.pager _, height := p.screen.Size() pos := 0 for _, token := range m.getMarkPrompt() { pos += p.screen.SetCell(pos, height-1, twin.NewStyledRune(token, twin.StyleDefault)) } } func (m PagerModeJumpToMark) getMarkPrompt() string { // Special case having zero, one or multiple marks if len(m.pager.bookmarks) == 0 { return "No marks set, press 'm' to set one!" } if len(m.pager.bookmarks) == 1 { for key := range m.pager.bookmarks { return "Jump to your mark: " + string(key) } } // Multiple marks, list them marks := maps.Keys(m.pager.bookmarks) sort.Slice(marks, func(i, j int) bool { return marks[i] < marks[j] }) prompt := "Jump to one of these marks: " for i, mark := range marks { if i > 0 { prompt += ", " } prompt += string(mark) } return prompt } func (m PagerModeJumpToMark) onKey(key twin.KeyCode) { p := m.pager switch key { case twin.KeyEnter, twin.KeyEscape: // Never mind I p.mode = PagerModeViewing{pager: p} default: // Never mind II p.mode = PagerModeViewing{pager: p} p.mode.onKey(key) } } func (m PagerModeJumpToMark) onRune(char rune) { if len(m.pager.bookmarks) == 0 && char == 'm' { m.pager.mode = PagerModeMark(m) return } destination, ok := m.pager.bookmarks[char] if ok { m.pager.scrollPosition = destination } m.pager.mode = PagerModeViewing(m) } moor-2.10.3/internal/pagermode-mark.go000066400000000000000000000016031513574474500176260ustar00rootroot00000000000000package internal import "github.com/walles/moor/v2/twin" type PagerModeMark struct { pager *Pager } func (m PagerModeMark) drawFooter(_ string, _ string, _ string) { p := m.pager _, height := p.screen.Size() pos := 0 for _, token := range "Press any key to label your mark: " { pos += p.screen.SetCell(pos, height-1, twin.NewStyledRune(token, twin.StyleDefault)) } // Add a cursor p.screen.SetCell(pos, height-1, twin.NewStyledRune(' ', twin.StyleDefault.WithAttr(twin.AttrReverse))) } func (m PagerModeMark) onKey(key twin.KeyCode) { p := m.pager switch key { case twin.KeyEnter, twin.KeyEscape: // Never mind I p.mode = PagerModeViewing{pager: p} default: // Never mind II p.mode = PagerModeViewing{pager: p} p.mode.onKey(key) } } func (m PagerModeMark) onRune(char rune) { m.pager.bookmarks[char] = m.pager.scrollPosition m.pager.mode = PagerModeViewing(m) } moor-2.10.3/internal/pagermode-not-found.go000066400000000000000000000013561513574474500206120ustar00rootroot00000000000000package internal import "github.com/walles/moor/v2/twin" type PagerModeNotFound struct { pager *Pager } func (m PagerModeNotFound) drawFooter(_ string, _ string, _ string) { m.pager.setFooter("Not found: "+m.pager.search.String(), "", "", "") } func (m PagerModeNotFound) onKey(key twin.KeyCode) { m.pager.mode = PagerModeViewing(m) m.pager.mode.onKey(key) } func (m PagerModeNotFound) onRune(char rune) { switch char { // Should match the pagermode-viewing.go next-search-hit bindings case 'n': m.pager.scrollToNextSearchHit() // Should match the pagermode-viewing.go previous-search-hit bindings case 'p', 'N': m.pager.scrollToPreviousSearchHit() default: m.pager.mode = PagerModeViewing(m) m.pager.mode.onRune(char) } } moor-2.10.3/internal/pagermode-not-found_test.go000066400000000000000000000027331513574474500216510ustar00rootroot00000000000000package internal import ( "testing" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) // Repro for not-found part of https://github.com/walles/moor/issues/182 func TestNotFoundFindPrevious(t *testing.T) { reader := reader.NewFromTextForTesting("TestNotFoundFindPrevious", "apa\nbepa\ncepa\ndepa") pager := NewPager(reader) pager.screen = twin.NewFakeScreen(40, 2) assert.NilError(t, reader.Wait()) // Look for a hit on the second line pager.search.For("bepa") // Press 'p' to find the previous hit pager.mode = PagerModeNotFound{pager: pager} pager.mode.onRune('p') // We should now be on the second line saying "bepa" assert.Equal(t, pager.scrollPosition.lineIndex(pager).Index(), 1) assert.Assert(t, pager.isViewing()) } func TestWrapSearchBackwards(t *testing.T) { reader := reader.NewFromTextForTesting("TestNotFoundFindPrevious", "gold\napa\nbepa\ngold") pager := NewPager(reader) pager.screen = twin.NewFakeScreen(40, 3) assert.NilError(t, reader.Wait()) // Looking for this should take us to the last line pager.search.For("gold") // Press 'p' to find the previous hit pager.mode = PagerModeNotFound{pager: pager} pager.mode.onRune('p') // We should now have found gold on the last line. Since the pager is // showing two lines on the screen, this puts the pager line number at 3 // (not 4). assert.Equal(t, pager.scrollPosition.lineIndex(pager).Index(), 2) assert.Assert(t, pager.isViewing()) } moor-2.10.3/internal/pagermode-search.go000066400000000000000000000064221513574474500201450ustar00rootroot00000000000000package internal import ( log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/twin" ) type SearchDirection bool const ( SearchDirectionForward SearchDirection = false SearchDirectionBackward SearchDirection = true ) type PagerModeSearch struct { pager *Pager initialScrollPosition scrollPosition // Pager position before search started direction SearchDirection inputBox *InputBox searchHistoryIndex int userEditedText string } func NewPagerModeSearch(p *Pager, direction SearchDirection, initialScrollPosition scrollPosition) *PagerModeSearch { m := &PagerModeSearch{ pager: p, initialScrollPosition: initialScrollPosition, direction: direction, searchHistoryIndex: len(p.searchHistory.entries), // Past the end } m.inputBox = &InputBox{ accept: INPUTBOX_ACCEPT_ALL, onTextChanged: func(text string) { m.pager.search.For(text) switch m.direction { case SearchDirectionBackward: m.pager.scrollToSearchHitsBackwards() case SearchDirectionForward: m.pager.scrollToSearchHits() } }, } return m } func (m PagerModeSearch) drawFooter(_ string, _ string, _ string) { prompt := "Search: " if m.direction == SearchDirectionBackward { prompt = "Search backwards: " } m.inputBox.draw(m.pager.screen, "Type to search, 'ENTER' submits, 'ESC' cancels, '↑↓' navigate history", prompt) } func (m *PagerModeSearch) moveSearchHistoryIndex(delta int) { if len(m.pager.searchHistory.entries) == 0 { return } m.searchHistoryIndex += delta if m.searchHistoryIndex < 0 { m.searchHistoryIndex = 0 } if m.searchHistoryIndex > len(m.pager.searchHistory.entries) { m.searchHistoryIndex = len(m.pager.searchHistory.entries) // Beyond the end of the history } if m.searchHistoryIndex == len(m.pager.searchHistory.entries) { // Reset to whatever the user typed last m.inputBox.setText(m.userEditedText) } else { // Get the history entry m.inputBox.setText(m.pager.searchHistory.entries[m.searchHistoryIndex]) } } func (m *PagerModeSearch) onKey(key twin.KeyCode) { if m.inputBox.handleKey(key) { m.searchHistoryIndex = len(m.pager.searchHistory.entries) // Reset history index when user types m.userEditedText = m.inputBox.text return } switch key { case twin.KeyEnter: m.pager.searchHistory.addEntry(m.inputBox.text) m.pager.mode = PagerModeViewing{pager: m.pager} m.pager.setTargetLine(nil) // Viewing doesn't need all lines case twin.KeyEscape: m.pager.searchHistory.addEntry(m.inputBox.text) m.pager.mode = PagerModeViewing{pager: m.pager} m.pager.scrollPosition = m.initialScrollPosition m.pager.setTargetLine(nil) // Viewing doesn't need all lines case twin.KeyPgUp, twin.KeyPgDown: m.pager.searchHistory.addEntry(m.inputBox.text) m.pager.mode = PagerModeViewing{pager: m.pager} m.pager.mode.onKey(key) m.pager.setTargetLine(nil) // Viewing doesn't need all lines case twin.KeyUp: m.moveSearchHistoryIndex(-1) case twin.KeyDown: m.moveSearchHistoryIndex(1) default: log.Debugf("Unhandled search key event %v", key) } } func (m *PagerModeSearch) onRune(char rune) { m.searchHistoryIndex = len(m.pager.searchHistory.entries) // Reset history index when user types m.inputBox.handleRune(char) m.userEditedText = m.inputBox.text } moor-2.10.3/internal/pagermode-viewing.go000066400000000000000000000143151513574474500203500ustar00rootroot00000000000000package internal import ( "fmt" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/search" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/twin" ) type PagerModeViewing struct { pager *Pager } func (m PagerModeViewing) drawFooter(filenameText string, statusText string, spinner string) { prefix := "" colonHelp := "" m.pager.readerLock.Lock() if len(m.pager.readers) > 1 { prefix = fmt.Sprintf("[%d/%d] ", m.pager.currentReader+1, len(m.pager.readers)) colonHelp = "':' to switch, " } m.pager.readerLock.Unlock() searchHelp := "'/' to search" if !m.pager.search.Inactive() { searchHelp = "'n'/'p' to search next/previous" } helpText := "Press 'ESC' / 'q' to exit, " + colonHelp + searchHelp + ", '&' to filter, 'h' for help" if m.pager.isShowingHelp { helpText = "Press 'ESC' / 'q' to exit help, " + searchHelp prefix = "" } if m.pager.ShowStatusBar { if len(spinner) > 0 { spinner = " " + spinner } m.pager.setFooter(prefix, filenameText, statusText+spinner, helpText) } } func (m PagerModeViewing) onKey(keyCode twin.KeyCode) { p := m.pager switch keyCode { case twin.KeyEscape: p.Quit() case twin.KeyUp: // Clipping is done in _Redraw() p.scrollPosition = p.scrollPosition.PreviousLine(1) p.handleScrolledUp() case twin.KeyDown, twin.KeyEnter: // Clipping is done in _Redraw() p.scrollPosition = p.scrollPosition.NextLine(1) p.handleScrolledDown() case twin.KeyRight: p.moveRight(p.SideScrollAmount) case twin.KeyLeft: p.moveRight(-p.SideScrollAmount) case twin.KeyAltRight: p.moveRight(1) case twin.KeyAltLeft: p.moveRight(-1) case twin.KeyHome: p.scrollPosition = newScrollPosition("Pager scroll position") p.handleScrolledUp() case twin.KeyEnd: p.scrollToEnd() case twin.KeyPgUp: p.scrollPosition = p.scrollPosition.PreviousLine(p.visibleHeight()) p.handleScrolledUp() case twin.KeyPgDown: p.scrollPosition = p.scrollPosition.NextLine(p.visibleHeight()) p.handleScrolledDown() default: log.Debugf("Unhandled key event %v", keyCode) } } func (m PagerModeViewing) onRune(char rune) { p := m.pager switch char { case 'q': p.Quit() case 'v': handleEditingRequest(p) case 'h': if p.isShowingHelp { break } p.preHelpState = &_PreHelpState{ scrollPosition: p.scrollPosition, leftColumnZeroBased: p.leftColumnZeroBased, targetLine: p.TargetLine, } p.scrollPosition = newScrollPosition("Pager scroll position") p.leftColumnZeroBased = 0 p.setTargetLine(nil) p.isShowingHelp = true case '=': p.ShowStatusBar = !p.ShowStatusBar // '\x10' = CTRL-p, should scroll up one line. // Ref: https://github.com/walles/moor/issues/107#issuecomment-1328354080 case 'k', 'y', '\x10': // Clipping is done in _Redraw() p.scrollPosition = p.scrollPosition.PreviousLine(1) p.handleScrolledUp() // '\x0e' = CTRL-n, should scroll down one line. // Ref: https://github.com/walles/moor/issues/107#issuecomment-1328354080 case 'j', 'e', '\x0e': // Clipping is done in _Redraw() p.scrollPosition = p.scrollPosition.NextLine(1) p.handleScrolledDown() case '<': p.scrollPosition = newScrollPosition("Pager scroll position") p.handleScrolledUp() case '>', 'G': p.scrollToEnd() case 'f', ' ': p.scrollPosition = p.scrollPosition.NextLine(p.visibleHeight()) p.handleScrolledDown() case 'b': p.scrollPosition = p.scrollPosition.PreviousLine(p.visibleHeight()) p.handleScrolledUp() // '\x15' = CTRL-u, should work like just 'u'. // Ref: https://github.com/walles/moor/issues/90 case 'u', '\x15': p.scrollPosition = p.scrollPosition.PreviousLine(p.visibleHeight() / 2) p.handleScrolledUp() // '\x04' = CTRL-d, should work like just 'd'. // Ref: https://github.com/walles/moor/issues/90 case 'd', '\x04': p.scrollPosition = p.scrollPosition.NextLine(p.visibleHeight() / 2) p.handleScrolledDown() case '/': p.mode = NewPagerModeSearch(p, SearchDirectionForward, p.scrollPosition) p.search.Clear() // Searchers want to scan the whole file, start reading as much as we can reallyHigh := linemetadata.IndexMax() p.setTargetLine(&reallyHigh) case '?': p.mode = NewPagerModeSearch(p, SearchDirectionBackward, p.scrollPosition) p.search.Clear() // Searchers want to scan the whole file, start reading as much as we can reallyHigh := linemetadata.IndexMax() p.setTargetLine(&reallyHigh) case '&': if !p.isShowingHelp { // Filtering the help text is not supported. Feel free to work on // that if you feel that's time well spent. p.mode = NewPagerModeFilter(p) p.search.Clear() p.filter = search.Search{} } case 'g': p.mode = NewPagerModeGotoLine(p) p.setTargetLine(nil) case ':': if len(p.readers) > 1 { p.mode = &PagerModeColonCommand{pager: p} p.setTargetLine(nil) } else { p.mode = &PagerModeInfo{Pager: p, Text: "Pass more files on the command line to be able to switch between them."} } // Should match the pagermode-not-found.go previous-search-hit bindings case 'n': p.scrollToNextSearchHit() // Should match the pagermode-not-found.go next-search-hit bindings case 'p', 'N': p.scrollToPreviousSearchHit() case 'm': p.mode = PagerModeMark{pager: p} p.setTargetLine(nil) case '\'': p.mode = PagerModeJumpToMark{pager: p} p.setTargetLine(nil) case 'w': p.WrapLongLines = !p.WrapLongLines if p.WrapLongLines { p.mode = &PagerModeInfo{Pager: p, Text: "Word wrapping enabled"} } else { p.mode = &PagerModeInfo{Pager: p, Text: "Word wrapping disabled"} } case '\x14': // CTRL-t p.cycleTabSize() case '\x01': // CTRL-a p.leftColumnZeroBased = 0 if !p.showLineNumbers { // Line numbers not visible, turn them on if the user wants them. p.showLineNumbers = p.ShowLineNumbers } default: log.Debugf("Unhandled rune keypress '%s'/0x%08x", string(char), int32(char)) } } func (p *Pager) cycleTabSize() { switch p.TabSize { case 8: p.TabSize = 4 default: // We really want to toggle betwewen 4 and 8, but if we start out // somewhere else let's just go for 8. That's less' default tab size. p.TabSize = 8 } textstyles.TabSize = p.TabSize p.mode = &PagerModeInfo{Pager: p, Text: fmt.Sprintf("Tab size set to %d", p.TabSize)} } moor-2.10.3/internal/pagermode-viewing_test.go000066400000000000000000000026321513574474500214060ustar00rootroot00000000000000package internal import ( "os" "testing" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) func TestErrUnlessExecutable_yes(t *testing.T) { // Find our own executable executable, err := os.Executable() if err != nil { t.Fatal(err) } // Check that it's executable err = errUnlessExecutable(executable) if err != nil { t.Fatal(err) } } func TestErrUnlessExecutable_no(t *testing.T) { textFile := "pagermode-viewing_test.go" if _, err := os.Stat(textFile); os.IsNotExist(err) { t.Fatal("Test setup failed, text file not found: " + textFile) } err := errUnlessExecutable(textFile) if err == nil { t.Fatal("Expected error, got nil") } } func TestViewingFooter_WithSpinner(t *testing.T) { r := reader.NewFromTextForTesting("", "text") pager := NewPager(r) pager.ShowStatusBar = true // Attach a fake screen large enough to render the footer screen := twin.NewFakeScreen(80, 10) pager.screen = screen // Drive the footer rendering directly via PagerModeViewing mode := PagerModeViewing{pager: pager} spinner := "<->" mode.drawFooter("", "1 line 100%", spinner) footer := rowToString(screen.GetRow(9)) // Quotes are stripped in rendering; expect plain keys expectedHelp := "Press ESC / q to exit, / to search, & to filter, h for help" expected := "1 line 100% " + spinner + " " + expectedHelp assert.Equal(t, expected, footer) } moor-2.10.3/internal/panicHandler.go000066400000000000000000000006131513574474500173230ustar00rootroot00000000000000package internal // NOTE: This file should be identical to twin/panicHandler.go import ( log "github.com/sirupsen/logrus" ) func PanicHandler(goroutineName string, recoverResult any, stackTrace []byte) { if recoverResult == nil { return } log.WithFields(log.Fields{ "panic": recoverResult, "stackTrace": string(stackTrace), }).Error("Goroutine panicked: " + goroutineName) } moor-2.10.3/internal/reader/000077500000000000000000000000001513574474500156465ustar00rootroot00000000000000moor-2.10.3/internal/reader/highlight.go000066400000000000000000000031711513574474500201460ustar00rootroot00000000000000package reader import ( "bytes" "strings" "github.com/alecthomas/chroma/v2" ) // Read and highlight some text using Chroma: // https://github.com/alecthomas/chroma // // If lexer is nil no highlighting will be performed. // // Returns nil with no error if highlighting would be a no-op. func Highlight(text string, style chroma.Style, formatter chroma.Formatter, lexer chroma.Lexer) (*string, error) { if lexer == nil { // No highlighter available for this file type return nil, nil } // FIXME: Can we test for the lexer implementation class instead? That // should be more resilient towards this arbitrary string changing if we // upgrade Chroma at some point. if lexer.Config().Name == "plaintext" { // This highlighter doesn't provide any highlighting, but not doing // anything at all is cheaper and simpler, so we do that. return nil, nil } // NOTE: We used to do... // // lexer = chroma.Coalesce(lexer) // // ... here, but with Chroma 2.12.0 that resulted in this problem: // https://github.com/walles/moor/issues/236#issuecomment-2282677792 // // So let's not do that anymore. iterator, err := lexer.Tokenise(nil, text) if err != nil { return nil, err } var stringBuffer bytes.Buffer err = formatter.Format(&stringBuffer, &style, iterator) if err != nil { return nil, err } highlighted := stringBuffer.String() // If buffer ends with SGR Reset ("[0m"), remove it. Chroma sometimes // (always?) puts one of those by itself on the last line, making us believe // there is one line too many. sgrReset := "\x1b[0m" trimmed := strings.TrimSuffix(highlighted, sgrReset) return &trimmed, nil } moor-2.10.3/internal/reader/inspection-reader.go000066400000000000000000000006641513574474500216160ustar00rootroot00000000000000package reader import "io" // Pass-through reader that counts the number of bytes read. type inspectionReader struct { base io.Reader bytesCount int64 endedWithNewline bool } func (r *inspectionReader) Read(p []byte) (n int, err error) { n, err = r.base.Read(p) r.bytesCount += int64(n) if err != nil { return } if n > 0 { r.endedWithNewline = p[n-1] == '\n' } else { r.endedWithNewline = false } return } moor-2.10.3/internal/reader/line.go000066400000000000000000000032551513574474500171310ustar00rootroot00000000000000package reader import ( "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/search" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/twin" ) // Returns a representation of the string split into styled tokens. Any regexp // matches are highlighted. A nil regexp means no highlighting. // // maxTokensCount: at most this many tokens will be included in the result. If // 0, do all runes. For BenchmarkRenderHugeLine() performance. func (line *Line) HighlightedTokens( plainTextStyle twin.Style, searchHitStyle twin.Style, search search.Search, lineIndex linemetadata.Index, maxTokensCount int, ) textstyles.StyledRunesWithTrailer { matchRanges := search.GetMatchRanges(line.Plain(lineIndex)) fromString := textstyles.StyledRunesFromString(plainTextStyle, string(line.raw), &lineIndex, maxTokensCount) returnRunes := make([]textstyles.CellWithMetadata, 0, len(fromString.StyledRunes)) lastWasSearchHit := false for _, token := range fromString.StyledRunes { style := token.Style searchHit := matchRanges.InRange(len(returnRunes)) if searchHit { // Highlight the search hit style = searchHitStyle } returnRunes = append(returnRunes, textstyles.CellWithMetadata{ Rune: token.Rune, Style: style, IsSearchHit: searchHit, StartsSearchHit: searchHit && !lastWasSearchHit, }) lastWasSearchHit = searchHit } return textstyles.StyledRunesWithTrailer{ StyledRunes: returnRunes, Trailer: fromString.Trailer, ContainsSearchHit: !matchRanges.Empty(), } } func (line *Line) HasManPageFormatting() bool { return textstyles.HasManPageFormatting(string(line.raw)) } moor-2.10.3/internal/reader/linePool.go000066400000000000000000000006771513574474500177700ustar00rootroot00000000000000package reader // This value affects BenchmarkReadLargeFile() performance. Validate changes // like this: // // go test -benchmem -run='^$' -bench 'BenchmarkReadLargeFile' ./internal/reader const linePoolSize = 1000 type linePool struct { pool []Line } func (lp *linePool) create(raw []byte) *Line { if len(lp.pool) == 0 { lp.pool = make([]Line, linePoolSize) } line := &lp.pool[0] lp.pool = lp.pool[1:] line.raw = raw return line } moor-2.10.3/internal/reader/line_test.go000066400000000000000000000053351513574474500201710ustar00rootroot00000000000000package reader import ( "testing" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/search" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) func TestHighlightedTokensWithManPageHeading(t *testing.T) { // Set a marker style we can recognize and test for textstyles.ManPageHeading = twin.StyleDefault.WithForeground(twin.NewColor16(2)) headingText := "JOHAN" manPageHeading := "" for _, char := range headingText { manPageHeading += string(char) + "\b" + string(char) } highlighted := textstyles.StyledRunesFromString(twin.StyleDefault, manPageHeading, nil, 0).StyledRunes assert.Equal(t, len(highlighted), len(headingText)) for i, cell := range highlighted { assert.Equal(t, cell.Rune, rune(headingText[i])) assert.Equal(t, cell.Style, textstyles.ManPageHeading) } } // Verify that a multi-rune search hit spanning a simulated wrap boundary // propagates the search-hit markers to both sub-lines. // // We don't call the actual wrapLine() here (different package + unexported); // instead we simulate a wrap by slicing at a chosen width. All runes are // single-width here so rune index == screen column. func TestSearchHitSpanningWrapBoundary(t *testing.T) { // Arrange: a line where the search hit crosses index 5 line := NewFromTextForTesting("TestSearchHitSpanningWrapBoundary", "0123456789").GetLine(linemetadata.Index{}).Line searchHitStyle := twin.StyleDefault.WithForeground(twin.NewColor16(3)) // Match runs from indices 3..8 inclusive ("345678") highlighted := line.HighlightedTokens(twin.StyleDefault, searchHitStyle, search.For("345678"), linemetadata.Index{}, 0) // Sanity: overall line reports having a search hit assert.Assert(t, highlighted.ContainsSearchHit, "Expected overall line to contain search hit") wrapWidth := 5 // Split after index 4 if len(highlighted.StyledRunes) <= wrapWidth+1 { t.Fatalf("Unexpected rune count %d, need > %d", len(highlighted.StyledRunes), wrapWidth+1) } first := textstyles.CellWithMetadataSlice(highlighted.StyledRunes[:wrapWidth]) second := textstyles.CellWithMetadataSlice(highlighted.StyledRunes[wrapWidth:]) // Assert: both wrapped parts contain search hit cells (continuation preserved) assert.Assert(t, first.ContainsSearchHit(), "First part should contain start of search hit") assert.Assert(t, second.ContainsSearchHit(), "Second part should contain continuation of search hit spanning wrap") // Additionally ensure styling applied to all hit cells (foreground color matches) for _, cell := range append(first, second...) { if cell.IsSearchHit && !cell.Style.Equal(searchHitStyle) { t.Fatalf("Search hit cell %#v does not have expected searchHitStyle", cell) } } } moor-2.10.3/internal/reader/numberedLine.go000066400000000000000000000017201513574474500206060ustar00rootroot00000000000000package reader import ( "github.com/rivo/uniseg" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/search" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/twin" ) type NumberedLine struct { Index linemetadata.Index Number linemetadata.Number Line *Line } func (nl *NumberedLine) Plain() string { return nl.Line.Plain(nl.Index) } // maxTokensCount: at most this many tokens will be included in the result. If // 0, do all runes. For BenchmarkRenderHugeLine() performance. func (nl *NumberedLine) HighlightedTokens(plainTextStyle twin.Style, searchHitStyle twin.Style, search search.Search, maxTokensCount int) textstyles.StyledRunesWithTrailer { return nl.Line.HighlightedTokens(plainTextStyle, searchHitStyle, search, nl.Index, maxTokensCount) } func (nl *NumberedLine) DisplayWidth() int { width := 0 for _, r := range nl.Plain() { width += uniseg.StringWidth(string(r)) } return width } moor-2.10.3/internal/reader/panicHandler.go000066400000000000000000000006111513574474500205630ustar00rootroot00000000000000package reader // NOTE: This file should be identical to twin/panicHandler.go import ( log "github.com/sirupsen/logrus" ) func PanicHandler(goroutineName string, recoverResult any, stackTrace []byte) { if recoverResult == nil { return } log.WithFields(log.Fields{ "panic": recoverResult, "stackTrace": string(stackTrace), }).Error("Goroutine panicked: " + goroutineName) } moor-2.10.3/internal/reader/reader.go000066400000000000000000000740521513574474500174470ustar00rootroot00000000000000package reader import ( "bufio" "bytes" "encoding/json" "encoding/xml" "fmt" "io" "math" "os" "path/filepath" "runtime/debug" "slices" "strings" "sync" "sync/atomic" "time" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/internal/util" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/lexers" log "github.com/sirupsen/logrus" ) // An 1.7MB file took 2s to highlight. The number for this limit is totally // negotiable. const MAX_HIGHLIGHT_SIZE int64 = 2_000_000 // To cap resource usage when not needed, start by reading this many lines into // memory. If the user scrolls near the end or starts searching, we'll read // more. // // Ref: https://github.com/walles/moor/issues/296 const DEFAULT_PAUSE_AFTER_LINES = 50_000 var DisablePlainCachingForBenchmarking = false type ReaderOptions struct { // Format JSON input ShouldFormat bool // Pause after reading this many lines, unless told otherwise. // Tune at runtime using SetPauseAfterLines(). // // nil means 20k lines. PauseAfterLines *int // If this is nil, you must call reader.SetStyleForHighlighting() later if // you want highlighting. Style *chroma.Style // If this is set, it will be used as the lexer for highlighting Lexer chroma.Lexer } type Reader interface { GetLineCount() int GetLine(index linemetadata.Index) *NumberedLine // This method will try to honor wantedLineCount over firstLine. This means // that the returned first line may be different from the requested one. GetLines(firstLine linemetadata.Index, wantedLineCount int) InputLines // GetLines gets the indicated lines from the input. The lines will be stored // in the provided preallocated slice to avoid allocations. The line count is // determined by the capacity of the provided slice. // // The return value is the status text for the returned lines. GetLinesPreallocated(firstLine linemetadata.Index, resultLines *[]NumberedLine) (string, string) // False when paused. Showing the paused line count is confusing, because // the user might think that the number is the total line count, even though // we are not done yet. // // When we're not paused, the number will be constantly changing, indicating // that the counting is not done yet. ShouldShowLineCount() bool } type Line struct { raw []byte plainTextCache atomic.Pointer[string] // Use line.Plain() to access this field } // ReaderImpl reads a file into an array of strings. // // It does the reading in the background, and it returns parts of the read data // upon request. // // This package provides query methods for the struct, no peeking!! type ReaderImpl struct { sync.RWMutex lines []*Line // Display name for the buffer. If not set, no buffer name will be shown. // // For files, this will be the basename of the file. For our help text, this // will be "Help". For streams this will generally not be set, but may come // from the $PAGER_LABEL environment variable. DisplayName *string // If this is set, it will point out the file we are reading from. If this // is not set, we are not reading from a file. FileName *string // How many bytes have we read so far? bytesCount int64 endsWithNewline bool Err error // Stream has been completely read. May not be highlighted yet. ReadingDone *atomic.Bool // Highlighting has been completed. HighlightingDone *atomic.Bool highlightingStyle chan chroma.Style // This channel expects to be read exactly once. All other uses will lead to // undefined behavior. doneWaitingForFirstByte chan bool // For telling the UI it should recheck the --quit-if-one-screen conditions. // Signalled when either highlighting is done or reading is done. MaybeDone chan bool MoreLinesAdded chan bool // Because we don't want to consume infinitely. // // Ref: https://github.com/walles/moor/issues/296 pauseAfterLines int pauseAfterLinesUpdated chan bool // PauseStatus is true if the reader is paused, false if it is not PauseStatus *atomic.Bool } // InputLines contains a number of lines from the reader, plus metadata type InputLines struct { Lines []NumberedLine // "monkey.txt: 1-23/45 51%" FilenameText string StatusText string } // This is the reader's main function. It will be run in a goroutine. First it // reads the stream until the end, then starts tailing. func (reader *ReaderImpl) readStream(stream io.Reader, formatter chroma.Formatter, options ReaderOptions) { reader.consumeLinesFromStream(stream) reader.ReadingDone.Store(true) select { case reader.MaybeDone <- true: default: } t0 := time.Now() style := <-reader.highlightingStyle options.Style = &style highlightFromMemory(reader, formatter, options) log.Debug("highlightFromMemory() took ", time.Since(t0)) reader.HighlightingDone.Store(true) select { case reader.MaybeDone <- true: default: } // Tail the file if the stream is coming from a file. // Ref: https://github.com/walles/moor/issues/224 err := reader.tailFile() if err != nil { log.Warn("Failed to tail file: ", err) } } // Pause if we should pause, otherwise not. Pausing means waiting for // pauseAfterLinesUpdated to be signalled in SetPauseAfterLines(). func (reader *ReaderImpl) assumeLockAndMaybePause() { for { shouldPause := len(reader.lines) >= reader.pauseAfterLines if !shouldPause { // Not there yet, no pause return } // Release lock while pausing reader.Unlock() reader.setPauseStatus(true) <-reader.pauseAfterLinesUpdated reader.setPauseStatus(false) reader.Lock() } } // Assume write lock held. Add a new line. If this function paused, it will // return the pause duration. func (reader *ReaderImpl) assumeLockAndAddLine(line []byte, considerAppending bool, linePool *linePool) time.Duration { // Line end if len(line) > 0 && line[len(line)-1] == '\r' { line = line[:len(line)-1] // Handle MSDOS line endings } if len(reader.lines) == 0 { // Can't append if there are no previous lines considerAppending = false } if !considerAppending { newLine := linePool.create(line) reader.lines = append(reader.lines, newLine) // New line added, time for a break? t0 := time.Now() reader.assumeLockAndMaybePause() pauseDuration := time.Since(t0) return pauseDuration } // Special case, append to the previous line baseLine := reader.lines[len(reader.lines)-1] // Build the complete line completeLine := make([]byte, len(baseLine.raw)+len(line)) copy(completeLine, baseLine.raw) copy(completeLine[len(baseLine.raw):], line) baseLine.raw = completeLine baseLine.plainTextCache.Store(nil) // Invalidate cache return 0 } // This function will update the Reader struct. It is expected to run in a // goroutine. // // It is used both during the initial read of the stream until it ends, and // while tailing files for changes. func (reader *ReaderImpl) consumeLinesFromStream(stream io.Reader) { // This value affects BenchmarkReadLargeFile() performance. Validate changes // like this: // // go test -benchmem -run='^$' -bench 'BenchmarkReadLargeFile' ./internal/reader const byteBufferSize = 16 * 1024 t0 := time.Now() // Preallocating the line pool and the lines slice improves large file // reading performance by 10%. linePool := linePool{} if reader.FileName != nil && reader.GetLineCount() == 0 { lineCount, err := countLines(*reader.FileName) if err != nil { log.Warn("Failed to count lines in file: ", err) } else { // We have a line count... reader.Lock() if len(reader.lines) == 0 { // ... and still no lines have been read, so preallocate both // the lines slice... reader.lines = make([]*Line, 0, lineCount) // ... and the line pool. linePool.pool = make([]Line, lineCount) } reader.Unlock() } } inspectionReader := inspectionReader{base: stream} awaitingFirstByte := true for { byteBuffer := make([]byte, byteBufferSize) readBytes, err := inspectionReader.Read(byteBuffer) if awaitingFirstByte && readBytes > 0 { // We got our first byte! select { case reader.doneWaitingForFirstByte <- true: default: } awaitingFirstByte = false } // Error or not, handle the bytes that we got reader.Lock() lineStart := 0 byteIndex := 0 for readBytes > 0 { relativeNewlineLocation := bytes.IndexByte(byteBuffer[byteIndex:readBytes], '\n') if relativeNewlineLocation == -1 { // No more newlines in this buffer break } byteIndex += relativeNewlineLocation considerAppending := lineStart == 0 && !reader.endsWithNewline pauseDuration := reader.assumeLockAndAddLine(byteBuffer[lineStart:byteIndex], considerAppending, &linePool) t0 = t0.Add(pauseDuration) lineStart = byteIndex + 1 byteIndex = lineStart } // Handle any remaining bytes as a partial line if lineStart < readBytes { considerAppending := lineStart == 0 && !reader.endsWithNewline pauseDuration := reader.assumeLockAndAddLine(byteBuffer[lineStart:readBytes], considerAppending, &linePool) t0 = t0.Add(pauseDuration) } reader.endsWithNewline = inspectionReader.endedWithNewline reader.Unlock() // This is how to do a non-blocking write to a channel: // https://gobyexample.com/non-blocking-channel-operations select { case reader.MoreLinesAdded <- true: default: // Default case required for the write to be non-blocking } if err == io.EOF { // Done! break } if err != nil { reader.Lock() if reader.Err == nil { // Store the error unless it overwrites one we already have reader.Err = fmt.Errorf("error reading from input stream: %w", err) } reader.Unlock() break } } if reader.FileName != nil { reader.Lock() reader.bytesCount += inspectionReader.bytesCount reader.Unlock() } if awaitingFirstByte { // If the stream was empty we never got any first byte. Make sure people // stop waiting in this case. Async write since it might already have been // written to. select { case reader.doneWaitingForFirstByte <- true: default: } } log.Info("Stream read in ", time.Since(t0), ", have ", reader.GetLineCount(), " lines") } func (reader *ReaderImpl) tailFile() error { reader.RLock() fileName := reader.FileName reader.RUnlock() if fileName == nil { return nil } log.Debugf("Tailing file %s", *fileName) for { // NOTE: We could use something like // https://github.com/fsnotify/fsnotify instead of sleeping and polling // here. time.Sleep(1 * time.Second) fileStats, err := os.Stat(*fileName) if err != nil { log.Debugf("Failed to stat file %s while tailing, giving up: %s", *fileName, err.Error()) return nil } reader.RLock() bytesCount := reader.bytesCount reader.RUnlock() if bytesCount == -1 { log.Debugf("Bytes count unknown for %s, stop tailing", *fileName) return nil } if fileStats.Size() == bytesCount { log.Tracef("File %s unchanged at %d bytes, continue tailing", *fileName, fileStats.Size()) continue } if fileStats.Size() < bytesCount { log.Debugf("File %s shrunk from %d to %d bytes, stop tailing", *fileName, bytesCount, fileStats.Size()) return nil } // File grew, read the new lines stream, _, err := ZOpen(*fileName) if err != nil { log.Debugf("Failed to open file %s for re-reading while tailing: %s", *fileName, err.Error()) return nil } seekable, ok := stream.(io.ReadSeekCloser) if !ok { err = stream.Close() if err != nil { log.Debugf("Giving up on tailing, failed to close non-seekable stream from %s: %s", *fileName, err.Error()) return nil } log.Debugf("Giving up on tailing, file %s is not seekable", *fileName) return nil } _, err = seekable.Seek(bytesCount, io.SeekStart) if err != nil { log.Debugf("Failed to seek in file %s while tailing: %s", *fileName, err.Error()) return nil } log.Tracef("File %s up from %d bytes to %d bytes, reading more lines...", *fileName, bytesCount, fileStats.Size()) reader.consumeLinesFromStream(seekable) err = seekable.Close() if err != nil { // This can lead to file handle leaks return fmt.Errorf("failed to close file %s after tailing: %w", *fileName, err) } } } // NewFromStream creates a new stream reader // // The display name can be an empty string (""). // // If non-empty, the name will be displayed by the pager in the bottom left // corner to help the user keep track of what is being paged. // // Note that you must call reader.SetStyleForHighlighting() after this to get // highlighting. func NewFromStream(displayName string, reader io.Reader, formatter chroma.Formatter, options ReaderOptions) (*ReaderImpl, error) { zReader, err := ZReader(reader) if err != nil { return nil, err } mReader := newReaderFromStream(zReader, nil, formatter, options) if len(displayName) > 0 { mReader.Lock() mReader.DisplayName = &displayName mReader.Unlock() } if options.Style != nil { mReader.SetStyleForHighlighting(*options.Style) } return mReader, nil } // newReaderFromStream creates a new stream reader // // originalFileName is used for counting the lines in the file. nil for // don't-know (streams) or not countable (compressed files). The line count is // then used for pre-allocating the lines slice, which improves large file // loading performance. // // If lexer is set, the file will be highlighted after being fully read. // // Whatever data we get from the reader, that's what we'll have. Or in other // words, if the input needs to be decompressed, do that before coming here. // // Note that you must call reader.SetStyleForHighlighting() after this to get // highlighting. func newReaderFromStream(reader io.Reader, originalFileName *string, formatter chroma.Formatter, options ReaderOptions) *ReaderImpl { readingDone := atomic.Bool{} readingDone.Store(false) highlightingDone := atomic.Bool{} highlightingDone.Store(false) pauseStatus := atomic.Bool{} pauseStatus.Store(false) pauseAfterLines := DEFAULT_PAUSE_AFTER_LINES if options.PauseAfterLines != nil { pauseAfterLines = *options.PauseAfterLines } var displayFileName *string if originalFileName != nil { basename := filepath.Base(*originalFileName) displayFileName = &basename } returnMe := ReaderImpl{ FileName: originalFileName, DisplayName: displayFileName, pauseAfterLines: pauseAfterLines, pauseAfterLinesUpdated: make(chan bool, 1), PauseStatus: &pauseStatus, MoreLinesAdded: make(chan bool, 1), MaybeDone: make(chan bool, 2), highlightingStyle: make(chan chroma.Style, 1), doneWaitingForFirstByte: make(chan bool, 1), HighlightingDone: &highlightingDone, ReadingDone: &readingDone, } go func() { defer func() { PanicHandler("newReaderFromStream()/readStream()", recover(), debug.Stack()) }() returnMe.readStream(reader, formatter, options) }() return &returnMe } // Testing only!! May or may not hang if run in real world scenarios. // // NewFromTextForTesting creates a Reader from a block of text. // // First parameter is the name of this Reader. This name will be displayed by // Moor in the bottom left corner of the screen. // // Calling Wait() on this Reader will always return immediately, no // asynchronous ops will be performed. func NewFromTextForTesting(name string, text string) *ReaderImpl { noExternalNewlines := strings.Trim(text, "\n") lines := []*Line{} if len(noExternalNewlines) > 0 { for _, lineString := range strings.Split(noExternalNewlines, "\n") { line := Line{raw: []byte(lineString)} lines = append(lines, &line) } } readingDone := atomic.Bool{} readingDone.Store(true) highlightingDone := atomic.Bool{} highlightingDone.Store(true) // No highlighting to do = nothing left = Done! returnMe := &ReaderImpl{ lines: lines, ReadingDone: &readingDone, HighlightingDone: &highlightingDone, doneWaitingForFirstByte: make(chan bool, 1), } if name != "" { returnMe.DisplayName = &name } return returnMe } // Duplicate of moor/moor.go:TryOpen func TryOpen(filename string) error { // Try opening the file tryMe, err := os.Open(filename) if err != nil { return err } // Try reading a byte buffer := make([]byte, 1) _, err = tryMe.Read(buffer) if err != nil && err.Error() == "EOF" { // Empty file, this is fine err = nil } closeErr := tryMe.Close() if err == nil && closeErr != nil { // Everything worked up until Close(), report the Close() error return closeErr } return err } // From: https://stackoverflow.com/a/52153000/473672 func countLines(filename string) (uint64, error) { const lineBreak = '\n' sliceWithSingleLineBreak := []byte{lineBreak} reader, _, err := ZOpen(filename) if err != nil { return 0, err } defer func() { err := reader.Close() if err != nil { log.Warn("Error closing file after counting the lines: ", err) } }() var count uint64 t0 := time.Now() buf := make([]byte, bufio.MaxScanTokenSize) lastReadEndsInNewline := true for { bufferSize, err := reader.Read(buf) if err != nil && err != io.EOF { return 0, err } if bufferSize > 0 { lastReadEndsInNewline = (buf[bufferSize-1] == lineBreak) } count += uint64(bytes.Count(buf[:bufferSize], sliceWithSingleLineBreak)) if err == io.EOF { break } } if !lastReadEndsInNewline { // No trailing line feed, this needs special handling count++ } t1 := time.Now() if count == 0 { log.Debug("Counted ", count, " lines in ", t1.Sub(t0)) } else { log.Debug("Counted ", count, " lines in ", t1.Sub(t0), " at ", t1.Sub(t0)/time.Duration(count), "/line") } return count, nil } // NewFromFilename creates a new file reader. // // If options.Lexer is nil it will be determined from the input file name. // // If options.Style is nil, you must call reader.SetStyleForHighlighting() later // to get highlighting. // // The Reader will try to uncompress various compressed file format, and also // apply highlighting to the file using Chroma: // https://github.com/alecthomas/chroma func NewFromFilename(filename string, formatter chroma.Formatter, options ReaderOptions) (*ReaderImpl, error) { fileError := TryOpen(filename) if fileError != nil { return nil, fileError } stream, highlightingFilename, err := ZOpen(filename) if err != nil { return nil, err } if options.Lexer == nil { options.Lexer = lexers.Match(highlightingFilename) } returnMe := newReaderFromStream(stream, &highlightingFilename, formatter, options) if options.Lexer == nil { returnMe.HighlightingDone.Store(true) } if options.Style != nil { returnMe.SetStyleForHighlighting(*options.Style) } return returnMe, nil } // Wait for reader to finish reading and highlighting. Used by tests. func (reader *ReaderImpl) Wait() error { // Wait for our goroutine to finish for !reader.ReadingDone.Load() { if reader.PauseStatus.Load() { // We want more lines reader.SetPauseAfterLines(reader.GetLineCount() * 2) } } //revive:disable-next-line:empty-block for !reader.HighlightingDone.Load() { } reader.RLock() defer reader.RUnlock() return reader.Err } func textAsString(reader *ReaderImpl, shouldFormat bool) string { reader.RLock() text := []byte{} for _, line := range reader.lines { text = append(text, line.raw...) text = append(text, '\n') } reader.RUnlock() var jsonData any err := json.Unmarshal(text, &jsonData) if err != nil { // Not JSON, return the text as-is return string(text) } if !shouldFormat { log.Info("Try the --reformat flag for automatic JSON reformatting") return string(text) } // Pretty print the JSON prettyJSON, err := json.MarshalIndent(jsonData, "", " ") if err != nil { log.Debug("Failed to pretty print JSON: ", err) return string(text) } log.Debug("Got the --reformat flag, reformatted JSON input") return string(prettyJSON) } func isXml(text string) bool { err := xml.Unmarshal([]byte(text), new(any)) return err == nil } // We expect this to be executed in a goroutine func highlightFromMemory(reader *ReaderImpl, formatter chroma.Formatter, options ReaderOptions) { // Is the buffer small enough? var byteCount int64 reader.RLock() for _, line := range reader.lines { byteCount += int64(len(line.raw)) if byteCount > MAX_HIGHLIGHT_SIZE { log.Info("File too large for highlighting: ", byteCount) reader.RUnlock() return } } reader.RUnlock() text := textAsString(reader, options.ShouldFormat) if len(text) == 0 { log.Debug("Buffer is empty, not highlighting") return } if options.Lexer == nil && json.Valid([]byte(text)) { log.Info("Buffer is valid JSON, highlighting as JSON") options.Lexer = lexers.Get("json") } else if options.Lexer == nil && isXml(text) { log.Info("Buffer is valid XML, highlighting as XML") options.Lexer = lexers.Get("xml") } if options.Lexer == nil { log.Debug("No lexer set, not highlighting") return } if options.Style == nil { log.Debug("No style set, not highlighting") return } if formatter == nil { log.Debug("No formatter set, not highlighting") return } highlighted, err := Highlight(text, *options.Style, formatter, options.Lexer) if err != nil { log.Warn("Highlighting failed: ", err) return } if highlighted == nil { // No highlighting would be done, never mind return } reader.setText(*highlighted) } // createStatusUnlocked() assumes that its caller is holding the read lock func (reader *ReaderImpl) createStatusUnlocked(lastLine linemetadata.Index) (string, string) { displayName := "" if reader.DisplayName != nil { displayName = *reader.DisplayName } if len(reader.lines) == 0 { empty := "" if len(displayName) > 0 { return displayName, ": " + empty } return "", empty } linesCount := "" percent := "" if len(reader.lines) == 1 { linesCount = "1 line" percent = "100%" } else { // More than one line linesCount = util.FormatInt(len(reader.lines)) + " lines" percent = fmt.Sprintf("%.0f%%", math.Floor(100*float64(lastLine.Index()+1)/float64(len(reader.lines)))) } if !reader.ShouldShowLineCount() { linesCount = "" } return_me := "" if len(linesCount) > 0 { if len(displayName) > 0 { return_me += ": " } return_me += linesCount } if len(percent) > 0 { if len(return_me) > 0 { return_me += " " } return_me += percent } if len(displayName) > 0 { return displayName, return_me } return "", return_me } // Wait for the first line to be read. // // Used for making sudo work: // https://github.com/walles/moor/issues/199 func (reader *ReaderImpl) AwaitFirstByte() { <-reader.doneWaitingForFirstByte } // GetLineCount returns the number of lines available for viewing func (reader *ReaderImpl) GetLineCount() int { reader.RLock() defer reader.RUnlock() return len(reader.lines) } func (reader *ReaderImpl) ShouldShowLineCount() bool { if reader.ReadingDone.Load() { // We are done, the number won't change, show it! return true } if !reader.PauseStatus.Load() { // Reading in progress, number is constantly changing so it's // obvious we aren't done yet. Show it! return true } return false } // The index is for error reporting. Set withCache to false to simulate a cache // miss for benchmarking. func (line *Line) Plain(index linemetadata.Index) string { fromCache := line.plainTextCache.Load() if DisablePlainCachingForBenchmarking { // Simulate a cache miss for benchmarking fromCache = nil } if fromCache != nil { return *fromCache } plain := textstyles.StripFormatting(string(line.raw), index) // If this succeeds, all good. If it fails it means some other goroutine // populated the cache before us, which is also fine. _ = line.plainTextCache.CompareAndSwap(nil, &plain) return plain } // GetLine gets a line. If the requested line number is out of bounds, nil is returned. func (reader *ReaderImpl) GetLine(index linemetadata.Index) *NumberedLine { reader.RLock() if index.Index() >= reader.pauseAfterLines-DEFAULT_PAUSE_AFTER_LINES/2 { // Switch to the write lock for changing the pause threshold reader.RUnlock() reader.Lock() // Getting close(ish) to the pause threshold, bump it up. The Max() // construct is to handle the case when the add overflows. reader.pauseAfterLines = slices.Max([]int{ reader.pauseAfterLines + DEFAULT_PAUSE_AFTER_LINES/2, reader.pauseAfterLines}) select { case reader.pauseAfterLinesUpdated <- true: default: // Default case required for the write to be non-blocking } // Back to read lock for the rest of this function reader.Unlock() reader.RLock() } if !index.IsWithinLength(len(reader.lines)) { reader.RUnlock() return nil } returnLine := reader.lines[index.Index()] reader.RUnlock() return &NumberedLine{ Index: index, Number: linemetadata.NumberFromZeroBased(index.Index()), Line: returnLine, } } // Given a starting point and a count, return a start and end index that don't // exceed maxIndex. On overflow, the requested range will be shifted backwards // to fit within maxIndex, and if that's not enough, cut at the end. func clipRangeToLength(start linemetadata.Index, wantedCount int, maxIndex int) (int, int) { if wantedCount <= 0 { panic(fmt.Sprintf("wantedCount must be at least 1, was %d", wantedCount)) } if maxIndex < 0 { panic(fmt.Sprintf("maxIndex must be at least 0, was %d", maxIndex)) } first := start.Index() // Cap wantedCount to the available length (maxIndex+1). available := maxIndex + 1 if wantedCount > available { wantedCount = available } // Clamp start so the window fits: start <= maxIndex - (wantedCount - 1) highestStart := maxIndex - (wantedCount - 1) if first > highestStart { first = highestStart } if first < 0 { first = 0 } last := first + wantedCount - 1 if last > maxIndex { last = maxIndex } return first, last } // GetLines gets the indicated lines from the input func (reader *ReaderImpl) GetLines(firstLine linemetadata.Index, wantedLineCount int) InputLines { reader.RLock() lineCount := len(reader.lines) if lineCount == 0 || wantedLineCount == 0 { filenameText, statusText := reader.createStatusUnlocked(firstLine) reader.RUnlock() return InputLines{ FilenameText: filenameText, StatusText: statusText, } } reader.RUnlock() firstLineIndex, lastLineIndex := clipRangeToLength(firstLine, wantedLineCount, lineCount-1) wantedLineCount = lastLineIndex - firstLineIndex + 1 resultLines := make([]NumberedLine, 0, wantedLineCount) filenameText, statusText := reader.GetLinesPreallocated(linemetadata.IndexFromZeroBased(firstLineIndex), &resultLines) return InputLines{ Lines: resultLines, FilenameText: filenameText, StatusText: statusText, } } // GetLines gets the indicated lines from the input. The lines will be stored // in the provided preallocated slice to avoid allocations. The line count is // determined by the capacity of the provided slice. // // The return value is the status text for the returned lines. func (reader *ReaderImpl) GetLinesPreallocated(firstLine linemetadata.Index, resultLines *[]NumberedLine) (string, string) { // Clear the result slice *resultLines = (*resultLines)[:0] reader.RLock() if len(reader.lines) == 0 || cap(*resultLines) == 0 { filenameText, statusText := reader.createStatusUnlocked(firstLine) reader.RUnlock() return filenameText, statusText } // Prevent reading past the end of the available lines firstLineIndex, lastLineIndex := clipRangeToLength(firstLine, cap(*resultLines), len(reader.lines)-1) filenameText, statusText := reader.createStatusUnlocked(linemetadata.IndexFromZeroBased(lastLineIndex)) for loopIndex, returnLine := range reader.lines[firstLineIndex : lastLineIndex+1] { *resultLines = append(*resultLines, NumberedLine{ Index: linemetadata.IndexFromZeroBased(firstLineIndex + loopIndex), Number: linemetadata.NumberFromZeroBased(firstLineIndex + loopIndex), Line: returnLine, }) } reader.RUnlock() return filenameText, statusText } func (reader *ReaderImpl) PumpToStdout() { const wantedLineCount = 100 firstNotPrintedLine := linemetadata.Index{} drainLines := func() bool { lines := reader.GetLines(firstNotPrintedLine, wantedLineCount) var firstReturnedIndex linemetadata.Index if len(lines.Lines) > 0 { firstReturnedIndex = lines.Lines[0].Index } // Print the lines we got printed := false for loopIndex, line := range lines.Lines { lineIndex := firstReturnedIndex.NonWrappingAdd(loopIndex) if lineIndex.IsBefore(firstNotPrintedLine) { continue } fmt.Println(string(line.Line.raw)) printed = true firstNotPrintedLine = lineIndex.NonWrappingAdd(1) } return printed } drainAllLines := func() { for drainLines() { // Loop here until nothing was printed } } done := false for !done { drainAllLines() select { case <-reader.MoreLinesAdded: continue case <-reader.MaybeDone: done = true } } // Print any remaining lines drainAllLines() } // Replace reader contents with the given text. Consider setting // HighlightingDone and signalling the MaybeDone channel afterwards. func (reader *ReaderImpl) setText(text string) { lines := []*Line{} for _, lineString := range strings.Split(text, "\n") { line := Line{raw: []byte(lineString)} lines = append(lines, &line) } if len(lines) > 0 && strings.HasSuffix(text, "\n") { // Input ends with an empty line. This makes our line count be // off-by-one, fix that! lines = lines[0 : len(lines)-1] } reader.Lock() reader.lines = lines reader.Unlock() log.Trace("Reader done, contents explicitly set") select { case reader.MoreLinesAdded <- true: default: } } func (reader *ReaderImpl) setPauseStatus(paused bool) { if !reader.PauseStatus.CompareAndSwap(!paused, paused) { // Pause status already had that value, we're done return } log.Debugf("Reader pause status changed to %t", paused) } func (reader *ReaderImpl) SetPauseAfterLines(lines int) { if lines < 0 { log.Warnf("Tried to set pause-after-lines to %d, ignoring", lines) return } log.Trace("Setting pause-after-lines to ", lines, "...") reader.Lock() reader.pauseAfterLines = lines reader.Unlock() // Notify the reader that the pause-after-lines value has been updated. Will // be noticed in the maybePause() function. select { case reader.pauseAfterLinesUpdated <- true: default: // Default case required for the write to be non-blocking } } func (reader *ReaderImpl) SetStyleForHighlighting(style chroma.Style) { reader.highlightingStyle <- style } moor-2.10.3/internal/reader/reader_pause_test.go000066400000000000000000000105121513574474500216720ustar00rootroot00000000000000package reader import ( "os" "strings" "testing" "time" "github.com/alecthomas/chroma/v2/formatters" "github.com/alecthomas/chroma/v2/styles" "github.com/walles/moor/v2/internal/linemetadata" "gotest.tools/v3/assert" ) func TestPauseAfterNLines(t *testing.T) { pauseAfterLines := 1 // Get ourselves a reader twoLines := strings.NewReader("one\ntwo\n") testMe, err := NewFromStream( "TestPauseAfterNLines", twoLines, formatters.TTY, ReaderOptions{ PauseAfterLines: &pauseAfterLines, Style: styles.Get("native"), }) assert.NilError(t, err) // Expect pause since we configured the reader to pause after 1 line ^ for testMe.PauseStatus.Load() != true { } assert.Assert(t, testMe.PauseStatus.Load() == true, "Reader should be paused after reading %d lines", pauseAfterLines) // Verify that we have *not* received a done notification yet assert.Assert(t, testMe.ReadingDone.Load() == false, "Reader should not be done yet, only paused") // Check that the reader has exactly the first line and nothing else lines := testMe.GetLines(linemetadata.Index{}, 2).Lines assert.Equal(t, len(lines), 1, "Reader should have exactly one line after pausing") assert.Equal(t, lines[0].Plain(), "one", "Reader should have the first line after pausing") // Tell reader to continue testMe.SetPauseAfterLines(99) // More lines allowed, expect pause to be over for testMe.PauseStatus.Load() != false { } assert.Assert(t, testMe.PauseStatus.Load() == false, "Reader should be unpaused after continuing") // Expect a done notification <-testMe.MaybeDone assert.Assert(t, testMe.ReadingDone.Load() == true, "Reader should be done after reading all lines") // Check that the reader has both lines lines = testMe.GetLines(linemetadata.Index{}, 3).Lines assert.Equal(t, len(lines), 2, "Reader should have two lines after unpausing") assert.Equal(t, lines[0].Plain(), "one", "Reader should have the first line after unpausing") assert.Equal(t, lines[1].Plain(), "two", "Reader should have the second line after unpausing") } // Test pausing behavior after we're done reading from a file, and then another line is added. func TestPauseAfterNLines_Polling(t *testing.T) { pauseAfterLines := 1 // Create a file with a line in it file, err := os.CreateTemp("", "TestPauseAfterNLines_Polling") assert.NilError(t, err) defer os.Remove(file.Name()) //nolint:errcheck _, err = file.WriteString("one\n") assert.NilError(t, err) // Point a reader at the file testMe, err := NewFromFilename(file.Name(), formatters.TTY, ReaderOptions{ PauseAfterLines: &pauseAfterLines, Style: styles.Get("native"), }) assert.NilError(t, err) // Expect pause since we configured the reader to pause after 1 line ^ for testMe.PauseStatus.Load() != true { } assert.Assert(t, testMe.PauseStatus.Load() == true, "Reader should be paused after reading %d lines", pauseAfterLines) // Verify state before we add another line to the file lines := testMe.GetLines(linemetadata.Index{}, 2).Lines assert.Equal(t, len(lines), 1, "Reader should have exactly one line after pausing") assert.Equal(t, lines[0].Plain(), "one", "Reader should have the first line after pausing") // Write another line to the file _, err = file.WriteString("two\n") assert.NilError(t, err) // Wait up to two seconds for tailFile() to give us the new line even though // we are paused. That shouldn't happen. If it does we fail here. // // tailFile() polls every second, so two seconds should cover it. for range 20 { allLines := testMe.GetLines(linemetadata.Index{}, 10) if len(allLines.Lines) == 2 { assert.Assert(t, false, "Reader should not have received a new line while paused") } time.Sleep(100 * time.Millisecond) } // No new line while paused, good! Unpause. testMe.SetPauseAfterLines(99) // Give the new line two seconds to arrive var bothLines []NumberedLine for range 20 { bothLines = testMe.GetLines(linemetadata.Index{}, 10).Lines if len(bothLines) > 1 { break } time.Sleep(100 * time.Millisecond) } // Verify that we have both lines now assert.Equal(t, len(bothLines), 2, "Reader should have two lines after unpausing") assert.Equal(t, bothLines[0].Plain(), "one", "Reader should have the first line after unpausing") assert.Equal(t, bothLines[1].Plain(), "two", "Reader should have the second line after unpausing") } moor-2.10.3/internal/reader/reader_test.go000066400000000000000000000525001513574474500205000ustar00rootroot00000000000000package reader import ( "math" "os" "os/exec" "path" "runtime" "strconv" "strings" "testing" "time" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/formatters" "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" log "github.com/sirupsen/logrus" "gotest.tools/v3/assert" "github.com/walles/moor/v2/internal/linemetadata" ) const samplesDir = "../../sample-files" func init() { // Info logs clutter at least benchmark output log.SetLevel(log.WarnLevel) } func testGetLineCount(t *testing.T, reader *ReaderImpl) { if strings.Contains(*reader.DisplayName, "compressed") { // We are no good at counting lines of compressed files, never mind return } cmd := exec.Command("wc", "-l", *reader.FileName) output, err := cmd.CombinedOutput() if err != nil { t.Error("Error calling wc -l to count lines of", *reader.FileName, err) } wcNumberString := strings.Split(strings.TrimSpace(string(output)), " ")[0] wcLineCount, err := strconv.Atoi(wcNumberString) if err != nil { t.Error("Error counting lines of", *reader.FileName, err) } // wc -l under-counts by 1 if the file doesn't end in a newline rawBytes, err := os.ReadFile(*reader.FileName) if err == nil && len(rawBytes) > 0 && rawBytes[len(rawBytes)-1] != '\n' { wcLineCount++ } if reader.GetLineCount() != wcLineCount { t.Errorf("Got %d lines from the reader but %d lines from wc -l: <%s>", reader.GetLineCount(), wcLineCount, *reader.FileName) } countLinesCount, err := countLines(*reader.FileName) assert.NilError(t, err) if countLinesCount != uint64(wcLineCount) { t.Errorf("Got %d lines from wc -l, but %d lines from our countLines() function", wcLineCount, countLinesCount) } } func firstLine(inputLines InputLines) linemetadata.Index { return inputLines.Lines[0].Index } func testGetLines(t *testing.T, reader *ReaderImpl) { lines := reader.GetLines(linemetadata.Index{}, 10) if len(lines.Lines) > 10 { t.Errorf("Asked for 10 lines, got too many: %d", len(lines.Lines)) } if len(lines.Lines) < 10 { // No good plan for how to test short files, more than just // querying them, which we just did return } // Test clipping at the end lines = reader.GetLines(linemetadata.IndexMax(), 10) if len(lines.Lines) != 10 { t.Errorf("Asked for 10 lines but got %d", len(lines.Lines)) return } startOfLastSection := firstLine(lines) lines = reader.GetLines(startOfLastSection, 10) if firstLine(lines) != startOfLastSection { t.Errorf("Expected start line %d when asking for the last 10 lines, got %s", startOfLastSection, firstLine(lines).Format()) return } if len(lines.Lines) != 10 { t.Errorf("Expected 10 lines when asking for the last 10 lines, got %d", len(lines.Lines)) return } lines = reader.GetLines(startOfLastSection.NonWrappingAdd(1), 10) if firstLine(lines) != startOfLastSection { t.Errorf("Expected start line %d when asking for the last+1 10 lines, got %s", startOfLastSection, firstLine(lines).Format()) return } if len(lines.Lines) != 10 { t.Errorf("Expected 10 lines when asking for the last+1 10 lines, got %d", len(lines.Lines)) return } lines = reader.GetLines(startOfLastSection.NonWrappingAdd(-1), 10) if firstLine(lines) != startOfLastSection.NonWrappingAdd(-1) { t.Errorf("Expected start line %d when asking for the last-1 10 lines, got %s", startOfLastSection, firstLine(lines).Format()) return } if len(lines.Lines) != 10 { t.Errorf("Expected 10 lines when asking for the last-1 10 lines, got %d", len(lines.Lines)) return } } func getTestFiles(t *testing.T) []string { files, err := os.ReadDir(samplesDir) assert.NilError(t, err) var filenames []string for _, file := range files { filenames = append(filenames, path.Join(samplesDir, file.Name())) } return filenames } func TestGetLines(t *testing.T) { for _, file := range getTestFiles(t) { t.Run(file, func(t *testing.T) { reader, err := NewFromFilename(file, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) if err != nil { t.Errorf("Error opening file <%s>: %s", file, err.Error()) return } if err := reader.Wait(); err != nil { t.Errorf("Error reading file <%s>: %s", file, err.Error()) return } t.Run(file, func(t *testing.T) { testGetLines(t, reader) testGetLineCount(t, reader) testHighlightingLineCount(t, file) }) }) } } func testHighlightingLineCount(t *testing.T, filenameWithPath string) { // This won't work on compressed files if strings.HasSuffix(filenameWithPath, ".xz") { return } if strings.HasSuffix(filenameWithPath, ".bz2") { return } if strings.HasSuffix(filenameWithPath, ".gz") { return } if strings.HasSuffix(filenameWithPath, ".zst") { return } if strings.HasSuffix(filenameWithPath, ".zstd") { return } // Load the unformatted file rawBytes, err := os.ReadFile(filenameWithPath) assert.NilError(t, err) rawContents := string(rawBytes) // Count its lines rawLinefeedsCount := strings.Count(rawContents, "\n") rawRunes := []rune(rawContents) rawFileEndsWithNewline := true // Special case empty files if len(rawRunes) > 0 { rawFileEndsWithNewline = rawRunes[len(rawRunes)-1] == '\n' } rawLinesCount := rawLinefeedsCount if !rawFileEndsWithNewline { rawLinesCount++ } // Then load the same file using one of our Readers reader, err := NewFromFilename(filenameWithPath, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) err = reader.Wait() assert.NilError(t, err) highlightedLinesCount := reader.GetLineCount() assert.Equal(t, rawLinesCount, highlightedLinesCount) } func TestGetLongLine(t *testing.T) { file := samplesDir + "/very-long-line.txt" reader, err := NewFromFilename(file, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, reader.Wait()) lines := reader.GetLines(linemetadata.Index{}, 5) assert.Equal(t, firstLine(lines), linemetadata.Index{}) assert.Equal(t, len(lines.Lines), 1) line := lines.Lines[0] assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line.Plain()) assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line.Plain()) assert.Equal(t, len(line.Plain()), 100021) } func getReaderWithLineCount(totalLines int) *ReaderImpl { return NewFromTextForTesting("", strings.Repeat("x\n", totalLines)) } func testStatusText(t *testing.T, fromLine linemetadata.Index, toLine linemetadata.Index, totalLines int, expected string) { testMe := getReaderWithLineCount(totalLines) linesRequested := fromLine.CountLinesTo(toLine) lines := testMe.GetLines(fromLine, linesRequested) statusText := lines.StatusText assert.Equal(t, statusText, expected) } func TestStatusText(t *testing.T) { testStatusText(t, linemetadata.Index{}, linemetadata.IndexFromOneBased(10), 20, "20 lines 50%") testStatusText(t, linemetadata.Index{}, linemetadata.IndexFromOneBased(5), 5, "5 lines 100%") testStatusText(t, linemetadata.IndexFromOneBased(998), linemetadata.IndexFromOneBased(999), 1000, "1000 lines 99%") testStatusText(t, linemetadata.Index{}, linemetadata.Index{}, 0, "") testStatusText(t, linemetadata.Index{}, linemetadata.Index{}, 1, "1 line 100%") // Test with filename testMe, err := NewFromFilename(samplesDir+"/empty", formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, testMe.Wait()) line := testMe.GetLines(linemetadata.Index{}, 0) if line.Lines != nil { t.Error("line.lines is should have been nil when reading from an empty stream") } assert.Equal(t, line.FilenameText, "empty") assert.Equal(t, line.StatusText, ": ") } func testCompressedFile(t *testing.T, filename string) { filenameWithPath := path.Join(samplesDir, filename) reader, e := NewFromFilename(filenameWithPath, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) if e != nil { t.Errorf("Error opening file <%s>: %s", filenameWithPath, e.Error()) panic(e) } assert.NilError(t, reader.Wait()) lines := reader.GetLines(linemetadata.Index{}, 5) assert.Equal(t, lines.Lines[0].Plain(), "This is a compressed file", "%s", filename) } func TestCompressedFiles(t *testing.T) { testCompressedFile(t, "compressed.txt.gz") testCompressedFile(t, "compressed.txt.bz2") testCompressedFile(t, "compressed.txt.xz") testCompressedFile(t, "compressed.txt.zst") testCompressedFile(t, "compressed.txt.zstd") } func TestReadFileDoneNoHighlighting(t *testing.T) { testMe, err := NewFromFilename(samplesDir+"/empty", formatters.TTY, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, testMe.Wait()) } func TestReadFileDoneYesHighlighting(t *testing.T) { testMe, err := NewFromFilename("reader_test.go", formatters.TTY, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, testMe.Wait()) } func TestReadStreamDoneNoHighlighting(t *testing.T) { testMe, err := NewFromStream("", strings.NewReader("Johan"), nil, ReaderOptions{Style: &chroma.Style{}}) assert.NilError(t, err) assert.NilError(t, testMe.Wait()) } func TestReadStreamDoneYesHighlighting(t *testing.T) { testMe, err := NewFromStream("", strings.NewReader("Johan"), formatters.TTY, ReaderOptions{Lexer: lexers.EmacsLisp, Style: styles.Get("native")}) assert.NilError(t, err) assert.NilError(t, testMe.Wait()) } func TestReadTextDone(t *testing.T) { testMe := NewFromTextForTesting("", "Johan") assert.NilError(t, testMe.Wait()) } // JSON should be auto detected and formatted func TestFormatJson(t *testing.T) { // Note the space after "key" to verify formatting actually happens jsonStream := strings.NewReader(`{"key" :"value"}`) testMe, err := NewFromStream( "JSON test", jsonStream, formatters.TTY, ReaderOptions{ Style: styles.Get("native"), ShouldFormat: true, }) assert.NilError(t, err) assert.NilError(t, testMe.Wait()) lines := testMe.GetLines(linemetadata.Index{}, 10) assert.Equal(t, lines.Lines[0].Plain(), "{") assert.Equal(t, lines.Lines[1].Plain(), ` "key": "value"`) assert.Equal(t, lines.Lines[2].Plain(), "}") assert.Equal(t, len(lines.Lines), 3) } func TestFormatJsonArray(t *testing.T) { // Note the space after "key" to verify formatting actually happens jsonStream := strings.NewReader(`[{"key" :"value"}]`) testMe, err := NewFromStream( "JSON test", jsonStream, formatters.TTY, ReaderOptions{ Style: styles.Get("native"), ShouldFormat: true, }) assert.NilError(t, err) assert.NilError(t, testMe.Wait()) lines := testMe.GetLines(linemetadata.Index{}, 10) assert.Equal(t, lines.Lines[0].Plain(), "[") assert.Equal(t, lines.Lines[1].Plain(), " {") assert.Equal(t, lines.Lines[2].Plain(), ` "key": "value"`) assert.Equal(t, lines.Lines[3].Plain(), " }") assert.Equal(t, lines.Lines[4].Plain(), "]") assert.Equal(t, len(lines.Lines), 5) } // If people keep appending to the currently opened file we should display those // changes. func TestReadUpdatingFile(t *testing.T) { // Make a temp file containing one line of text, ending with a newline file, err := os.CreateTemp("", "moor-TestReadUpdatingFile-*.txt") assert.NilError(t, err) defer os.Remove(file.Name()) //nolint:errcheck const firstLineString = "First line\n" _, err = file.WriteString(firstLineString) assert.NilError(t, err) // Start a reader on that file testMe, err := NewFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading assert.NilError(t, testMe.Wait()) assert.Equal(t, len([]byte(firstLineString)), int(testMe.bytesCount)) // Verify we got the single line allLines := testMe.GetLines(linemetadata.Index{}, 10) assert.Equal(t, len(allLines.Lines), 1) assert.Equal(t, testMe.GetLineCount(), 1) assert.Equal(t, allLines.Lines[0].Plain(), "First line") // Append a line to the file const secondLineString = "Second line\n" _, err = file.WriteString(secondLineString) assert.NilError(t, err) // Give the reader some time to react for range 20 { allLines := testMe.GetLines(linemetadata.Index{}, 10) if len(allLines.Lines) == 2 { break } time.Sleep(100 * time.Millisecond) } // Verify we got the two lines allLines = testMe.GetLines(linemetadata.Index{}, 10) assert.Equal(t, len(allLines.Lines), 2, "Expected two lines after adding a second one, got %d", len(allLines.Lines)) assert.Equal(t, testMe.GetLineCount(), 2) assert.Equal(t, allLines.Lines[0].Plain(), "First line") assert.Equal(t, allLines.Lines[1].Plain(), "Second line") assert.Equal(t, int(testMe.bytesCount), len([]byte(firstLineString+secondLineString))) // Append a third line to the file. We want to verify line 2 didn't just // succeed due to special handling. const thirdLineString = "Third line\n" _, err = file.WriteString(thirdLineString) assert.NilError(t, err) // Give the reader some time to react for i := 0; i < 20; i++ { allLines = testMe.GetLines(linemetadata.Index{}, 10) if len(allLines.Lines) == 3 { break } time.Sleep(100 * time.Millisecond) } // Verify we got all three lines allLines = testMe.GetLines(linemetadata.Index{}, 10) assert.Equal(t, len(allLines.Lines), 3, "Expected three lines after adding a third one, got %d", len(allLines.Lines)) assert.Equal(t, testMe.GetLineCount(), 3) assert.Equal(t, allLines.Lines[0].Plain(), "First line") assert.Equal(t, allLines.Lines[1].Plain(), "Second line") assert.Equal(t, allLines.Lines[2].Plain(), "Third line") assert.Equal(t, int(testMe.bytesCount), len([]byte(firstLineString+secondLineString+thirdLineString))) } // If people keep appending to the currently opened file we should display those // changes. // // This test verifies it with an initially empty file. func TestReadUpdatingFile_InitiallyEmpty(t *testing.T) { // Make a temp file containing one line of text, ending with a newline file, err := os.CreateTemp("", "moor-TestReadUpdatingFile_NoNewlineAtEOF-*.txt") assert.NilError(t, err) defer os.Remove(file.Name()) //nolint:errcheck // Start a reader on that file testMe, err := NewFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading assert.NilError(t, testMe.Wait()) // Verify no lines allLines := testMe.GetLines(linemetadata.Index{}, 10) assert.Equal(t, len(allLines.Lines), 0) assert.Equal(t, testMe.GetLineCount(), 0) // Append a line to the file _, err = file.WriteString("Text\n") assert.NilError(t, err) // Give the reader some time to react for i := 0; i < 20; i++ { allLines := testMe.GetLines(linemetadata.Index{}, 10) if len(allLines.Lines) == 1 { break } time.Sleep(100 * time.Millisecond) } // Verify we got the two lines allLines = testMe.GetLines(linemetadata.Index{}, 10) assert.Equal(t, len(allLines.Lines), 1, "Expected one line after adding one, got %d", len(allLines.Lines)) assert.Equal(t, testMe.GetLineCount(), 1) assert.Equal(t, allLines.Lines[0].Plain(), "Text") } // If people keep appending to the currently opened file we should display those // changes. // // This test verifies it with the initial contents not ending with a linefeed. func TestReadUpdatingFile_HalfLine(t *testing.T) { // Make a temp file containing one line of text, ending with a newline file, err := os.CreateTemp("", "moor-TestReadUpdatingFile-*.txt") assert.NilError(t, err) defer os.Remove(file.Name()) //nolint:errcheck _, err = file.WriteString("Start") assert.NilError(t, err) // Start a reader on that file testMe, err := NewFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading assert.NilError(t, testMe.Wait()) assert.Equal(t, int(testMe.bytesCount), len([]byte("Start"))) // Append the rest of the line const secondLineString = ", end\n" _, err = file.WriteString(secondLineString) assert.NilError(t, err) // Give the reader some time to react for i := 0; i < 20; i++ { allLines := testMe.GetLines(linemetadata.Index{}, 10) if len(allLines.Lines) == 2 { break } time.Sleep(100 * time.Millisecond) } // Verify we got the two lines allLines := testMe.GetLines(linemetadata.Index{}, 10) assert.Equal(t, len(allLines.Lines), 1, "Still expecting one line, got %d", len(allLines.Lines)) assert.Equal(t, testMe.GetLineCount(), 1) assert.Equal(t, allLines.Lines[0].Plain(), "Start, end") assert.Equal(t, int(testMe.bytesCount), len([]byte("Start, end\n"))) } // If people keep appending to the currently opened file we should display those // changes. // // This test verifies it with the initial contents ending in the middle of an UTF-8 character. func TestReadUpdatingFile_HalfUtf8(t *testing.T) { // Make a temp file containing one line of text, ending with a newline file, err := os.CreateTemp("", "moor-TestReadUpdatingFile-*.txt") assert.NilError(t, err) defer os.Remove(file.Name()) //nolint:errcheck // Write "h" and half an "ä" to the file _, err = file.Write([]byte("här"[0:2])) assert.NilError(t, err) // Start a reader on that file testMe, err := NewFromFilename(file.Name(), formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(t, err) // Wait for the reader to finish reading assert.NilError(t, testMe.Wait()) assert.Equal(t, testMe.GetLineCount(), 1) // Append the rest of the UTF-8 character _, err = file.WriteString("här"[2:]) assert.NilError(t, err) // Give the reader some time to react for range 20 { allLines := testMe.GetLines(linemetadata.Index{}, 10) if len(allLines.Lines) == 2 { break } time.Sleep(100 * time.Millisecond) } // Verify we got the two lines allLines := testMe.GetLines(linemetadata.Index{}, 10) assert.Equal(t, len(allLines.Lines), 1, "Still expecting one line, got %d", len(allLines.Lines)) assert.Equal(t, testMe.GetLineCount(), 1) assert.Equal(t, allLines.Lines[0].Plain(), "här") assert.Equal(t, int(testMe.bytesCount), len([]byte("här"))) } func TestClipRangeToLength(t *testing.T) { // Within bounds i0, i1 := clipRangeToLength(linemetadata.Index{}, 1, 20) assert.Equal(t, i0, 0) assert.Equal(t, i1, 0) // Touching the end, still within bounds i0, i1 = clipRangeToLength(linemetadata.Index{}, 1, 0) assert.Equal(t, i0, 0) assert.Equal(t, i1, 0) // Overflow, push down to indices 6, 7, 8 i0, i1 = clipRangeToLength(linemetadata.IndexFromOneBased(100), 3, 8) assert.Equal(t, i0, 6) assert.Equal(t, i1, 8) // Overflow, push down and clip to indices 0, 1 i0, i1 = clipRangeToLength(linemetadata.IndexFromOneBased(100), 3, 1) assert.Equal(t, i0, 0) assert.Equal(t, i1, 1) // Maxed out start i0, i1 = clipRangeToLength(linemetadata.IndexMax(), 1, 0) assert.Equal(t, i0, 0) assert.Equal(t, i1, 0) // Maxed out count i0, i1 = clipRangeToLength(linemetadata.Index{}, math.MaxInt, 0) assert.Equal(t, i0, 0) assert.Equal(t, i1, 0) // Maxed out start and count i0, i1 = clipRangeToLength(linemetadata.IndexMax(), math.MaxInt, 3) assert.Equal(t, i0, 0) assert.Equal(t, i1, 3) } // How long does it take to read a file? // // This can be slow due to highlighting. // // Run with: go test -run='^$' -bench=. . ./... func BenchmarkReaderDone(b *testing.B) { filename := "reader.go" // This is our longest .go file b.ResetTimer() for n := 0; n < b.N; n++ { // This is our longest .go file readMe, err := NewFromFilename(filename, formatters.TTY16m, ReaderOptions{Style: styles.Get("native")}) assert.NilError(b, err) assert.NilError(b, readMe.Wait()) assert.NilError(b, readMe.Err) } } // Try loading a large file func BenchmarkReadLargeFile(b *testing.B) { // Try loading a file this large const largeSizeBytes = 35_000_000 // First, create it from something... inputFilename := "reader.go" contents, err := os.ReadFile(inputFilename) assert.NilError(b, err) testdir := b.TempDir() largeFileName := testdir + "/large-file" largeFile, err := os.Create(largeFileName) assert.NilError(b, err) totalBytesWritten := 0 for totalBytesWritten < largeSizeBytes { written, err := largeFile.Write(contents) assert.NilError(b, err) totalBytesWritten += written } err = largeFile.Close() assert.NilError(b, err) // Make sure we don't pause during the benchmark targetLineCount := largeSizeBytes * 2 b.SetBytes(int64(totalBytesWritten)) // Try making the whole run more predictable runtime.GC() b.ResetTimer() for n := 0; n < b.N; n++ { readMe, err := NewFromFilename( largeFileName, formatters.TTY16m, ReaderOptions{ Style: styles.Get("native"), PauseAfterLines: &targetLineCount, }) assert.NilError(b, err) <-readMe.MaybeDone assert.NilError(b, readMe.Wait()) assert.NilError(b, readMe.Err) } } // Count lines in pager.go func BenchmarkCountLines(b *testing.B) { // First, get some sample lines... inputFilename := "reader.go" contents, err := os.ReadFile(inputFilename) assert.NilError(b, err) testdir := b.TempDir() countFileName := testdir + "/count-file" countFile, err := os.Create(countFileName) assert.NilError(b, err) // Make a large enough test case that a majority of the time is spent // counting lines, rather than on any counting startup cost. // // We used to have 1000 here, but that made the benchmark result fluctuate // too much. 10_000 seems to provide stable enough results. for range 10_000 { _, err := countFile.Write(contents) assert.NilError(b, err) } err = countFile.Close() assert.NilError(b, err) b.ResetTimer() for range b.N { _, err = countLines(countFileName) assert.NilError(b, err) } } moor-2.10.3/internal/reader/zopen.go000066400000000000000000000072311513574474500173330ustar00rootroot00000000000000package reader import ( "bytes" "compress/bzip2" "compress/gzip" "fmt" "io" "os" "strings" "github.com/klauspost/compress/zstd" log "github.com/sirupsen/logrus" "github.com/ulikunitz/xz" ) var gzipMagic = []byte{0x1f, 0x8b} var bzip2Magic = []byte{0x42, 0x5a, 0x68} var zstdMagic = []byte{0x28, 0xb5, 0x2f, 0xfd} var xzMagic = []byte{0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00} // The second return value is the file name with any compression extension removed. func ZOpen(filename string) (io.ReadCloser, string, error) { file, err := os.Open(filename) if err != nil { return nil, "", err } // Read the first 6 bytes to determine the compression type firstBytes := make([]byte, 6) _, err = file.Read(firstBytes) if err != nil { if err == io.EOF { // File was empty return file, filename, nil } return nil, "", fmt.Errorf("failed to read file: %w", err) } // Reset file reader to start of file _, err = file.Seek(0, 0) if err != nil { return nil, "", fmt.Errorf("failed to seek to start of file: %w", err) } switch { case bytes.HasPrefix(firstBytes, gzipMagic): log.Debugf("File is gzip compressed: %v", filename) reader, err := gzip.NewReader(file) if err != nil { return nil, "", err } newName := strings.TrimSuffix(filename, ".gz") // Ref: https://github.com/walles/moor/issues/194 if strings.HasSuffix(newName, ".tgz") { newName = strings.TrimSuffix(newName, ".tgz") + ".tar" } return reader, newName, err case bytes.HasPrefix(firstBytes, bzip2Magic): log.Debugf("File is bzip2 compressed: %v", filename) return struct { io.Reader io.Closer }{bzip2.NewReader(file), file}, strings.TrimSuffix(filename, ".bz2"), nil case bytes.HasPrefix(firstBytes, zstdMagic): log.Debugf("File is zstd compressed: %v", filename) decoder, err := zstd.NewReader(file) if err != nil { return nil, "", err } newName := strings.TrimSuffix(filename, ".zst") newName = strings.TrimSuffix(newName, ".zstd") return decoder.IOReadCloser(), newName, nil case bytes.HasPrefix(firstBytes, xzMagic): log.Debugf("File is xz compressed: %v", filename) xzReader, err := xz.NewReader(file) if err != nil { return nil, "", err } return struct { io.Reader io.Closer }{xzReader, file}, strings.TrimSuffix(filename, ".xz"), nil } log.Debugf("File is assumed to be uncompressed: %v", filename) return file, filename, nil } // ZReader returns a reader that decompresses the input stream. Any input stream // compression will be automatically detected. Uncompressed streams will be // returned as-is. // // Ref: https://github.com/walles/moor/issues/261 func ZReader(input io.Reader) (io.Reader, error) { // Read the first 6 bytes to determine the compression type firstBytes := make([]byte, 6) count, err := input.Read(firstBytes) if err != nil { if err == io.EOF { // Stream was empty return input, nil } return nil, fmt.Errorf("failed to read stream: %w", err) } firstBytes = firstBytes[:count] // Reset input reader to start of stream input = io.MultiReader(bytes.NewReader(firstBytes), input) switch { case bytes.HasPrefix(firstBytes, gzipMagic): log.Info("Input stream is gzip compressed") return gzip.NewReader(input) case bytes.HasPrefix(firstBytes, zstdMagic): log.Info("Input stream is zstd compressed") return zstd.NewReader(input) case bytes.HasPrefix(firstBytes, bzip2Magic): log.Info("Input stream is bzip2 compressed") return bzip2.NewReader(input), nil case bytes.HasPrefix(firstBytes, xzMagic): log.Info("Input stream is xz compressed") return xz.NewReader(input) default: // No magic numbers matched log.Info("Input stream is assumed to be uncompressed") return input, nil } } moor-2.10.3/internal/reader/zopen_test.go000066400000000000000000000012631513574474500203710ustar00rootroot00000000000000package reader import ( "bytes" "io" "testing" "gotest.tools/v3/assert" ) // Test that ZReader works with an empty stream func TestZReaderEmpty(t *testing.T) { bytesReader := bytes.NewReader([]byte{}) zReader, err := ZReader(bytesReader) assert.NilError(t, err) all, err := io.ReadAll(zReader) assert.NilError(t, err) assert.Equal(t, 0, len(all)) } // Test that ZReader works with a one-byte stream func TestZReaderOneByte(t *testing.T) { bytesReader := bytes.NewReader([]byte{42}) zReader, err := ZReader(bytesReader) assert.NilError(t, err) all, err := io.ReadAll(zReader) assert.NilError(t, err) assert.Equal(t, 1, len(all)) assert.Equal(t, byte(42), all[0]) } moor-2.10.3/internal/screenLines.go000066400000000000000000000334721513574474500172160ustar00rootroot00000000000000package internal import ( "fmt" "github.com/davecgh/go-spew/spew" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/twin" ) type renderedLine struct { // Certain lines are available for viewing. This index is the (zero based) // position of this line among those. inputLineIndex linemetadata.Index // If an input line has been wrapped into two, the part on the second line // will have a wrapIndex of 1. wrapIndex int containsSearchHit bool cells textstyles.CellWithMetadataSlice // Used for rendering clear-to-end-of-line control sequences: // https://en.wikipedia.org/wiki/ANSI_escape_code#EL // // Ref: https://github.com/walles/moor/issues/106 trailer twin.Style } type renderedScreen struct { lines []renderedLine inputLines []reader.NumberedLine numberPrefixWidth int // Including padding. 0 means no line numbers. filenameText string statusText string } // Refresh the whole pager display, both contents lines and the status line at // the bottom func (p *Pager) redraw(spinner string) { log.Trace("redraw called") p.screen.Clear() p.longestLineLength = 0 lastUpdatedScreenLineNumber := -1 renderedScreen := p.renderLines() for screenLineNumber, row := range renderedScreen.lines { lastUpdatedScreenLineNumber = screenLineNumber column := 0 for _, cell := range row.cells { column += p.screen.SetCell(column, lastUpdatedScreenLineNumber, cell.ToStyledRune()) } } // Status line code follows eofSpinner := spinner if eofSpinner == "" { // This happens when we're done eofSpinner = "---" } spinnerLine := textstyles.StyledRunesFromString(statusbarStyle, eofSpinner, nil, 0).StyledRunes column := 0 for _, cell := range spinnerLine { column += p.screen.SetCell(column, lastUpdatedScreenLineNumber+1, cell.ToStyledRune()) } p.mode.drawFooter(renderedScreen.filenameText, renderedScreen.statusText, spinner) p.screen.Show() } // Render all lines that should go on the screen. // // Returns both the lines and a suitable status text. // // The returned lines are display ready, meaning that they come with horizontal // scroll markers and line numbers as necessary. // // The maximum number of lines returned by this method is limited by the screen // height. If the status line is visible, you'll get at most one less than the // screen height from this method. func (p *Pager) renderLines() renderedScreen { rendered := p.internalRenderLines(true) hasSearchHitLines := false hasNonSearchHitLines := false for _, line := range rendered.lines { if line.containsSearchHit { hasSearchHitLines = true } else { hasNonSearchHitLines = true } } if hasSearchHitLines && !hasNonSearchHitLines { // All lines have search hits, don't highlight any lines // Ref: https://github.com/walles/moor/issues/335 rendered = p.internalRenderLines(false) } return rendered } func (p *Pager) internalRenderLines(highlightSearchHitLines bool) renderedScreen { var lineIndexToShow linemetadata.Index if p.lineIndex() != nil { lineIndexToShow = *p.lineIndex() } inputLines := p.Reader().GetLines(lineIndexToShow, p.visibleHeight()) if len(inputLines.Lines) == 0 { // Empty input, empty output return renderedScreen{filenameText: inputLines.FilenameText, statusText: inputLines.StatusText} } lastVisibleLineNumber := inputLines.Lines[len(inputLines.Lines)-1].Number numberPrefixLength := p.getLineNumberPrefixLength(lastVisibleLineNumber) allLines := make([]renderedLine, 0) for _, line := range inputLines.Lines { rendering := p.renderLine(line, numberPrefixLength, highlightSearchHitLines) var onScreenLength int for i := range rendering { trimmedLen := len(rendering[i].cells.WithoutSpaceRight()) if trimmedLen > onScreenLength { onScreenLength = trimmedLen } } // We're trying to find the max length of readable characters to limit // the scrolling to right, so we don't go over into the vast emptiness for no reason. // // The -1 fixed an issue that seemed like an off-by-one where sometimes, when first // scrolling completely to the right, the first left scroll did not show the text again. displayLength := p.leftColumnZeroBased + onScreenLength - 1 if displayLength >= p.longestLineLength { p.longestLineLength = displayLength } allLines = append(allLines, rendering...) } // Find which index in allLines the user wants to see at the top of the // screen firstVisibleIndex := -1 // Not found for index, line := range allLines { if p.lineIndex() == nil { // Expected zero lines but got some anyway, grab the first one! firstVisibleIndex = index break } if line.inputLineIndex == *p.lineIndex() && line.wrapIndex == p.deltaScreenLines() { firstVisibleIndex = index break } } if firstVisibleIndex == -1 { allLinesDescription := fmt.Sprintf("size %d", len(allLines)) if len(allLines) > 0 { allLinesDescription += fmt.Sprintf(", indexed %d-%d:\n", allLines[0].inputLineIndex, allLines[len(allLines)-1].inputLineIndex) } panic(fmt.Errorf("scrollPosition index=%d not found in allLines %s:\n%s", lineIndexToShow, allLinesDescription, spew.Sdump(p.scrollPosition), )) } // Drop the lines that should go above the screen allLines = allLines[firstVisibleIndex:] // Drop the lines that would have gone below the screen wantedLineCount := p.visibleHeight() if len(allLines) > wantedLineCount { allLines = allLines[0:wantedLineCount] } // Fill in the line trailers screenWidth, _ := p.screen.Size() for i := range allLines { line := &allLines[i] if line.trailer == twin.StyleDefault { continue } for len(line.cells) < screenWidth { line.cells = append(line.cells, textstyles.CellWithMetadata{Rune: ' ', Style: line.trailer}) } } return renderedScreen{ lines: allLines, filenameText: inputLines.FilenameText, statusText: inputLines.StatusText, inputLines: inputLines.Lines, numberPrefixWidth: numberPrefixLength, } } // Render one input line into one or more screen lines. // // The returned line is display ready, meaning that it comes with horizontal // scroll markers and line number as necessary. // // lineNumber and numberPrefixLength are required for knowing how much to // indent, and to (optionally) render the line number. func (p *Pager) renderLine(line reader.NumberedLine, numberPrefixLength int, highlightSearchHitLines bool) []renderedLine { width, _ := p.screen.Size() var wrapped []textstyles.StyledRunesWithTrailer var highlighted textstyles.StyledRunesWithTrailer if p.WrapLongLines { highlighted = line.HighlightedTokens(plainTextStyle, searchHitStyle, p.search, 0) wrapped = wrapLine(width-numberPrefixLength, highlighted.StyledRunes) } else { // Request only screen width tokens plus whatever is needed on the left // due to horizontal scrolling. Also, get one extra to the right so we // can know whether to show overflow markers. // // This is a huge performance gain when dealing with files with // extremeny long lines: https://github.com/walles/moor/issues/358 highlighted = line.HighlightedTokens(plainTextStyle, searchHitStyle, p.search, width+p.leftColumnZeroBased+1) // All on one line wrapped = []textstyles.StyledRunesWithTrailer{{ StyledRunes: highlighted.StyledRunes, Trailer: highlighted.Trailer, ContainsSearchHit: highlighted.ContainsSearchHit, }} } if highlightSearchHitLines && searchHitLineBackground != nil { // Highlight any sub lines with search hits for i := range wrapped { line := &wrapped[i] // We need a pointer to modify in place, otherwise setting the trailer won't have any effect if line.ContainsSearchHit { // Highlight this line! for i := range line.StyledRunes { line.StyledRunes[i].Style = line.StyledRunes[i].Style.WithBackground(*searchHitLineBackground) } line.Trailer = line.Trailer.WithBackground(*searchHitLineBackground) } } } rendered := make([]renderedLine, 0) for wrapIndex, subLine := range wrapped { lineNumber := line.Number visibleLineNumber := &lineNumber if wrapIndex > 0 { visibleLineNumber = nil } decorated := p.decorateLine(visibleLineNumber, numberPrefixLength, subLine.StyledRunes) rendered = append(rendered, renderedLine{ inputLineIndex: line.Index, wrapIndex: wrapIndex, cells: decorated, containsSearchHit: subLine.ContainsSearchHit, trailer: subLine.Trailer, }) } lastRenderedLine := &rendered[len(rendered)-1] lastLineHasSearchHighlight := highlightSearchHitLines && lastRenderedLine.containsSearchHit if highlighted.Trailer != twin.StyleDefault && !lastLineHasSearchHighlight { // In the presence of wrapping, add the trailer to the last of the wrap // lines only. This matches what both iTerm and the macOS Terminal does. lastRenderedLine.trailer = highlighted.Trailer } return rendered } // Take a rendered line and decorate as needed: // - Line number, or leading whitespace for wrapped lines // - Scroll left indicator // - Scroll right indicator func (p *Pager) decorateLine(lineNumberToShow *linemetadata.Number, numberPrefixLength int, contents []textstyles.CellWithMetadata) []textstyles.CellWithMetadata { width, _ := p.screen.Size() newLine := make([]textstyles.CellWithMetadata, 0, width) newLine = append(newLine, createLinePrefix(lineNumberToShow, numberPrefixLength)...) // Find the first and last fully visible runes. var firstVisibleRuneIndex *int lastVisibleRuneIndex := -1 screenColumn := numberPrefixLength // Zero based lastVisibleScreenColumn := p.leftColumnZeroBased + width - 1 cutOffRuneToTheLeft := false cutOffRuneToTheRight := false canScrollRight := false for i, char := range contents { if firstVisibleRuneIndex == nil && screenColumn >= p.leftColumnZeroBased { // Found the first fully visible rune. We need to point to a copy of // our loop variable, not the loop variable itself. Just pointing to // i, will make firstVisibleRuneIndex point to a new value for every // iteration of the loop. copyOfI := i firstVisibleRuneIndex = ©OfI if i > 0 && screenColumn > p.leftColumnZeroBased && contents[i-1].Width() > 1 { // We had to cut a rune in half at the start cutOffRuneToTheLeft = true } } screenReached := firstVisibleRuneIndex != nil currentCharRightEdge := screenColumn + char.Width() - 1 beforeRightEdge := currentCharRightEdge <= lastVisibleScreenColumn if screenReached { if beforeRightEdge { // This rune is fully visible lastVisibleRuneIndex = i } else { // We're just outside the screen on the right canScrollRight = true currentCharLeftEdge := screenColumn if currentCharLeftEdge <= lastVisibleScreenColumn { // We have to cut this rune in half cutOffRuneToTheRight = true } // Search done, we're off the right edge break } } screenColumn += char.Width() } // Prepend a space if we had to cut a rune in half at the start if cutOffRuneToTheLeft { newLine = append([]textstyles.CellWithMetadata{{Rune: ' ', Style: p.ScrollLeftHint.Style}}, newLine...) } // Add the visible runes if firstVisibleRuneIndex != nil { newLine = append(newLine, contents[*firstVisibleRuneIndex:lastVisibleRuneIndex+1]...) } // Append a space if we had to cut a rune in half at the end if cutOffRuneToTheRight { newLine = append(newLine, textstyles.CellWithMetadata{Rune: ' ', Style: p.ScrollRightHint.Style}) } // Add scroll left indicator canScrollLeft := p.leftColumnZeroBased > 0 if canScrollLeft && len(contents) > 0 { if len(newLine) == 0 { // Make room for the scroll left indicator newLine = make([]textstyles.CellWithMetadata, 1) } if newLine[0].Width() > 1 { // Replace the first rune with two spaces so we can replace the // leftmost cell with a scroll left indicator. First, convert to one // space... newLine[0] = textstyles.CellWithMetadata{Rune: ' ', Style: p.ScrollLeftHint.Style} // ...then prepend another space: newLine = append([]textstyles.CellWithMetadata{{Rune: ' ', Style: p.ScrollLeftHint.Style}}, newLine...) // Prepending ref: https://stackoverflow.com/a/53737602/473672 } // Set can-scroll-left marker newLine[0] = p.ScrollLeftHint } // Add scroll right indicator if canScrollRight { if newLine[len(newLine)-1].Width() > 1 { // Replace the last rune with two spaces so we can replace the // rightmost cell with a scroll right indicator. First, convert to one // space... newLine[len(newLine)-1] = textstyles.CellWithMetadata{Rune: ' ', Style: p.ScrollRightHint.Style} // ...then append another space: newLine = append(newLine, textstyles.CellWithMetadata{Rune: ' ', Style: p.ScrollRightHint.Style}) } newLine[len(newLine)-1] = p.ScrollRightHint } return newLine } // Generate a line number prefix of the given length. // // Can be empty or all-whitespace depending on parameters. func createLinePrefix(lineNumber *linemetadata.Number, numberPrefixLength int) []textstyles.CellWithMetadata { if numberPrefixLength == 0 { return []textstyles.CellWithMetadata{} } lineNumberPrefix := make([]textstyles.CellWithMetadata, 0, numberPrefixLength) if lineNumber == nil { for len(lineNumberPrefix) < numberPrefixLength { lineNumberPrefix = append(lineNumberPrefix, textstyles.CellWithMetadata{Rune: ' '}) } return lineNumberPrefix } lineNumberString := fmt.Sprintf("%*s ", numberPrefixLength-1, lineNumber.Format()) if len(lineNumberString) > numberPrefixLength { panic(fmt.Errorf( "lineNumberString <%s> longer than numberPrefixLength %d", lineNumberString, numberPrefixLength)) } for column, digit := range lineNumberString { if column >= numberPrefixLength { break } lineNumberPrefix = append(lineNumberPrefix, textstyles.CellWithMetadata{Rune: digit, Style: lineNumbersStyle}) } return lineNumberPrefix } moor-2.10.3/internal/screenLines_test.go000066400000000000000000000344501513574474500202520ustar00rootroot00000000000000package internal import ( "strconv" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/internal/search" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" log "github.com/sirupsen/logrus" ) // NOTE: You can find related tests in pager_test.go. // Converts a cell row to a plain string and removes trailing whitespace. func renderedToString(row []textstyles.CellWithMetadata) string { rowString := "" for _, cell := range row { rowString += string(cell.Rune) } return strings.TrimRight(rowString, " ") } func testHorizontalCropping(t *testing.T, contents string, firstVisibleColumn int, lastVisibleColumn int, expected string) { log.SetLevel(log.WarnLevel) // Stop info logs from polluting benchmark output reader := reader.NewFromTextForTesting("testHorizontalCropping", contents) assert.NilError(t, reader.Wait()) pager := NewPager(reader) pager.ShowLineNumbers = false pager.showLineNumbers = false pager.screen = twin.NewFakeScreen(1+lastVisibleColumn-firstVisibleColumn, 99) pager.leftColumnZeroBased = firstVisibleColumn pager.scrollPosition = newScrollPosition("testHorizontalCropping") numberedLine := reader.GetLine(linemetadata.IndexFromZeroBased(0)) assert.Assert(t, numberedLine != nil) screenLine := pager.renderLine(*numberedLine, pager.getLineNumberPrefixLength(numberedLine.Number), true) assert.Equal(t, renderedToString(screenLine[0].cells), expected) } func TestCreateScreenLine(t *testing.T) { testHorizontalCropping(t, "abc", 0, 10, "abc") } func TestCreateScreenLineCanScrollLeft(t *testing.T) { testHorizontalCropping(t, "abc", 1, 10, "") } func TestCreateScreenLineCanAlmostScrollRight(t *testing.T) { testHorizontalCropping(t, "abc", 0, 2, "abc") } func TestCreateScreenLineCanScrollBoth(t *testing.T) { testHorizontalCropping(t, "abcde", 1, 3, "") } func TestCreateScreenLineCanAlmostScrollBoth(t *testing.T) { testHorizontalCropping(t, "abcd", 1, 3, "") testHorizontalCropping(t, "上åˆä¸‹", 0, 3, "上 >") testHorizontalCropping(t, "上åˆä¸‹", 0, 2, "上>") testHorizontalCropping(t, "上åˆä¸‹", 0, 1, " >") } func TestEmpty(t *testing.T) { pager := Pager{ screen: twin.NewFakeScreen(99, 10), // No lines available readers: []*reader.ReaderImpl{reader.NewFromTextForTesting("test", "")}, scrollPosition: newScrollPosition("TestEmpty"), } pager.filteringReader = FilteringReader{ BackingReader: pager.readers[pager.currentReader], Filter: &pager.filter, } rendered := pager.renderLines() assert.Equal(t, len(rendered.lines), 0) assert.Equal(t, "test", rendered.filenameText) assert.Equal(t, ": ", rendered.statusText) assert.Assert(t, pager.lineIndex() == nil) } // Repro case for a search bug discovered in v1.9.8. func TestSearchHighlight(t *testing.T) { numberedLine := reader.NewFromTextForTesting("TestSearchHighlight", "x\"\"x").GetLine(linemetadata.Index{}) pager := Pager{ screen: twin.NewFakeScreen(100, 10), search: search.For("\""), } rendered := pager.renderLine(*numberedLine, pager.getLineNumberPrefixLength(numberedLine.Number), true) assert.DeepEqual(t, []renderedLine{ { inputLineIndex: linemetadata.Index{}, wrapIndex: 0, containsSearchHit: true, cells: []textstyles.CellWithMetadata{ {Rune: 'x', Style: twin.StyleDefault}, {Rune: '"', Style: twin.StyleDefault.WithAttr(twin.AttrReverse), IsSearchHit: true, StartsSearchHit: true}, {Rune: '"', Style: twin.StyleDefault.WithAttr(twin.AttrReverse), IsSearchHit: true, StartsSearchHit: false}, {Rune: 'x', Style: twin.StyleDefault}, }, }, }, rendered, cmp.AllowUnexported(twin.Style{}), cmp.AllowUnexported(renderedLine{}), cmp.AllowUnexported(linemetadata.Number{}), cmp.AllowUnexported(linemetadata.Index{}), ) } func TestOverflowDown(t *testing.T) { pager := Pager{ screen: twin.NewFakeScreen( 10, // Longer than the raw line, we're testing vertical overflow, not horizontal 2, // Single line of contents + one status line ), // Single line of input readers: []*reader.ReaderImpl{reader.NewFromTextForTesting("test", "hej")}, // This value can be anything and should be clipped, that's what we're testing scrollPosition: *scrollPositionFromIndex("TestOverflowDown", linemetadata.IndexFromOneBased(42)), } pager.filteringReader = FilteringReader{ BackingReader: pager.readers[pager.currentReader], Filter: &pager.filter, } rendered := pager.renderLines() assert.Equal(t, len(rendered.lines), 1) assert.Equal(t, "hej", renderedToString(rendered.lines[0].cells)) assert.Equal(t, "test", rendered.filenameText) assert.Equal(t, ": 1 line 100%", rendered.statusText) assert.Assert(t, pager.lineIndex().IsZero()) assert.Equal(t, pager.deltaScreenLines(), 0) } func TestOverflowUp(t *testing.T) { pager := Pager{ screen: twin.NewFakeScreen( 10, // Longer than the raw line, we're testing vertical overflow, not horizontal 2, // Single line of contents + one status line ), // Single line of input readers: []*reader.ReaderImpl{reader.NewFromTextForTesting("test", "hej")}, // NOTE: scrollPosition intentionally not initialized } pager.filteringReader = FilteringReader{ BackingReader: pager.readers[pager.currentReader], Filter: &pager.filter, } rendered := pager.renderLines() assert.Equal(t, len(rendered.lines), 1) assert.Equal(t, "hej", renderedToString(rendered.lines[0].cells)) assert.Equal(t, "test", rendered.filenameText) assert.Equal(t, ": 1 line 100%", rendered.statusText) assert.Assert(t, pager.lineIndex().IsZero()) assert.Equal(t, pager.deltaScreenLines(), 0) } func TestWrapping(t *testing.T) { reader := reader.NewFromTextForTesting("", "first line\nline two will be wrapped\nhere's the last line") pager := NewPager(reader) pager.screen = twin.NewFakeScreen(40, 40) pager.WrapLongLines = true pager.ShowLineNumbers = false pager.showLineNumbers = false assert.NilError(t, reader.Wait()) // This is what we're testing really pager.scrollToEnd() // Higher than needed, we'll just be validating the necessary lines at the // top. screen := twin.NewFakeScreen(10, 99) // Exit immediately pager.Quit() // Get contents onto our fake screen pager.StartPaging(screen, nil, nil) pager.redraw("") actual := strings.Join([]string{ rowToString(screen.GetRow(0)), rowToString(screen.GetRow(1)), rowToString(screen.GetRow(2)), rowToString(screen.GetRow(3)), rowToString(screen.GetRow(4)), rowToString(screen.GetRow(5)), rowToString(screen.GetRow(6)), rowToString(screen.GetRow(7)), }, "\n") assert.Equal(t, actual, strings.Join([]string{ "first line", "line two", "will be", "wrapped", "here's the", "last line", "---", "", }, "\n")) } // Repro for https://github.com/walles/moor/issues/153 func TestOneLineTerminal(t *testing.T) { pager := Pager{ // Single line terminal window, this is what we're testing screen: twin.NewFakeScreen(20, 1), readers: []*reader.ReaderImpl{reader.NewFromTextForTesting("test", "hej")}, ShowStatusBar: true, } pager.filteringReader = FilteringReader{ BackingReader: pager.readers[pager.currentReader], Filter: &pager.filter, } rendered := pager.renderLines() assert.Equal(t, len(rendered.lines), 0) } // What happens if we are scrolled to the bottom of a 1000 lines file, and then // add a filter matching only the first line? // // What should happen is that we should go as far down as possible. func TestShortenedInput(t *testing.T) { pager := Pager{ screen: twin.NewFakeScreen(20, 10), // 1000 lines of input, we will scroll to the bottom readers: []*reader.ReaderImpl{reader.NewFromTextForTesting("test", "first\n"+strings.Repeat("line\n", 1000))}, scrollPosition: newScrollPosition("TestShortenedInput"), } // Hide the status bar for this test pager.mode = PagerModeViewing{&pager} pager.ShowStatusBar = false pager.filteringReader = FilteringReader{ BackingReader: pager.readers[pager.currentReader], Filter: &pager.filter, } pager.scrollToEnd() assert.Equal(t, pager.lineIndex().Index(), 991, "This should have been the effect of calling scrollToEnd()") pager.mode = NewPagerModeFilter(&pager) pager.filter = search.For("first") // Match only the first line rendered := pager.renderLines() assert.Equal(t, len(rendered.lines), 1, "Should have rendered one line") assert.Equal(t, "first", renderedToString(rendered.lines[0].cells)) assert.Equal(t, pager.lineIndex().Index(), 0, "Should have scrolled to the first line") } // - Start with a 1000 lines file // - Scroll to the bottom // - Add a filter matching the first 100 lines // - Render // - Verify that the 10 last matching lines were rendered func TestShortenedInputManyLines(t *testing.T) { lines := []string{"first"} for i := range 999 { if i < 100 { lines = append(lines, "match "+strconv.Itoa(i)) } else { lines = append(lines, "other "+strconv.Itoa(i)) } } pager := Pager{ screen: twin.NewFakeScreen(20, 10), readers: []*reader.ReaderImpl{reader.NewFromTextForTesting("test", strings.Join(lines, "\n"))}, scrollPosition: newScrollPosition("TestShortenedInputManyLines"), } pager.filteringReader = FilteringReader{ BackingReader: pager.readers[pager.currentReader], Filter: &pager.filter, } pager.scrollToEnd() assert.Equal(t, pager.lineIndex().Index(), 991, "Should be at the last line before filtering") pager.mode = NewPagerModeFilter(&pager) pager.filter = search.For(`^match`) rendered := pager.renderLines() assert.Equal(t, len(rendered.lines), 9, "Should have rendered 9 lines (10 minus one status bar)") expectedLines := []string{} for i := 91; i < 100; i++ { expectedLines = append(expectedLines, "match "+strconv.Itoa(i)) } for i, row := range rendered.lines { assert.Equal(t, renderedToString(row.cells), expectedLines[i], "Line %d mismatch", i) } assert.Equal(t, pager.lineIndex().Index(), 91, "The last lines should now be visible") assert.Equal(t, "match 99", renderedToString(rendered.lines[len(rendered.lines)-1].cells)) } // Text lines are fewer than the number of screen lines. All lines have search // hits. // // Expected: No search hit line highlighting, since highlighting all lines is // sort of like highlighting no lines but with an uglier background. func TestRenderLines_FewLinesAllWithSearchHits(t *testing.T) { testRenderLinesWithSearchHits(t, "1xxx", []twin.Color{twin.ColorDefault}, "TestRenderLines_FewLinesAllWithSearchHits", ) } // Text lines are more than the number of screen lines. All lines on screen have // search hits. // // Expected: No search hit line highlighting, since highlighting all lines is // sort of like highlighting no lines but with an uglier background. func TestRenderLines_ManyLinesAllWithSearchHits(t *testing.T) { testRenderLinesWithSearchHits(t, "1xxx\n2xxx\n3xxx\n4xxx", []twin.Color{twin.ColorDefault, twin.ColorDefault, twin.ColorDefault}, "TestRenderLines_ManyLinesAllWithSearchHits", ) } // Some lines with search hits and one line without. The ones with search hits // should be line highlighted. func TestRenderLines_OneLineWithoutSearchHit(t *testing.T) { testRenderLinesWithSearchHits(t, "1xxx\n2miss\n3xxx\n4xxx", []twin.Color{red, twin.ColorDefault, red}, "TestRenderLines_OneLineWithoutSearchHit", ) } var red = twin.NewColor24Bit(0xff, 0x00, 0x00) // Helper for search hit line highlight tests. // // Search pattern is always "xxx", because "x" marks the spot. The // expectedBackgrounds are the expected background colors for the first (always // unmatched) letter on each line. // // The screen being rendered to is always 10 columns wide and 3 rows high. func testRenderLinesWithSearchHits(t *testing.T, input string, expectedBackgrounds []twin.Color, scrollPositionName string) { r := reader.NewFromTextForTesting("test", input) pager := Pager{ screen: twin.NewFakeScreen(10, 3), readers: []*reader.ReaderImpl{r}, scrollPosition: newScrollPosition(scrollPositionName), } pager.filteringReader = FilteringReader{ BackingReader: pager.readers[pager.currentReader], Filter: &pager.filter, } pager.search = search.For("xxx") pager.ShowStatusBar = false pager.mode = PagerModeViewing{&pager} pager.showLineNumbers = false assert.NilError(t, r.Wait()) searchHitLineBackground = &red rendered := pager.renderLines() assert.Equal(t, len(rendered.lines), len(expectedBackgrounds)) for i := range expectedBackgrounds { assert.Equal(t, rendered.lines[i].cells[0].Style.Background(), expectedBackgrounds[i]) } } func BenchmarkRenderLines(b *testing.B) { input := reader.NewFromTextForTesting( "BenchmarkRenderLine()", strings.Repeat("This is a line with text and some more text to make it long enough.\n", 100)) pager := NewPager(input) pager.screen = twin.NewFakeScreen(80, 25) assert.NilError(b, input.Wait()) pager.renderLines() // Warm up b.ResetTimer() for i := 0; i < b.N; i++ { pager.renderLines() } } // Inspired by https://github.com/walles/moor/issues/358 func BenchmarkRenderHugeLine(b *testing.B) { const megabytes = 5 builder := strings.Builder{} for builder.Len() < megabytes*1024*1024 { builder.WriteString("Romani ite domum. ") } b.SetBytes(int64(builder.Len())) input := reader.NewFromTextForTesting( "BenchmarkRenderHugeLine()", builder.String()) pager := NewPager(input) pager.screen = twin.NewFakeScreen(80, 25) assert.NilError(b, input.Wait()) pager.renderLines() // Warm up b.ResetTimer() for i := 0; i < b.N; i++ { pager.renderLines() } } moor-2.10.3/internal/screenStyle.go000066400000000000000000000023501513574474500172330ustar00rootroot00000000000000package internal import ( "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/styles" "github.com/walles/moor/v2/twin" ) const defaultDarkTheme = "native" // I decided on a light theme by doing this, looking for black text on white, // and no background colors on strings (they look like error markers): // // rg -i 'Background.*bg:#ffffff' | rg -v '[^:]#[^0]' | sort | cut -d: -f1 | xargs rg --files-without-match '"LiteralString".*bg:' | xargs wc -l | sort -r // // Then I picked tango because it has a bright background, a dark foreground and // it looks OK on a white terminal with an unmodified color palette. const defaultLightTheme = "tango" // Checks the terminal background color and returns either a dark or light theme func GetStyleForScreen(screen twin.Screen) chroma.Style { bgColor := screen.TerminalBackground() if bgColor == nil { // Fall back to dark theme if we can't detect the background color return *styles.Get(defaultDarkTheme) } distanceToBlack := bgColor.Distance(twin.NewColor24Bit(0, 0, 0)) distanceToWhite := bgColor.Distance(twin.NewColor24Bit(255, 255, 255)) if distanceToBlack < distanceToWhite { return *styles.Get(defaultDarkTheme) } else { return *styles.Get(defaultLightTheme) } } moor-2.10.3/internal/scrollPosition.go000066400000000000000000000314011513574474500177550ustar00rootroot00000000000000package internal import ( "fmt" "github.com/walles/moor/v2/internal/linemetadata" ) // Please create using newScrollPosition(name) type scrollPosition struct { internalDontTouch scrollPositionInternal } func newScrollPosition(name string) scrollPosition { if len(name) == 0 { panic("Non-empty name required") } return scrollPosition{ internalDontTouch: scrollPositionInternal{ name: name, }, } } type scrollPositionInternal struct { // Index into the array of visible input lines, or nil if nothing has been // read yet or there are no lines. lineIndex *linemetadata.Index // Scroll this many screen lines before rendering. Can be negative. deltaScreenLines int name string canonicalizing bool canonical scrollPositionCanonical } // If any of these change, we have to recompute the scrollPositionInternal values type scrollPositionCanonical struct { width int // From pager height int // From pager showLineNumbers bool // From pager showStatusBar bool // From pager wrapLongLines bool // From pager pagerLineCount int // From pager.Reader().GetLineCount() lineIndex *linemetadata.Index // From scrollPositionInternal deltaScreenLines int // From scrollPositionInternal } func canonicalFromPager(pager *Pager) scrollPositionCanonical { width, _ := pager.screen.Size() height := pager.visibleHeight() return scrollPositionCanonical{ width: width, height: height, showLineNumbers: pager.showLineNumbers, showStatusBar: pager.ShowStatusBar, wrapLongLines: pager.WrapLongLines, pagerLineCount: pager.Reader().GetLineCount(), lineIndex: pager.scrollPosition.internalDontTouch.lineIndex, deltaScreenLines: pager.scrollPosition.internalDontTouch.deltaScreenLines, } } // Create a new position, scrolled towards the beginning of the file func (sp scrollPosition) PreviousLine(scrollDistance int) scrollPosition { return scrollPosition{ internalDontTouch: scrollPositionInternal{ name: sp.internalDontTouch.name, lineIndex: sp.internalDontTouch.lineIndex, deltaScreenLines: sp.internalDontTouch.deltaScreenLines - scrollDistance, }, } } // Create a new position, scrolled towards the end of the file func (sp scrollPosition) NextLine(scrollDistance int) scrollPosition { return scrollPosition{ internalDontTouch: scrollPositionInternal{ name: sp.internalDontTouch.name, lineIndex: sp.internalDontTouch.lineIndex, deltaScreenLines: sp.internalDontTouch.deltaScreenLines + scrollDistance, }, } } func (p *Pager) ScrollPositionsEqual(a, b scrollPosition) bool { a.internalDontTouch.canonicalize(p) b.internalDontTouch.canonicalize(p) if a.internalDontTouch.deltaScreenLines != b.internalDontTouch.deltaScreenLines { return false } if (a.internalDontTouch.lineIndex == nil) != (b.internalDontTouch.lineIndex == nil) { // One line index is nil, the other is not return false } if a.internalDontTouch.lineIndex != nil && b.internalDontTouch.lineIndex != nil { // Both line indexes are non-nil... if *a.internalDontTouch.lineIndex != *b.internalDontTouch.lineIndex { // ... but they point to different lines. return false } } return true } // Create a new position, scrolled to the given line number // //revive:disable-next-line:unexported-return func NewScrollPositionFromIndex(index linemetadata.Index, name string) scrollPosition { return scrollPosition{ internalDontTouch: scrollPositionInternal{ name: name, lineIndex: &index, deltaScreenLines: 0, }, } } // Move towards the top until deltaScreenLines is not negative any more func (si *scrollPositionInternal) handleNegativeDeltaScreenLines(pager *Pager) { for !si.lineIndex.IsZero() && si.deltaScreenLines < 0 { // Render the previous line previousLineIndex := si.lineIndex.NonWrappingAdd(-1) previousLine := pager.Reader().GetLine(previousLineIndex) previousSubLinesCount := 0 if previousLine != nil { previousSubLines := pager.renderLine(*previousLine, si.getMaxNumberPrefixLength(pager), true) previousSubLinesCount = len(previousSubLines) } // Adjust lineNumber and deltaScreenLines to move up into the previous // screen line si.lineIndex = &previousLineIndex si.deltaScreenLines += previousSubLinesCount } if si.lineIndex.IsZero() && si.deltaScreenLines <= 0 { // Can't go any higher si.deltaScreenLines = 0 return } } // Move towards the bottom until deltaScreenLines is within range of the // rendering of the current line. // // This method will not do any screen-height based clipping, so it could be that // the position is too far down to display after this returns. func (si *scrollPositionInternal) handlePositiveDeltaScreenLines(pager *Pager) { maxPrefixLength := si.getMaxNumberPrefixLength(pager) for { line := pager.Reader().GetLine(*si.lineIndex) if line == nil { // Out of bounds downwards, get the last line... si.lineIndex = linemetadata.IndexFromLength(pager.Reader().GetLineCount()) line = pager.Reader().GetLine(*si.lineIndex) if line == nil { panic(fmt.Errorf("Last line is nil")) } subLines := pager.renderLine(*line, maxPrefixLength, true) // ... and go to the bottom of that. si.deltaScreenLines = len(subLines) - 1 return } subLines := pager.renderLine(*line, maxPrefixLength, true) if si.deltaScreenLines < len(subLines) { // Sublines are within bounds! return } nextLineIndex := si.lineIndex.NonWrappingAdd(1) si.lineIndex = &nextLineIndex si.deltaScreenLines -= len(subLines) } } // This method assumes si contains a canonical position func (si *scrollPositionInternal) emptyBottomLinesCount(pager *Pager) int { unclaimedViewportLines := pager.visibleHeight() if unclaimedViewportLines == 0 { // No lines at all => no lines are empty. Happens (at least) during // testing. return 0 } if pager.Reader().GetLineCount() == 0 { // No lines available, so all viewport lines are unclaimed return unclaimedViewportLines } // Start counting where the current input line begins unclaimedViewportLines += si.deltaScreenLines lineIndex := *si.lineIndex lastLineNumberWidth := si.getMaxNumberPrefixLength(pager) for { line := pager.Reader().GetLine(lineIndex) if line == nil { // No more lines! break } subLines := pager.renderLine(*line, lastLineNumberWidth, true) unclaimedViewportLines -= len(subLines) if unclaimedViewportLines <= 0 { return 0 } // Move to the next line lineIndex = lineIndex.NonWrappingAdd(1) } return unclaimedViewportLines } func (si *scrollPositionInternal) isCanonical(pager *Pager) bool { if si.canonical.lineIndex == nil { // Awaiting initial lines from the reader return false } if si.canonical == canonicalFromPager(pager) { return true } return false } // Only to be called from the scrollPosition getters!! // // Canonicalize the scroll position vs the given pager. A canonical position can // just be displayed on screen, it has been clipped both towards the top and // bottom of the screen, taking into account the screen height. func (si *scrollPositionInternal) canonicalize(pager *Pager) { if si.isCanonical(pager) { return } if si.canonicalizing { panic(fmt.Errorf("Scroll position canonicalize() called recursively for %s", si.name)) } si.canonicalizing = true defer func() { si.canonical = canonicalFromPager(pager) si.canonicalizing = false }() if pager.Reader().GetLineCount() == 0 { si.lineIndex = nil si.deltaScreenLines = 0 return } if si.lineIndex == nil { // We have lines, but no line number, start at the top si.lineIndex = &linemetadata.Index{} } si.handleNegativeDeltaScreenLines(pager) si.handlePositiveDeltaScreenLines(pager) emptyBottomLinesCount := si.emptyBottomLinesCount(pager) if emptyBottomLinesCount > 0 { // First, adjust deltaScreenLines to get us to the top si.deltaScreenLines -= emptyBottomLinesCount // Then, actually go up that many lines si.handleNegativeDeltaScreenLines(pager) } } func scrollPositionFromIndex(name string, index linemetadata.Index) *scrollPosition { return &scrollPosition{ internalDontTouch: scrollPositionInternal{ name: name, lineIndex: &index, }, } } // Line index in the input stream, or nil if nothing has been read func (p *Pager) lineIndex() *linemetadata.Index { p.scrollPosition.internalDontTouch.canonicalize(p) return p.scrollPosition.internalDontTouch.lineIndex } // Line index in the input stream, or nil if nothing has been read func (sp *scrollPosition) lineIndex(pager *Pager) *linemetadata.Index { sp.internalDontTouch.canonicalize(pager) return sp.internalDontTouch.lineIndex } // Scroll this many screen lines before rendering // // Always >= 0. func (p *Pager) deltaScreenLines() int { p.scrollPosition.internalDontTouch.canonicalize(p) return p.scrollPosition.internalDontTouch.deltaScreenLines } func (p *Pager) scrollToEnd() { inputLineCount := p.Reader().GetLineCount() if inputLineCount == 0 { return } lastInputIndex := *linemetadata.IndexFromLength(inputLineCount) lastInputLine := p.Reader().GetLine(lastInputIndex) p.scrollPosition.internalDontTouch.lineIndex = &lastInputIndex // Scroll down enough. We know for sure the last line won't wrap into more // lines than the number of characters it contains. p.scrollPosition.internalDontTouch.deltaScreenLines = len(lastInputLine.Line.Plain(lastInputIndex)) if p.TargetLine == nil { // Start following the end of the file // // Otherwise, if we're already aiming for some place, don't overwrite // that. maxLineIndex := linemetadata.IndexMax() p.setTargetLine(&maxLineIndex) } } // Can be either because Pager.scrollToEnd() was just called or because the user // has pressed the down arrow enough times. func (p *Pager) isScrolledToEnd() bool { inputLineCount := p.Reader().GetLineCount() if inputLineCount == 0 { // No lines available, which means we can't scroll any further down return true } lastInputLineIndex := *linemetadata.IndexFromLength(inputLineCount) visibleLines := p.renderLines().lines if len(visibleLines) == 0 { // This can happen when terminal height is 1. In that case, the status // bar takes up that single line, leaving no room for contents. // // The actual return value here was picked based on that it prevents the // crash reported here: https://github.com/walles/moor/issues/378 return true } lastVisibleLine := visibleLines[len(visibleLines)-1] if lastVisibleLine.inputLineIndex != lastInputLineIndex { // Last input line is not on the screen return false } // Last line is on screen, now we need to figure out whether we can see all // of it lastInputLine := p.Reader().GetLine(lastInputLineIndex) lastInputLineRendered := p.renderLine(*lastInputLine, p.getLineNumberPrefixLength(lastInputLine.Number), true) lastRenderedSubLine := lastInputLineRendered[len(lastInputLineRendered)-1] // If the last visible subline is the same as the last possible subline then // we're at the bottom return lastVisibleLine.wrapIndex == lastRenderedSubLine.wrapIndex } // Returns nil if there are no lines func (p *Pager) getLastVisiblePosition() *scrollPosition { rendered := p.renderLines() if len(rendered.lines) == 0 { return nil } lastRenderedLine := rendered.lines[len(rendered.lines)-1] return &scrollPosition{ internalDontTouch: scrollPositionInternal{ name: "Last Visible Position", lineIndex: &lastRenderedLine.inputLineIndex, deltaScreenLines: lastRenderedLine.wrapIndex, }, } } func (si *scrollPositionInternal) getMaxNumberPrefixLength(pager *Pager) int { maxPossibleIndex := *linemetadata.IndexFromLength(pager.Reader().GetLineCount()) // This is an approximation assuming we don't do any wrapping. Finding the // real answer while wrapping requires rendering, which requires the real // answer and so on, so we do an approximation here to save us from // recursion. // // Let's improve on demand. var index linemetadata.Index // Ref: https://github.com/walles/moor/issues/198 if si.lineIndex != nil { index = *si.lineIndex } // Not going back when computing the max number prefix length prevents // https://github.com/walles/moor/issues/338. // // FIXME: Is there something better we should be doing instead? positiveDeltaScreenLines := si.deltaScreenLines if positiveDeltaScreenLines < 0 { positiveDeltaScreenLines = 0 } maxVisibleIndex := index.NonWrappingAdd( positiveDeltaScreenLines + pager.visibleHeight() - 1) if maxVisibleIndex.IsAfter(maxPossibleIndex) { maxVisibleIndex = maxPossibleIndex } var number linemetadata.Number lastVisibleLine := pager.Reader().GetLine(maxVisibleIndex) // nil can happen when the input stream is empty if lastVisibleLine != nil { number = lastVisibleLine.Number } // Count the length of the last line number return pager.getLineNumberPrefixLength(number) } moor-2.10.3/internal/scrollPosition_test.go000066400000000000000000000111351513574474500210160ustar00rootroot00000000000000package internal import ( "fmt" "strings" "testing" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) const screenHeight = 60 // Repro for: https://github.com/walles/moor/issues/166 func testCanonicalize1000(t *testing.T, withStatusBar bool, currentStartLine linemetadata.Index, lastVisibleLine linemetadata.Index) { pager := Pager{} pager.screen = twin.NewFakeScreen(100, screenHeight) pager.readers = []*reader.ReaderImpl{reader.NewFromTextForTesting("test", strings.Repeat("a\n", 2000))} pager.filteringReader = FilteringReader{ BackingReader: pager.readers[pager.currentReader], Filter: &pager.filter, } pager.ShowLineNumbers = true pager.showLineNumbers = true pager.ShowStatusBar = withStatusBar pager.scrollPosition = scrollPosition{ internalDontTouch: scrollPositionInternal{ lineIndex: ¤tStartLine, deltaScreenLines: 0, name: "findFirstHit", canonicalizing: false, }, } lastVisiblePosition := scrollPosition{ internalDontTouch: scrollPositionInternal{ lineIndex: &lastVisibleLine, deltaScreenLines: 0, name: "Last Visible Position", }, } assert.Equal(t, *lastVisiblePosition.lineIndex(&pager), lastVisibleLine) } func TestCanonicalize1000WithStatusBar(t *testing.T) { for startLine := 0; startLine < 1500; startLine++ { t.Run(fmt.Sprint("startLine=", startLine), func(t *testing.T) { testCanonicalize1000(t, true, linemetadata.IndexFromZeroBased(startLine), linemetadata.IndexFromZeroBased(startLine+screenHeight-2), ) }) } } func TestCanonicalize1000WithoutStatusBar(t *testing.T) { for startLine := 0; startLine < 1500; startLine++ { t.Run(fmt.Sprint("startLine=", startLine), func(t *testing.T) { testCanonicalize1000(t, true, linemetadata.IndexFromZeroBased(startLine), linemetadata.IndexFromZeroBased(startLine+screenHeight-1), ) }) } } // Try scrolling between two points, on a 80 x screenHeight screen with 1492 // lines of input. func tryScrollAmount(t *testing.T, scrollFrom linemetadata.Index, scrollDistance int) { // Create 1492 lines of single-char content pager := Pager{} pager.screen = twin.NewFakeScreen(80, screenHeight) pager.readers = []*reader.ReaderImpl{reader.NewFromTextForTesting("test", strings.Repeat("x\n", 1492))} pager.filteringReader = FilteringReader{ BackingReader: pager.readers[pager.currentReader], Filter: &pager.filter, } pager.ShowLineNumbers = true pager.showLineNumbers = true pager.scrollPosition = scrollPosition{ internalDontTouch: scrollPositionInternal{ name: "tryScrollAmount", lineIndex: &scrollFrom, deltaScreenLines: scrollDistance, }, } // Trigger rendering (and canonicalization). If the prefix is miscomputed // this would previously panic inside createLinePrefix(). rendered := pager.renderLines() // Sanity check the result assert.Assert(t, rendered.lines != nil) assert.Equal(t, len(rendered.lines), pager.visibleHeight()) assert.Equal(t, rendered.lines[0].inputLineIndex, scrollFrom.NonWrappingAdd(scrollDistance)) } // Repro for https://github.com/walles/moor/issues/313: Rapid scroll // (deltaScreenLines > 0) crossing from 3 to 4 digits must not panic due to // too-short number prefix length. func TestFastScrollAcross1000DoesNotPanic(t *testing.T) { tryScrollAmount(t, linemetadata.IndexFromZeroBased(900), 200) } // Repro for https://github.com/walles/moor/issues/338 func TestIssue338(t *testing.T) { tryScrollAmount(t, linemetadata.IndexFromZeroBased(1000), -60) } func TestMultipleScrollStartsAcross1000DoNotPanic(t *testing.T) { for scrollFrom := 1000 - screenHeight - 10; scrollFrom <= 1000; scrollFrom++ { tryScrollAmount(t, linemetadata.IndexFromZeroBased(scrollFrom), screenHeight) } } func TestMultipleScrollDistancesAcross1000DoNotPanic(t *testing.T) { scrollFrom := 1000 - screenHeight - 10 for scrollDistance := 0; scrollDistance <= 3*screenHeight; scrollDistance++ { tryScrollAmount(t, linemetadata.IndexFromZeroBased(scrollFrom), scrollDistance) } } func TestMultipleBackwardsScrollStartsAcross1000DoNotPanic(t *testing.T) { for scrollFrom := 1000 + screenHeight + 10; scrollFrom >= 1000; scrollFrom-- { tryScrollAmount(t, linemetadata.IndexFromZeroBased(scrollFrom), -screenHeight) } } func TestMultipleBackwardsScrollDistancesAcross1000DoNotPanic(t *testing.T) { scrollFrom := 1000 + screenHeight + 10 for scrollDistance := 0; scrollDistance <= 3*screenHeight; scrollDistance++ { tryScrollAmount(t, linemetadata.IndexFromZeroBased(scrollFrom), -scrollDistance) } } moor-2.10.3/internal/search-history.go000066400000000000000000000213041513574474500176770ustar00rootroot00000000000000package internal import ( "bufio" "errors" "fmt" "os" "path/filepath" "strings" "unicode" "github.com/adrg/xdg" log "github.com/sirupsen/logrus" ) type SearchHistory struct { // Empty means no history file. Set by BootSearchHistory(). absFileName string entries []string } /* Search history semantics: - On startup, load history or import from other pager - Any change to the input box resets the history index to "past the end" - If the user starts typing, then arrows up once and down once, whatever the user typed should still be there. - On importing from less, remove unprintable characters - On exiting search, no matter how, add a new entry at the end and deduplicate. Save to disk. */ const maxSearchHistoryEntries = 640 // This should be enough for anyone // A relative path or just a file name means relative to the user's home // directory. Empty means follow the XDG spec for data files. func BootSearchHistory(fileName string) SearchHistory { fileName = resolveHistoryFilePath(fileName) history, err := loadMoorSearchHistory(fileName) if err != nil { log.Infof("Could not load moor search history from %s: %v", fileName, err) // IO Error, give up return SearchHistory{} } if history != nil { log.Infof("Loaded %d search history entries from %s", len(history), fileName) return SearchHistory{ absFileName: fileName, entries: history, } } history, err = loadLessSearchHistory() if err != nil { log.Infof("Could not import less search history: %v", err) return SearchHistory{ absFileName: fileName, entries: []string{}, } } if history == nil { // Neither moor nor less history found, so we start a new history file // from scratch return SearchHistory{ absFileName: fileName, entries: []string{}, } } log.Infof("Imported %d search history entries from less", len(history)) return SearchHistory{ absFileName: fileName, entries: history, } } // Returns (nil, nil) if the file doesn't exist. Otherwise returns history slice // or error. func loadMoorSearchHistory(absHistoryFileName string) ([]string, error) { if absHistoryFileName == "" { // No history file return nil, nil } lines := []string{} err := iterateFileByLines(absHistoryFileName, func(line string) { if len(line) > 640 { // Line too long, 640 chars should be enough for anyone return } lines = append(lines, line) if len(lines) > maxSearchHistoryEntries { // Throw away the first (oldest) history line, we don't want more // than this lines = lines[1:] } }) if errors.Is(err, os.ErrNotExist) { // No history file found, not a problem but no history either, return // nil rather than empty slice return nil, nil } if err != nil { return nil, err } return removeDupsKeepingLast(lines), nil } // Empty file name will resolve to a default XDG friendly path. Absolute will be // left untouched. Relative will be interpreted relative to the user's home // directory. func resolveHistoryFilePath(fileName string) string { if fileName == "-" || fileName == "/dev/null" { // No history file return "" } if fileName == "" { xdgPath, err := xdg.DataFile("moor/search_history") if err != nil { log.Infof("Could not resolve XDG data file path for search history: %v", err) return "" } return xdgPath } if filepath.IsAbs(fileName) { return fileName } // Path relative to home directory home, err := os.UserHomeDir() if err != nil { log.Infof("Could not get user home dir to resolve history file path: %v", err) return "" } return filepath.Join(home, fileName) } // Return a new string with any unprintable characters removed func withoutUnprintables(s string) string { var builder strings.Builder for _, r := range s { if unicode.IsPrint(r) { builder.WriteRune(r) } } return builder.String() } // File format ref: https://unix.stackexchange.com/a/246641/384864 func loadLessSearchHistory() ([]string, error) { lessHistFileValue := os.Getenv("LESSHISTFILE") if lessHistFileValue == "/dev/null" { // No less history file return nil, nil } if lessHistFileValue == "-" { // No less history file return nil, nil } fileNames := []string{".lesshst", "_lesshst"} if lessHistFileValue != "" { // Check the user-specified file first fileNames = append([]string{lessHistFileValue}, fileNames...) } for _, fileName := range fileNames { lines := []string{} err := iterateFileByLines(fileName, func(line string) { if !strings.HasPrefix(line, "\"") { // Not a search history line return } if len(line) > 640 { // Line too long, 640 chars should be enough for anyone return } lines = append(lines, withoutUnprintables(line[1:])) // Strip leading " if len(lines) > maxSearchHistoryEntries { // Throw away the first (oldest) history line, we don't want more // than this lines = lines[1:] } }) if errors.Is(err, os.ErrNotExist) { // No such file, try next continue } if err != nil { return nil, err } return removeDupsKeepingLast(lines), nil } // No history files found, not a problem but no history either, return return nil, nil } // path can be relative or absolute, or just a single file name (also relative). // If path is relative, treat it as relative to the user's home directory func iterateFileByLines(path string, processLine func(string)) error { if !filepath.IsAbs(path) { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("could not get user home dir for iterating %s: %w", path, err) } path = filepath.Join(home, path) } f, err := os.Open(path) if err != nil { return fmt.Errorf("could not open %s for iteration: %w", path, err) } defer func() { err := f.Close() if err != nil { log.Warnf("closing %s failed when iterating: %v", path, err) } }() scanner := bufio.NewScanner(f) counter := 0 for scanner.Scan() { line := scanner.Text() if len(line) == 0 { continue } processLine(line) counter++ } if err := scanner.Err(); err != nil { return fmt.Errorf("scan %s: %w", path, err) } log.Debugf("%d lines of search history processed from %s", counter, path) return nil } // If there are duplicates, retain only the last of each func removeDupsKeepingLast(history []string) []string { if history == nil { return nil } seen := make(map[string]bool) cleaned := make([]string, 0, len(history)) cleanCount := 0 // Iterate backwards to keep the last occurrence for i := len(history) - 1; i >= 0; i-- { entry := history[i] if !seen[entry] { seen[entry] = true cleaned = append(cleaned, entry) } else { cleanCount++ } } // Reverse the cleaned slice to restore original order for i, j := 0, len(cleaned)-1; i < j; i, j = i+1, j-1 { cleaned[i], cleaned[j] = cleaned[j], cleaned[i] } log.Debugf("Removed %d redundant search history lines", cleanCount) return cleaned } func (h *SearchHistory) addEntry(entry string) { if entry == "" { return } if len(h.entries) > 0 && h.entries[len(h.entries)-1] == entry { // Same as last entry, do nothing return } // Append the new entry in-memory h.entries = removeDupsKeepingLast(append(h.entries, entry)) for len(h.entries) > maxSearchHistoryEntries { // Remove oldest entry h.entries = h.entries[1:] } if os.Getenv("LESSSECURE") == "1" { // LESSSECURE=1 means not writing anything to disk return } if h.absFileName == "" { // No history file configured return } // Write new file to a temp file and rename it into place tmpFilePath := h.absFileName + ".tmp" f, err := os.Create(tmpFilePath) if err != nil { log.Infof("Could not create temp history file %s: %v", tmpFilePath, err) return } // Prevent others from reading your history file. Best effort, if this // fails it fails. _ = f.Chmod(0o600) shouldRename := true defer func() { err := f.Close() if err != nil { // If close fails we don't really know what's in the temp file log.Infof("Could not close temp history file %s, giving up: %v", tmpFilePath, err) return } if shouldRename { // Rename temp file into place err = os.Rename(tmpFilePath, h.absFileName) if err != nil { log.Infof("Could not rename temp history file %s to %s: %v", tmpFilePath, h.absFileName, err) return } } else { // Remove temp file err = os.Remove(tmpFilePath) if err != nil { log.Infof("Could not remove temp history file %s: %v", tmpFilePath, err) } } }() writer := bufio.NewWriter(f) for _, line := range h.entries { _, err := writer.WriteString(line + "\n") if err != nil { log.Infof("Could not write to temp history file %s: %v", tmpFilePath, err) shouldRename = false return } } err = writer.Flush() if err != nil { log.Infof("Could not flush to temp history file %s: %v", tmpFilePath, err) shouldRename = false return } } moor-2.10.3/internal/search-line-cache.go000066400000000000000000000042171513574474500201720ustar00rootroot00000000000000package internal import ( "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/reader" ) // For small searches or few cores, search will be fast no matter what we put // here. For large searches on many-core systems, a larger cache will help // performance. But a larger cache without a performance increase has no value. // To evaluate: // // go test -run='^$' -bench 'Warm' ./internal // // Results from Johan's laptop. The numbers are the test iteration counts for // BenchmarkHighlightedWarmSearch and BenchmarkPlainTextWarmSearch. The // optimization has been done to improve the sum of these two benchmarks. // // 100: 741+726=1467 // 500: 832+882=1714 <-- Best // 1000: 754+810=1564 // 5000: 637+658=1295 const searchLineCacheSize = 500 type searchLineCache struct { lines []reader.NumberedLine } func (c *searchLineCache) GetLine(reader reader.Reader, index linemetadata.Index, direction SearchDirection) *reader.NumberedLine { // Do we have a cache hit? cacheHit := c.getLineFromCache(index) if cacheHit != nil { return cacheHit } // Cache miss, load new lines firstIndexToRequest := index if direction == SearchDirectionBackward { // Let's say we want index 10 to be in the cache. Cache size is 5. // Then, the first index must be 6, so that we get 6,7,8,9,10. // Or in other words, 10 - 5 + 1 = 6. firstIndexToRequest = index.NonWrappingAdd(-searchLineCacheSize + 1) } reader.GetLinesPreallocated(firstIndexToRequest, &c.lines) // Get the line from the cache return c.getLineFromCache(index) } // Or nil if that line isn't in the cache func (c *searchLineCache) getLineFromCache(index linemetadata.Index) *reader.NumberedLine { if cap(c.lines) != searchLineCacheSize { // Initialize cache c.lines = make([]reader.NumberedLine, 0, searchLineCacheSize) } if len(c.lines) == 0 { return nil } firstCachedIndexInclusive := c.lines[0].Index if index.IsBefore(firstCachedIndexInclusive) { return nil } lastCachedIndexInclusive := c.lines[len(c.lines)-1].Index if index.IsAfter(lastCachedIndexInclusive) { return nil } return &c.lines[index.Index()-firstCachedIndexInclusive.Index()] } moor-2.10.3/internal/search-linescanner.go000066400000000000000000000116141513574474500205020ustar00rootroot00000000000000// This file contains code for scanning input lines for search hits. package internal import ( "fmt" "runtime" "runtime/debug" "time" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/internal/search" ) // Search input lines. Not screen lines! // // The `beforePosition` parameter is exclusive, meaning that line will not be // searched. // // For the actual searching, this method will call _findFirstHit() in parallel // on multiple cores, to help large file search performance. func FindFirstHit(reader reader.Reader, search search.Search, startPosition linemetadata.Index, beforePosition *linemetadata.Index, direction SearchDirection) *linemetadata.Index { // If the number of lines to search matches the number of cores (or more), // divide the search into chunks. Otherwise use one chunk. chunkCount := runtime.NumCPU() var linesCount int if direction == SearchDirectionBackward { // If the startPosition is zero, that should make the count one linesCount = startPosition.Index() + 1 if beforePosition != nil { // Searching from 1 with before set to 0 should make the count 1 linesCount = startPosition.Index() - beforePosition.Index() } } else { linesCount = reader.GetLineCount() - startPosition.Index() if beforePosition != nil { // Searching from 1 with before set to 2 should make the count 1 linesCount = beforePosition.Index() - startPosition.Index() } } if linesCount < chunkCount { chunkCount = 1 } chunkSize := linesCount / chunkCount log.Debugf("Searching %d lines across %d cores with %d lines per core...", linesCount, chunkCount, chunkSize) t0 := time.Now() defer func() { dt := time.Since(t0) linesPerSecond := float64(linesCount) / dt.Seconds() linesPerSecondS := fmt.Sprintf("%.0f", linesPerSecond) if linesPerSecond > 7_000_000_000.0 { linesPerSecondS = fmt.Sprintf("%.0fG", linesPerSecond/1000_000_000.0) } else if linesPerSecond > 7_000_000.0 { linesPerSecondS = fmt.Sprintf("%.0fM", linesPerSecond/1000_000.0) } else if linesPerSecond > 7_000.0 { linesPerSecondS = fmt.Sprintf("%.0fk", linesPerSecond/1000.0) } if linesCount > 0 { log.Debugf("Searched %d lines in %s at %slines/s or %s/line", linesCount, dt, linesPerSecondS, (dt / time.Duration(linesCount)).String()) } else { log.Debugf("Searched %d lines in %s at %slines/s", linesCount, dt, linesPerSecondS) } }() // Each parallel search will start at one of these positions searchStarts := make([]linemetadata.Index, chunkCount) directionSign := 1 if direction == SearchDirectionBackward { directionSign = -1 } for i := 0; i < chunkCount; i++ { searchStarts[i] = startPosition.NonWrappingAdd(i * directionSign * chunkSize) } // Make a results array, with one result per chunk findings := make([]chan *linemetadata.Index, chunkCount) // Search all chunks in parallel for i, searchStart := range searchStarts { findings[i] = make(chan *linemetadata.Index) searchEndIndex := i + 1 var chunkBefore *linemetadata.Index if searchEndIndex < len(searchStarts) { chunkBefore = &searchStarts[searchEndIndex] } else if beforePosition != nil { chunkBefore = beforePosition } go func(i int, searchStart linemetadata.Index, chunkBefore *linemetadata.Index) { defer func() { PanicHandler("findFirstHit()/chunkSearch", recover(), debug.Stack()) }() findings[i] <- _findFirstHit(reader, searchStart, search, chunkBefore, direction) }(i, searchStart, chunkBefore) } // Return the first non-nil result for _, finding := range findings { result := <-finding if result != nil { return result } } return nil } // NOTE: When we search, we do that by looping over the *input lines*, not the // screen lines. That's why startPosition is an Index rather than a // scrollPosition. // // The `beforePosition` parameter is exclusive, meaning that line will not be // searched. // // This method will run over multiple chunks of the input file in parallel to // help large file search performance. func _findFirstHit(reader reader.Reader, startPosition linemetadata.Index, search search.Search, beforePosition *linemetadata.Index, direction SearchDirection) *linemetadata.Index { searchPosition := startPosition lineCache := searchLineCache{} for { line := lineCache.GetLine(reader, searchPosition, direction) if line == nil { // No match, give up return nil } lineText := line.Plain() if search.Matches(lineText) { return &searchPosition } if direction == SearchDirectionForward { searchPosition = searchPosition.NonWrappingAdd(1) } else { if (searchPosition == linemetadata.Index{}) { // Reached the top without any match, give up return nil } searchPosition = searchPosition.NonWrappingAdd(-1) } if beforePosition != nil && searchPosition == *beforePosition { // No match, give up return nil } } } moor-2.10.3/internal/search-linescanner_test.go000066400000000000000000000123021513574474500215340ustar00rootroot00000000000000package internal import ( "fmt" "os" "runtime" "strings" "testing" "github.com/alecthomas/chroma/v2/formatters" "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/internal/search" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" log "github.com/sirupsen/logrus" ) func TestFindFirstHitSimple(t *testing.T) { reader := reader.NewFromTextForTesting("TestFindFirstHitSimple", "AB") assert.NilError(t, reader.Wait()) hit := FindFirstHit(reader, search.For("AB"), linemetadata.Index{}, nil, SearchDirectionForward) assert.Assert(t, hit.IsZero()) } func TestFindFirstHitAnsi(t *testing.T) { reader := reader.NewFromTextForTesting("", "A\x1b[30mB") assert.NilError(t, reader.Wait()) hit := FindFirstHit(reader, search.For("AB"), linemetadata.Index{}, nil, SearchDirectionForward) assert.Assert(t, hit.IsZero()) } func TestFindFirstHitNoMatch(t *testing.T) { reader := reader.NewFromTextForTesting("TestFindFirstHitSimple", "AB") assert.NilError(t, reader.Wait()) hit := FindFirstHit(reader, search.For("this pattern should not be found"), linemetadata.Index{}, nil, SearchDirectionForward) assert.Assert(t, hit == nil) } func TestFindFirstHitNoMatchBackwards(t *testing.T) { reader := reader.NewFromTextForTesting("TestFindFirstHitSimple", "AB") assert.NilError(t, reader.Wait()) theEnd := *linemetadata.IndexFromLength(reader.GetLineCount()) hit := FindFirstHit(reader, search.For("this pattern should not be found"), theEnd, nil, SearchDirectionBackward) assert.Assert(t, hit == nil) } // Converts a cell row to a plain string and removes trailing whitespace. func rowToString(row []twin.StyledRune) string { rowString := "" for _, cell := range row { rowString += string(cell.Rune) } return strings.TrimRight(rowString, " ") } func benchmarkSearch(b *testing.B, highlighted bool, warm bool) { log.SetLevel(log.WarnLevel) // Stop info logs from polluting benchmark output // Pick a go file so we get something with highlighting _, sourceFilename, _, ok := runtime.Caller(0) if !ok { panic("Getting current filename failed") } sourceBytes, err := os.ReadFile(sourceFilename) assert.NilError(b, err) fileContents := string(sourceBytes) // Repeat input enough times to get to some target size, before highlighting // to get the same amount of text in either case replications := 5_000_000 / len(fileContents) if highlighted { highlightedSourceCode, err := reader.Highlight(fileContents, *styles.Get("native"), formatters.TTY16m, lexers.Get("go")) assert.NilError(b, err) if highlightedSourceCode == nil { panic("Highlighting didn't want to, returned nil") } fileContents = *highlightedSourceCode } if !warm { // This makes the ns/op benchmark numbers more comparable between plain // and highlighted in the cold case. replications = 5_000_000 / len(fileContents) } // Create some input to search. Use a Builder to avoid quadratic string concatenation time. var builder strings.Builder builder.Grow(len(fileContents) * replications) for range replications { builder.WriteString(fileContents) } testString := builder.String() benchMe := reader.NewFromTextForTesting("hello", testString) assert.NilError(b, benchMe.Wait()) // The target string is split to not match, remember we're searching through // this very file. Same string as in BenchmarkCaseInsensitiveSubstringMatch // in search/search_test.go. // // NOTE: The search term is all lowercase to trigger case-insensitive // search. I believe this is the most common case, so that's what we should // benchmark. search := search.For("this won't match " + "anything") reader.DisablePlainCachingForBenchmarking = !warm if warm { // Warm up any caches etc by doing one search before we start measuring hit := FindFirstHit(benchMe, search, linemetadata.Index{}, nil, SearchDirectionForward) if hit != nil { panic(fmt.Errorf("This test is meant to scan the whole file without finding anything")) } } // I hope forcing a GC here will make numbers more predictable runtime.GC() b.SetBytes(int64(len(testString))) b.ResetTimer() for range b.N { // This test will search through all the N copies we made of our file hit := FindFirstHit(benchMe, search, linemetadata.Index{}, nil, SearchDirectionForward) if hit != nil { panic(fmt.Errorf("This test is meant to scan the whole file without finding anything")) } } } // How long does it take to search a highlighted file for some regex the first time? // // Run with: go test -run='^$' -bench=. . ./... func BenchmarkHighlightedColdSearch(b *testing.B) { benchmarkSearch(b, true, false) } // How long does it take to search a plain text file for some regex the first time? // // Search performance was a problem for me when I had a 600MB file to search in. // // Run with: go test -run='^$' -bench=. . ./... func BenchmarkPlainTextColdSearch(b *testing.B) { benchmarkSearch(b, false, false) } // How long does it take to search a plain text file for some regex the second time? // // Run with: go test -run='^$' -bench=. . ./... func BenchmarkPlainTextWarmSearch(b *testing.B) { benchmarkSearch(b, false, true) } moor-2.10.3/internal/search/000077500000000000000000000000001513574474500156515ustar00rootroot00000000000000moor-2.10.3/internal/search/matchRanges.go000066400000000000000000000010551513574474500204350ustar00rootroot00000000000000package search // MatchRanges collects match indices type MatchRanges struct { Matches [][2]int } // InRange says true if the index is part of a regexp match func (mr *MatchRanges) InRange(index int) bool { if mr == nil { return false } for _, match := range mr.Matches { matchFirstIndex := match[0] matchLastIndex := match[1] - 1 if index < matchFirstIndex { continue } if index > matchLastIndex { continue } return true } return false } func (mr *MatchRanges) Empty() bool { return mr == nil || len(mr.Matches) == 0 } moor-2.10.3/internal/search/matchRanges_test.go000066400000000000000000000100551513574474500214740ustar00rootroot00000000000000package search import ( "testing" "gotest.tools/v3/assert" ) // This is really a constant, don't change it! var _TestString = "mamma" func TestGetMatchRanges(t *testing.T) { matchRanges := For("m+").GetMatchRanges(_TestString) assert.Equal(t, len(matchRanges.Matches), 2) // Two matches assert.DeepEqual(t, matchRanges.Matches[0][0], 0) // First match starts at 0 assert.DeepEqual(t, matchRanges.Matches[0][1], 1) // And ends on 1 exclusive assert.DeepEqual(t, matchRanges.Matches[1][0], 2) // Second match starts at 2 assert.DeepEqual(t, matchRanges.Matches[1][1], 4) // And ends on 4 exclusive } func TestGetMatchRangesNilPattern(t *testing.T) { matchRanges := For("").GetMatchRanges(_TestString) assert.Assert(t, matchRanges == nil) assert.Assert(t, !matchRanges.InRange(0)) } func TestInRange(t *testing.T) { // Should match the one in TestGetMatchRanges() matchRanges := For("m+").GetMatchRanges(_TestString) assert.Assert(t, !matchRanges.InRange(-1)) // Before start assert.Assert(t, matchRanges.InRange(0)) // m assert.Assert(t, !matchRanges.InRange(1)) // a assert.Assert(t, matchRanges.InRange(2)) // m assert.Assert(t, matchRanges.InRange(3)) // m assert.Assert(t, !matchRanges.InRange(4)) // a assert.Assert(t, !matchRanges.InRange(5)) // After end } func TestUtf8(t *testing.T) { // This test verifies that the match ranges are by rune rather than by byte unicodes := "-ä-ä-" matchRanges := For("ä").GetMatchRanges(unicodes) assert.Assert(t, !matchRanges.InRange(0)) // - assert.Assert(t, matchRanges.InRange(1)) // ä assert.Assert(t, !matchRanges.InRange(2)) // - assert.Assert(t, matchRanges.InRange(3)) // ä assert.Assert(t, !matchRanges.InRange(4)) // - } func TestNoMatch(t *testing.T) { // This test verifies that the match ranges are by rune rather than by byte unicodes := "gris" matchRanges := For("apa").GetMatchRanges(unicodes) assert.Assert(t, !matchRanges.InRange(0)) assert.Assert(t, !matchRanges.InRange(1)) assert.Assert(t, !matchRanges.InRange(2)) assert.Assert(t, !matchRanges.InRange(3)) assert.Assert(t, !matchRanges.InRange(4)) } func TestEndMatch(t *testing.T) { // This test verifies that the match ranges are by rune rather than by byte unicodes := "-ä" matchRanges := For("ä").GetMatchRanges(unicodes) assert.Assert(t, !matchRanges.InRange(0)) // - assert.Assert(t, matchRanges.InRange(1)) // ä assert.Assert(t, !matchRanges.InRange(2)) // Past the end } func TestRealWorldBug(t *testing.T) { // Verify a real world bug found in v1.9.8 testString := "anna" matchRanges := For("n").GetMatchRanges(testString) assert.Equal(t, len(matchRanges.Matches), 2) // Two matches assert.DeepEqual(t, matchRanges.Matches[0][0], 1) // First match starts at 1 assert.DeepEqual(t, matchRanges.Matches[0][1], 2) // And ends on 2 exclusive assert.DeepEqual(t, matchRanges.Matches[1][0], 2) // Second match starts at 2 assert.DeepEqual(t, matchRanges.Matches[1][1], 3) // And ends on 3 exclusive } func TestMatchRanges_CaseSensitiveRegex(t *testing.T) { matchRanges := For("G.*S").GetMatchRanges("GRIIIS") assert.Assert(t, len(matchRanges.Matches) > 0) matchRangesLower := For("G.*S").GetMatchRanges("griiis") assert.Assert(t, matchRangesLower == nil || len(matchRangesLower.Matches) == 0) } func TestMatchRanges_CaseInsensitiveRegex(t *testing.T) { testString := "GRIIIS" matchRanges := For("g.*s").GetMatchRanges(testString) assert.Assert(t, len(matchRanges.Matches) > 0) } func TestMatchRanges_CaseSensitiveSubstring(t *testing.T) { matchRanges := For(")G").GetMatchRanges(")G") assert.Assert(t, len(matchRanges.Matches) == 1) matchRangesLower := For(")G").GetMatchRanges(")g") assert.Assert(t, matchRangesLower == nil || len(matchRangesLower.Matches) == 0) } func TestMatchRanges_CaseInsensitiveSubstring(t *testing.T) { testString := ")G" matchRanges := For(")g").GetMatchRanges(testString) assert.Assert(t, len(matchRanges.Matches) == 1) } func TestMatchRanges_EmptyPattern(t *testing.T) { testString := "anything" matchRanges := For("").GetMatchRanges(testString) assert.Assert(t, matchRanges == nil) } moor-2.10.3/internal/search/search.go000066400000000000000000000067661513574474500174640ustar00rootroot00000000000000package search import ( "regexp" "strings" "unicode" "github.com/charlievieth/strcase" ) type Search struct { findMe string // If this is false it means the input has to be interpreted as a regexp. isSubstringSearch bool hasUppercase bool pattern *regexp.Regexp } func (search Search) Equals(other Search) bool { return search.findMe == other.findMe } func (search Search) String() string { return search.findMe } func For(s string) Search { search := Search{} search.For(s) return search } func (search *Search) For(s string) *Search { search.findMe = s if s == "" { // No search search.pattern = nil return search } var err error hasSpecialChars := regexp.QuoteMeta(s) != s search.pattern, err = regexp.Compile(s) isValidRegexp := err == nil regexpMatchingRequired := hasSpecialChars && isValidRegexp search.isSubstringSearch = !regexpMatchingRequired search.hasUppercase = false for _, char := range s { if unicode.IsUpper(char) { search.hasUppercase = true break } } if search.isSubstringSearch { // Pattern still needed for GetMatchRanges() search.pattern, err = regexp.Compile(regexp.QuoteMeta(s)) if err != nil { panic(err) } return search } // At this point we know it's a valid regexp, and that it does include // regexp specific characters. We also know the pattern has been // successfully compiled. return search } func (search *Search) Clear() { search.findMe = "" search.pattern = nil } func (search Search) Active() bool { return search.findMe != "" } func (search Search) Inactive() bool { return search.findMe == "" } func (search Search) Matches(line string) bool { if search.findMe == "" { return false } if search.isSubstringSearch && search.hasUppercase { // Case sensitive substring search return strings.Contains(line, search.findMe) } if search.isSubstringSearch && !search.hasUppercase { // Case insensitive substring search return strcase.Contains(line, search.findMe) } // Regexp search if !search.hasUppercase { // Regexp is already lowercase, do the same to the line to make the // search case insensitive line = strings.ToLower(line) } return search.pattern.MatchString(line) } // getMatchRanges locates one or more regexp matches in a string func (search Search) GetMatchRanges(String string) *MatchRanges { if search.Inactive() { return nil } if !search.hasUppercase { // Case insensitive search, lowercase the string. The pattern is already // lowercase whenever hasUppercase is false. String = strings.ToLower(String) } return &MatchRanges{ Matches: toRunePositions(search.pattern.FindAllStringIndex(String, -1), String), } } // Convert byte indices to rune indices func toRunePositions(byteIndices [][]int, matchedString string) [][2]int { var returnMe [][2]int if len(byteIndices) == 0 { // Nothing to see here, move along return returnMe } runeIndex := 0 byteIndicesToRuneIndices := make(map[int]int, 0) for byteIndex := range matchedString { byteIndicesToRuneIndices[byteIndex] = runeIndex runeIndex++ } // If a match touches the end of the string, that will be encoded as one // byte past the end of the string. Therefore we must add a mapping for // first-index-after-the-end. byteIndicesToRuneIndices[len(matchedString)] = runeIndex for _, bytePair := range byteIndices { fromRuneIndex := byteIndicesToRuneIndices[bytePair[0]] toRuneIndex := byteIndicesToRuneIndices[bytePair[1]] returnMe = append(returnMe, [2]int{fromRuneIndex, toRuneIndex}) } return returnMe } moor-2.10.3/internal/search/search_test.go000066400000000000000000000031111513574474500205000ustar00rootroot00000000000000package search import ( "os" "strings" "testing" "gotest.tools/v3/assert" ) func TestSearchForMatch(t *testing.T) { assert.Assert(t, For("").pattern == nil) // Test regexp matching assert.Assert(t, For("G.*S").Matches("GRIIIS")) assert.Assert(t, !For("G.*S").Matches("gRIIIS")) // Test case insensitive regexp matching assert.Assert(t, For("g.*s").Matches("GRIIIS")) assert.Assert(t, For("g.*s").Matches("gRIIIS")) // Test non-regexp matching assert.Assert(t, For(")G").Matches(")G")) assert.Assert(t, !For(")G").Matches(")g")) // Test case insensitive non-regexp matching assert.Assert(t, For(")g").Matches(")G")) assert.Assert(t, For(")g").Matches(")g")) } func benchmarkMatch(b *testing.B, searchTerm string) { sourceBytes, err := os.ReadFile("../../sample-files/large-git-log-patch-no-color.txt") assert.NilError(b, err) fileContents := string(sourceBytes) b.SetBytes(int64(len(fileContents))) lines := strings.Split(fileContents, "\n") search := For(searchTerm) b.ResetTimer() for i := 0; i < b.N; i++ { for _, line := range lines { search.Matches(line) } } } func BenchmarkCaseSensitiveSubstringMatch(b *testing.B) { benchmarkMatch(b, "This won't match anything") } func BenchmarkCaseInsensitiveSubstringMatch(b *testing.B) { // Same as in benchmarkSearch() in search-linescanner_test.go benchmarkMatch(b, "this won't match anything") } func BenchmarkCaseSensitiveRegexMatch(b *testing.B) { benchmarkMatch(b, "This [w]on't match anything") } func BenchmarkCaseInsensitiveRegexMatch(b *testing.B) { benchmarkMatch(b, "this [w]on't match anything") } moor-2.10.3/internal/styling.go000066400000000000000000000222631513574474500164310ustar00rootroot00000000000000package internal import ( "fmt" "os" "strings" "github.com/alecthomas/chroma/v2" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/textstyles" "github.com/walles/moor/v2/twin" ) // From LESS_TERMCAP_so, overrides statusbarStyle from the Chroma style if set var standoutStyle *twin.Style var lineNumbersStyle = twin.StyleDefault.WithAttr(twin.AttrDim) // Status bar and EOF marker style var statusbarStyle = twin.StyleDefault.WithAttr(twin.AttrReverse) var statusbarFileStyle = twin.StyleDefault.WithAttr(twin.AttrReverse).WithAttr(twin.AttrUnderline) var plainTextStyle = twin.StyleDefault var searchHitStyle = twin.StyleDefault.WithAttr(twin.AttrReverse) // This can be nil var searchHitLineBackground *twin.Color func setStyle(updateMe *twin.Style, envVarName string, fallback *twin.Style) { envValue := os.Getenv(envVarName) if envValue == "" { if fallback != nil { *updateMe = *fallback } return } style, err := TermcapToStyle(envValue) if err != nil { log.Info("Ignoring invalid ", envVarName, ": ", strings.ReplaceAll(envValue, "\x1b", "ESC"), ": ", err) return } *updateMe = style } // With exact set, only return a style if the Chroma formatter has an explicit // configuration for that style. Otherwise, we might return fallback styles, not // exactly matching what you requested. func twinStyleFromChroma(terminalBackground *twin.Color, chromaStyle *chroma.Style, chromaFormatter *chroma.Formatter, chromaToken chroma.TokenType, exact bool) *twin.Style { if chromaStyle == nil || chromaFormatter == nil { return nil } stringBuilder := strings.Builder{} err := (*chromaFormatter).Format(&stringBuilder, chromaStyle, chroma.Literator(chroma.Token{ Type: chromaToken, Value: "X", })) if err != nil { panic(err) } formatted := stringBuilder.String() cells := textstyles.StyledRunesFromString(twin.StyleDefault, formatted, nil, 0).StyledRunes if len(cells) != 1 { log.Warnf("Chroma formatter didn't return exactly one cell: %#v", cells) return nil } inexactStyle := cells[0].Style if inexactStyle.Background() == twin.ColorDefault && terminalBackground != nil { // Real colors can be mixed & matched, which we do in // Line.HighlightedTokens(). So we prefer real colors when we have them. inexactStyle = inexactStyle.WithBackground(*terminalBackground) } if !exact { return &inexactStyle } unstyled := twinStyleFromChroma(terminalBackground, chromaStyle, chromaFormatter, chroma.None, false) if unstyled == nil { panic("Chroma formatter didn't return a style for chroma.None") } if inexactStyle != *unstyled { // We got something other than the style of None, return it! return &inexactStyle } return nil } // consumeLessTermcapEnvs parses LESS_TERMCAP_xx environment variables and // adapts the moor output accordingly. func consumeLessTermcapEnvs(terminalBackground *twin.Color, chromaStyle *chroma.Style, chromaFormatter *chroma.Formatter) { // Requested here: https://github.com/walles/moor/issues/14 setStyle( &textstyles.ManPageBold, "LESS_TERMCAP_md", twinStyleFromChroma(terminalBackground, chromaStyle, chromaFormatter, chroma.GenericStrong, false), ) setStyle(&textstyles.ManPageUnderline, "LESS_TERMCAP_us", twinStyleFromChroma(terminalBackground, chromaStyle, chromaFormatter, chroma.GenericUnderline, false), ) // Since standoutStyle defaults to nil we can't just pass it to setStyle(). // Instead we give it special treatment here and set it only if its // environment variable is set. // // Ref: https://github.com/walles/moor/issues/171 envValue := os.Getenv("LESS_TERMCAP_so") if envValue != "" { style, err := TermcapToStyle(envValue) if err == nil { log.Trace("Standout style set from LESS_TERMCAP_so: ", style) standoutStyle = &style } else { log.Info("Ignoring invalid LESS_TERMCAP_so: ", strings.ReplaceAll(envValue, "\x1b", "ESC"), ": ", err) } } } func getOppositeColor(base twin.Color) twin.Color { if base == twin.ColorDefault { panic("can't get opposite of default color") } white := twin.NewColor24Bit(255, 255, 255) black := twin.NewColor24Bit(0, 0, 0) if base.Distance(white) > base.Distance(black) { // Foreground is far away from white, so pretend the background is white return white } else { // Foreground is far away from black, so pretend the background is black return black } } func styleUI(terminalBackground *twin.Color, chromaStyle *chroma.Style, chromaFormatter *chroma.Formatter, statusbarOption StatusBarOption, withTerminalFg bool, configureSearchHitLineBackground bool) { // Set defaults plainTextStyle = twin.StyleDefault textstyles.ManPageHeading = twin.StyleDefault.WithAttr(twin.AttrBold) lineNumbersStyle = twin.StyleDefault.WithAttr(twin.AttrDim) if chromaStyle == nil || chromaFormatter == nil { return } headingStyle := twinStyleFromChroma(terminalBackground, chromaStyle, chromaFormatter, chroma.GenericHeading, true) if headingStyle != nil && !withTerminalFg { log.Trace("Heading style set from Chroma: ", *headingStyle) textstyles.ManPageHeading = *headingStyle } chromaLineNumbers := twinStyleFromChroma(terminalBackground, chromaStyle, chromaFormatter, chroma.LineNumbers, true) if chromaLineNumbers != nil && !withTerminalFg { // NOTE: We used to dim line numbers here, but Johan found them too hard // to read. If line numbers should look some other way for some Chroma // style, go fix that in Chroma! log.Trace("Line numbers style set from Chroma: ", *chromaLineNumbers) lineNumbersStyle = *chromaLineNumbers } plainText := twinStyleFromChroma(terminalBackground, chromaStyle, chromaFormatter, chroma.None, false) if plainText != nil && !withTerminalFg { log.Trace("Plain text style set from Chroma: ", *plainText) plainTextStyle = *plainText } if standoutStyle != nil { log.Trace("Status bar style set from standout style: ", *standoutStyle) statusbarStyle = *standoutStyle } else if statusbarOption == STATUSBAR_STYLE_INVERSE { statusbarStyle = plainTextStyle.WithAttr(twin.AttrReverse) } else if statusbarOption == STATUSBAR_STYLE_PLAIN { plain := twinStyleFromChroma(terminalBackground, chromaStyle, chromaFormatter, chroma.None, false) if plain != nil { statusbarStyle = *plain } else { statusbarStyle = twin.StyleDefault } } else if statusbarOption == STATUSBAR_STYLE_BOLD { bold := twinStyleFromChroma(terminalBackground, chromaStyle, chromaFormatter, chroma.GenericStrong, true) if bold != nil { statusbarStyle = *bold } else { statusbarStyle = twin.StyleDefault.WithAttr(twin.AttrBold) } } else { panic(fmt.Sprint("Unrecognized status bar style: ", statusbarOption)) } statusbarFileStyle = statusbarStyle.WithAttr(twin.AttrUnderline) configureHighlighting(terminalBackground, configureSearchHitLineBackground) } // Expects to be called from the end of styleUI(), since at that // point we should have all data we need to set up highlighting. func configureHighlighting(terminalBackground *twin.Color, configureSearchHitLineBackground bool) { if standoutStyle != nil { searchHitStyle = *standoutStyle log.Trace("Search hit style set from standout style: ", searchHitStyle) } else { log.Trace("Search hit style set to default: ", searchHitStyle) } // // Everything below this point relates to figuring out which background // color we should use for lines with search hits. // if !configureSearchHitLineBackground { log.Trace("Not configuring search hit line background color") return } var plainBg twin.Color if terminalBackground != nil { plainBg = *terminalBackground } else if plainTextStyle.HasAttr(twin.AttrReverse) { plainBg = plainTextStyle.Foreground() } else { plainBg = plainTextStyle.Background() } hitBg := searchHitStyle.Background() hitFg := searchHitStyle.Foreground() if searchHitStyle.HasAttr(twin.AttrReverse) { hitBg = searchHitStyle.Foreground() hitFg = searchHitStyle.Background() } if hitBg == twin.ColorDefault && hitFg != twin.ColorDefault { // Not knowing the hit background color will be a problem further down // when we want to create a line background color for lines with search // hits. // // But since we know the foreground color, we can cheat and pretend the // background is as far away from the foreground as possible. hitBg = getOppositeColor(hitFg) } if hitBg == twin.ColorDefault && terminalBackground != nil { // Assume the hit background is the opposite of the terminal background hitBg = getOppositeColor(*terminalBackground) } if plainBg != twin.ColorDefault && hitBg != twin.ColorDefault { // We have two real colors. Mix them! I got to "0.2" by testing some // numbers. 0.2 is visible but not too strong. mixed := plainBg.Mix(hitBg, 0.2) searchHitLineBackground = &mixed log.Trace("Search hit line background set to mixed color: ", *searchHitLineBackground) } else { log.Debug("Cannot set search hit line background based on plainBg=", plainBg, " hitBg=", hitBg) } } func TermcapToStyle(termcap string) (twin.Style, error) { // Add a character to be sure we have one to take the format from cells := textstyles.StyledRunesFromString(twin.StyleDefault, termcap+"x", nil, 0).StyledRunes if len(cells) != 1 { return twin.StyleDefault, fmt.Errorf("Expected styling only and no text") } return cells[len(cells)-1].Style, nil } moor-2.10.3/internal/styling_test.go000066400000000000000000000022031513574474500174600ustar00rootroot00000000000000package internal import ( "os" "testing" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/formatters" "github.com/alecthomas/chroma/v2/styles" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) func TestTwinStyleFromChroma(t *testing.T) { // Test that getting exact GenericHeading from base16-snazzy works style := twinStyleFromChroma( nil, styles.Registry["base16-snazzy"], &formatters.TTY16m, chroma.GenericHeading, true, ) assert.Equal(t, *style, twin.StyleDefault. WithAttr(twin.AttrBold). WithForeground(twin.NewColor24Bit(0xe2, 0xe4, 0xe5))) } func TestSetStyle(t *testing.T) { assert.NilError(t, os.Setenv("MOOR_TEST_STYLE", "\x1b[1;31m")) style := twin.StyleDefault setStyle(&style, "MOOR_TEST_STYLE", nil) assert.Equal(t, style, twin.StyleDefault.WithAttr(twin.AttrBold).WithForeground(twin.NewColor16(1))) } // Regression test for https://github.com/walles/moor/issues/339 // // We used to crash doing this. func TestConfigureHighlighting_No24BitColors(t *testing.T) { searchHitStyle = twin.StyleDefault.WithForeground(twin.NewColor16(3)) configureHighlighting(nil, true) } moor-2.10.3/internal/textstyles/000077500000000000000000000000001513574474500166345ustar00rootroot00000000000000moor-2.10.3/internal/textstyles/ansiTokenizer.go000066400000000000000000000500061513574474500220110ustar00rootroot00000000000000// This package handles styled strings. It can strip styling from strings and it // can turn a styled string into a series of screen cells. Some global variables // can be used to configure how various things are rendered. package textstyles import ( "fmt" "slices" "strconv" "strings" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/twin" ) // How do we render unprintable characters? type UnprintableStyleT int const ( UnprintableStyleHighlight UnprintableStyleT = iota UnprintableStyleWhitespace ) var UnprintableStyle UnprintableStyleT // These three styles will be configured from styling.go var ManPageBold = twin.StyleDefault.WithAttr(twin.AttrBold) var ManPageUnderline = twin.StyleDefault.WithAttr(twin.AttrUnderline) var ManPageHeading = twin.StyleDefault.WithAttr(twin.AttrBold) // This is what less (version 581.2 on macOS) defaults to var TabSize = 8 const BACKSPACE = '\b' type StyledRunesWithTrailer struct { StyledRunes []CellWithMetadata Trailer twin.Style ContainsSearchHit bool } func isPlain(s string) bool { for i := 0; i < len(s); i++ { byteAtIndex := s[i] if byteAtIndex < 32 { return false } if byteAtIndex > 127 { return false } } return true } // lineIndex is only used for error reporting func StripFormatting(s string, lineIndex linemetadata.Index) string { if isPlain(s) { return s } stripped := strings.Builder{} stripped.Grow(len(s)) // This makes BenchmarkStripFormatting 6% faster runeCount := 0 styledStringsFromString(twin.StyleDefault, s, &lineIndex, 0, func(str string, style twin.Style) { for _, runeValue := range runesFromStyledString(_StyledString{String: str, Style: style}) { switch runeValue { case '\x09': // TAB for { stripped.WriteRune(' ') runeCount++ if runeCount%TabSize == 0 { // We arrived at the next tab stop break } } case '�': // Go's broken-UTF8 marker switch UnprintableStyle { case UnprintableStyleHighlight: stripped.WriteRune('?') case UnprintableStyleWhitespace: stripped.WriteRune(' ') default: panic(fmt.Errorf("Unsupported unprintable-style: %#v", UnprintableStyle)) } runeCount++ case BACKSPACE: stripped.WriteRune('<') runeCount++ default: if !twin.Printable(runeValue) { stripped.WriteRune('?') runeCount++ continue } stripped.WriteRune(runeValue) runeCount++ } } }) return stripped.String() } // Turn a (formatted) string into a series of screen cells // // The prefix will be prepended to the string before parsing. The lineIndex is // used for error reporting. // // maxTokensCount: at most this many tokens will be included in the result. If // 0, do all runes. For BenchmarkRenderHugeLine() performance. func StyledRunesFromString(plainTextStyle twin.Style, s string, lineIndex *linemetadata.Index, maxTokensCount int) StyledRunesWithTrailer { manPageHeading := manPageHeadingFromString(s) if manPageHeading != nil { return *manPageHeading } cells := make([]CellWithMetadata, 0, len(s)) // Specs: https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit styleUnprintable := twin.StyleDefault.WithBackground(twin.NewColor16(1)).WithForeground(twin.NewColor16(7)) trailer := styledStringsFromString(plainTextStyle, s, lineIndex, maxTokensCount, func(str string, style twin.Style) { for _, token := range tokensFromStyledString(_StyledString{String: str, Style: style}, maxTokensCount) { switch token.Rune { case '\x09': // TAB for { cells = append(cells, CellWithMetadata{ Rune: ' ', Style: style, }) if (len(cells))%TabSize == 0 { // We arrived at the next tab stop break } } case '�': // Go's broken-UTF8 marker switch UnprintableStyle { case UnprintableStyleHighlight: cells = append(cells, CellWithMetadata{ Rune: '?', Style: styleUnprintable, }) case UnprintableStyleWhitespace: cells = append(cells, CellWithMetadata{ Rune: '?', Style: twin.StyleDefault, }) default: panic(fmt.Errorf("Unsupported unprintable-style: %#v", UnprintableStyle)) } case BACKSPACE: cells = append(cells, CellWithMetadata{ Rune: '<', Style: styleUnprintable, }) default: if !twin.Printable(token.Rune) { switch UnprintableStyle { case UnprintableStyleHighlight: cells = append(cells, CellWithMetadata{ Rune: '?', Style: styleUnprintable, }) case UnprintableStyleWhitespace: cells = append(cells, CellWithMetadata{ Rune: ' ', Style: twin.StyleDefault, }) default: panic(fmt.Errorf("Unsupported unprintable-style: %#v", UnprintableStyle)) } continue } cells = append(cells, CellWithMetadata{ Rune: token.Rune, Style: token.Style, }) } } }) return StyledRunesWithTrailer{ StyledRunes: cells, Trailer: trailer, // Populated in Line.HighlightedTokens(), where the search hit // highlighting happens ContainsSearchHit: false, } } // Consume '_ 0 && maxTokensCount < maxBackspaceCheck { maxBackspaceCheck = maxTokensCount + 20 // Some extra to account for backspaces further out } if maxBackspaceCheck > len(styledString.String) { maxBackspaceCheck = len(styledString.String) } if !strings.ContainsRune(styledString.String[:maxBackspaceCheck], BACKSPACE) { // Shortcut when there's no backspace based formatting to worry about returnTokensCount := len(styledString.String) if maxTokensCount > 0 && returnTokensCount > maxTokensCount { returnTokensCount = maxTokensCount } tokens := make([]twin.StyledRune, 0, returnTokensCount) for _, runeValue := range styledString.String { if len(tokens) == cap(tokens) { // We have enough runes, stop here break } tokens = append(tokens, twin.StyledRune{ Rune: runeValue, Style: styledString.Style, }) } return tokens } tokens := make([]twin.StyledRune, 0, maxTokensCount) // Special handling for man page formatted lines. If this is updated you // must update HasManPageFormatting() as well. for runes := (lazyRunes{str: styledString.String}); runes.getRelative(0) != nil; runes.next() { if maxTokensCount > 0 && len(tokens) >= maxTokensCount { // We have enough runes, stop here break } token := consumeBullet(&runes) if token != nil { tokens = append(tokens, *token) continue } token = consumeBoldUnderline(&runes) if token != nil { tokens = append(tokens, *token) continue } token = consumeBold(&runes) if token != nil { tokens = append(tokens, *token) continue } token = consumeUnderline(&runes) if token != nil { tokens = append(tokens, *token) continue } tokens = append(tokens, twin.StyledRune{ Rune: *runes.getRelative(0), Style: styledString.Style, }) } return tokens } // Like tokensFromStyledString(), but only checks without building any formatting func HasManPageFormatting(s string) bool { for runes := (lazyRunes{str: s}); runes.getRelative(0) != nil; runes.next() { consumed := consumeBullet(&runes) if consumed != nil { return true } consumed = consumeBoldUnderline(&runes) if consumed != nil { return true } consumed = consumeBold(&runes) if consumed != nil { return true } consumed = consumeUnderline(&runes) if consumed != nil { return true } } return false } type _StyledString struct { String string Style twin.Style } // To avoid allocations, our caller is expected to provide us with a // pre-allocated numbersBuffer for storing the result. // // This function is part of the hot code path while searching, so we want it to // be fast. // // # Benchmarking instructions // // go test -benchmem -run='^$' -bench=BenchmarkHighlightedSearch . ./... func splitIntoNumbers(s string, numbersBuffer []uint) ([]uint, error) { numbers := numbersBuffer[:0] afterLastSeparator := 0 for i, char := range s { if char >= '0' && char <= '9' { continue } if char == ';' || char == ':' { numberString := s[afterLastSeparator:i] if numberString == "" { numbers = append(numbers, 0) continue } number, err := strconv.ParseUint(numberString, 10, 64) if err != nil { return numbers, err } numbers = append(numbers, uint(number)) afterLastSeparator = i + 1 continue } return numbers, fmt.Errorf("Unrecognized character in <%s>: %c", s, char) } // Now we have to handle the last number numberString := s[afterLastSeparator:] if numberString == "" { numbers = append(numbers, 0) return numbers, nil } number, err := strconv.ParseUint(numberString, 10, 64) if err != nil { return numbers, err } numbers = append(numbers, uint(number)) return numbers, nil } // rawUpdateStyle parses a string of the form "33m" into changes to style. This // is what comes after ESC[ in an ANSI SGR sequence. func rawUpdateStyle(style twin.Style, escapeSequenceWithoutHeader string, numbersBuffer []uint) (twin.Style, []uint, error) { if len(escapeSequenceWithoutHeader) == 0 { return style, numbersBuffer, fmt.Errorf("empty escape sequence, expected at least an ending letter") } if escapeSequenceWithoutHeader[len(escapeSequenceWithoutHeader)-1] != 'm' { return style, numbersBuffer, fmt.Errorf("escape sequence does not end with 'm': %s", escapeSequenceWithoutHeader) } numbersBuffer, err := splitIntoNumbers(escapeSequenceWithoutHeader[:len(escapeSequenceWithoutHeader)-1], numbersBuffer) if err != nil { return style, numbersBuffer, fmt.Errorf("splitIntoNumbers: %w", err) } index := 0 for index < len(numbersBuffer) { number := numbersBuffer[index] index++ switch number { case 0: // SGR Reset should not affect the OSC8 hyperlink style = twin.StyleDefault.WithHyperlink(style.HyperlinkURL()) case 1: style = style.WithAttr(twin.AttrBold) case 2: style = style.WithAttr(twin.AttrDim) case 3: style = style.WithAttr(twin.AttrItalic) case 4: style = style.WithAttr(twin.AttrUnderline) case 7: style = style.WithAttr(twin.AttrReverse) case 22: style = style.WithoutAttr(twin.AttrBold).WithoutAttr(twin.AttrDim) case 23: style = style.WithoutAttr(twin.AttrItalic) case 24: style = style.WithoutAttr(twin.AttrUnderline) case 27: style = style.WithoutAttr(twin.AttrReverse) // Foreground colors, https://pkg.go.dev/github.com/gdamore/tcell#Color case 30: style = style.WithForeground(twin.NewColor16(0)) case 31: style = style.WithForeground(twin.NewColor16(1)) case 32: style = style.WithForeground(twin.NewColor16(2)) case 33: style = style.WithForeground(twin.NewColor16(3)) case 34: style = style.WithForeground(twin.NewColor16(4)) case 35: style = style.WithForeground(twin.NewColor16(5)) case 36: style = style.WithForeground(twin.NewColor16(6)) case 37: style = style.WithForeground(twin.NewColor16(7)) case 38: var err error var color *twin.Color index, color, err = consumeCompositeColor(numbersBuffer, index-1) if err != nil { return style, numbersBuffer, fmt.Errorf("Foreground: %w", err) } style = style.WithForeground(*color) case 39: style = style.WithForeground(twin.ColorDefault) // Background colors, see https://pkg.go.dev/github.com/gdamore/Color case 40: style = style.WithBackground(twin.NewColor16(0)) case 41: style = style.WithBackground(twin.NewColor16(1)) case 42: style = style.WithBackground(twin.NewColor16(2)) case 43: style = style.WithBackground(twin.NewColor16(3)) case 44: style = style.WithBackground(twin.NewColor16(4)) case 45: style = style.WithBackground(twin.NewColor16(5)) case 46: style = style.WithBackground(twin.NewColor16(6)) case 47: style = style.WithBackground(twin.NewColor16(7)) case 48: var err error var color *twin.Color index, color, err = consumeCompositeColor(numbersBuffer, index-1) if err != nil { return style, numbersBuffer, fmt.Errorf("Background: %w", err) } style = style.WithBackground(*color) case 49: style = style.WithBackground(twin.ColorDefault) case 58: var err error var color *twin.Color index, color, err = consumeCompositeColor(numbersBuffer, index-1) if err != nil { return style, numbersBuffer, fmt.Errorf("Underline: %w", err) } style = style.WithUnderlineColor(*color) case 59: style = style.WithUnderlineColor(twin.ColorDefault) // Bright foreground colors: see https://pkg.go.dev/github.com/gdamore/Color // // After testing vs less and cat on iTerm2 3.3.9 / macOS Catalina // 10.15.4 that's how they seem to handle this, tested with: // * TERM=xterm-256color // * TERM=screen-256color case 90: style = style.WithForeground(twin.NewColor16(8)) case 91: style = style.WithForeground(twin.NewColor16(9)) case 92: style = style.WithForeground(twin.NewColor16(10)) case 93: style = style.WithForeground(twin.NewColor16(11)) case 94: style = style.WithForeground(twin.NewColor16(12)) case 95: style = style.WithForeground(twin.NewColor16(13)) case 96: style = style.WithForeground(twin.NewColor16(14)) case 97: style = style.WithForeground(twin.NewColor16(15)) case 100: style = style.WithBackground(twin.NewColor16(8)) case 101: style = style.WithBackground(twin.NewColor16(9)) case 102: style = style.WithBackground(twin.NewColor16(10)) case 103: style = style.WithBackground(twin.NewColor16(11)) case 104: style = style.WithBackground(twin.NewColor16(12)) case 105: style = style.WithBackground(twin.NewColor16(13)) case 106: style = style.WithBackground(twin.NewColor16(14)) case 107: style = style.WithBackground(twin.NewColor16(15)) default: return style, numbersBuffer, fmt.Errorf("Unrecognized ANSI SGR code <%d>", number) } } return style, numbersBuffer, nil } func joinUints(ints []uint) string { joinedWithBrackets := strings.ReplaceAll(fmt.Sprint(ints), " ", ";") joined := joinedWithBrackets[1 : len(joinedWithBrackets)-1] return joined } // numbers is a list of numbers from a ANSI SGR string // index points to either 38 or 48 in that string // // This method will return: // - The first index in the string that this function did not consume // - A color value that can be applied to a style func consumeCompositeColor(numbers []uint, index int) (int, *twin.Color, error) { baseIndex := index if numbers[index] != 38 && numbers[index] != 48 && numbers[index] != 58 { err := fmt.Errorf( "unknown start of color sequence <%d>, expected 38 (foreground), 48 (background) or 58 (underline): ", numbers[index], joinUints(numbers[baseIndex:])) return -1, nil, err } index++ if index >= len(numbers) { err := fmt.Errorf( "incomplete color sequence: ", joinUints(numbers[baseIndex:])) return -1, nil, err } if numbers[index] == 5 { // Handle 8 bit color index++ if index >= len(numbers) { err := fmt.Errorf( "incomplete 8 bit color sequence: ", joinUints(numbers[baseIndex:])) return -1, nil, err } colorNumber := numbers[index] colorValue := twin.NewColor256(uint8(colorNumber)) return index + 1, &colorValue, nil } if numbers[index] == 2 { // Handle 24 bit color rIndex := index + 1 gIndex := index + 2 bIndex := index + 3 if bIndex >= len(numbers) { err := fmt.Errorf( "incomplete 24 bit color sequence, expected N8;2;R;G;Bm: ", joinUints(numbers[baseIndex:])) return -1, nil, err } rValue := uint8(numbers[rIndex]) gValue := uint8(numbers[gIndex]) bValue := uint8(numbers[bIndex]) colorValue := twin.NewColor24Bit(rValue, gValue, bValue) return bIndex + 1, &colorValue, nil } err := fmt.Errorf( "unknown color type <%d>, expected 5 (8 bit color) or 2 (24 bit color): ", numbers[index], joinUints(numbers[baseIndex:])) return -1, nil, err } moor-2.10.3/internal/textstyles/ansiTokenizer_test.go000066400000000000000000000334361513574474500230600ustar00rootroot00000000000000package textstyles import ( "bufio" "fmt" "os" "path" "strings" "testing" "unicode/utf8" "github.com/google/go-cmp/cmp" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) const samplesDir = "../../sample-files" // Convert a cells array to a plain string func cellsToPlainString(cells []CellWithMetadata) string { returnMe := "" for _, cell := range cells { returnMe += string(cell.Rune) } return returnMe } func getTestFiles(t *testing.T) []string { files, err := os.ReadDir(samplesDir) assert.NilError(t, err) var filenames []string for _, file := range files { filenames = append(filenames, path.Join(samplesDir, file.Name())) } return filenames } // Verify that we can tokenize all lines in ../sample-files/* // without logging any errors func TestTokenize(t *testing.T) { for _, fileName := range getTestFiles(t) { t.Run(fileName, func(t *testing.T) { file, err := os.Open(fileName) if err != nil { t.Errorf("Error opening file <%s>: %s", fileName, err.Error()) return } defer func() { if err := file.Close(); err != nil { panic(err) } }() fileReader, err := os.Open(fileName) assert.NilError(t, err) fileScanner := bufio.NewScanner(fileReader) // Upping the buffer like this (from a default of 64kb) makes the // tests go faster fileScanner.Buffer(make([]byte, 1024*1024), 1024*1024) var lineIndex *linemetadata.Index for fileScanner.Scan() { line := fileScanner.Text() if lineIndex == nil { lineIndex = &linemetadata.Index{} } else { next := lineIndex.NonWrappingAdd(1) lineIndex = &next } var loglines strings.Builder log.SetOutput(&loglines) tokens := StyledRunesFromString(twin.StyleDefault, line, lineIndex, 0).StyledRunes plainString := StripFormatting(line, *lineIndex) if len(tokens) != utf8.RuneCountInString(plainString) { t.Errorf("%s:%s: len(tokens)=%d, len(plainString)=%d for: <%s>", fileName, lineIndex.Format(), len(tokens), utf8.RuneCountInString(plainString), line) continue } // Tokens and plain have the same lengths, compare contents plainStringChars := []rune(plainString) for index, plainChar := range plainStringChars { cellChar := tokens[index] if cellChar.Rune == plainChar { continue } if cellChar.Rune == '•' && plainChar == 'o' { // Pretty bullets on man pages continue } // Chars mismatch! plainStringFromCells := cellsToPlainString(tokens) positionMarker := strings.Repeat(" ", index) + "^" cellCharString := string(cellChar.Rune) if !twin.Printable(cellChar.Rune) { cellCharString = fmt.Sprint(int(cellChar.Rune)) } plainCharString := string(plainChar) if !twin.Printable(plainChar) { plainCharString = fmt.Sprint(int(plainChar)) } t.Errorf("%s:%s, 0-based column %d: cell char <%s> != plain char <%s>:\nPlain: %s\nCells: %s\n %s", fileName, lineIndex.Format(), index, cellCharString, plainCharString, plainString, plainStringFromCells, positionMarker, ) break } if len(loglines.String()) != 0 { t.Errorf("%s: %s", fileName, loglines.String()) continue } } }) } } func TestUnderline(t *testing.T) { tokens := StyledRunesFromString(twin.StyleDefault, "a\x1b[4mb\x1b[24mc", nil, 0).StyledRunes assert.Equal(t, len(tokens), 3) assert.Equal(t, tokens[0], CellWithMetadata{Rune: 'a', Style: twin.StyleDefault}) assert.Equal(t, tokens[1], CellWithMetadata{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)}) assert.Equal(t, tokens[2], CellWithMetadata{Rune: 'c', Style: twin.StyleDefault}) } func TestManPages(t *testing.T) { // Bold tokens := StyledRunesFromString(twin.StyleDefault, "ab\bbc", nil, 0).StyledRunes assert.Equal(t, len(tokens), 3) assert.Equal(t, tokens[0], CellWithMetadata{Rune: 'a', Style: twin.StyleDefault}) assert.Equal(t, tokens[1], CellWithMetadata{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrBold)}) assert.Equal(t, tokens[2], CellWithMetadata{Rune: 'c', Style: twin.StyleDefault}) // Underline tokens = StyledRunesFromString(twin.StyleDefault, "a_\bbc", nil, 0).StyledRunes assert.Equal(t, len(tokens), 3) assert.Equal(t, tokens[0], CellWithMetadata{Rune: 'a', Style: twin.StyleDefault}) assert.Equal(t, tokens[1], CellWithMetadata{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)}) assert.Equal(t, tokens[2], CellWithMetadata{Rune: 'c', Style: twin.StyleDefault}) // Bullet point 1, taken from doing this on my macOS system: // env PAGER="hexdump -C" man printf | moor tokens = StyledRunesFromString(twin.StyleDefault, "a+\b+\bo\bob", nil, 0).StyledRunes assert.Equal(t, len(tokens), 3) assert.Equal(t, tokens[0], CellWithMetadata{Rune: 'a', Style: twin.StyleDefault}) assert.Equal(t, tokens[1], CellWithMetadata{Rune: '•', Style: twin.StyleDefault}) assert.Equal(t, tokens[2], CellWithMetadata{Rune: 'b', Style: twin.StyleDefault}) // Bullet point 2, taken from doing this using the "fish" shell on my macOS system: // man printf | hexdump -C | moor tokens = StyledRunesFromString(twin.StyleDefault, "a+\bob", nil, 0).StyledRunes assert.Equal(t, len(tokens), 3) assert.Equal(t, tokens[0], CellWithMetadata{Rune: 'a', Style: twin.StyleDefault}) assert.Equal(t, tokens[1], CellWithMetadata{Rune: '•', Style: twin.StyleDefault}) assert.Equal(t, tokens[2], CellWithMetadata{Rune: 'b', Style: twin.StyleDefault}) } func TestManPageHeadings(t *testing.T) { // Set a marker style we can recognize and test for ManPageHeading = twin.StyleDefault.WithForeground(twin.NewColor16(2)) manPageHeading := "" for _, char := range "JOHAN HELLO" { manPageHeading += string(char) + "\b" + string(char) } notAllCaps := "" for _, char := range "Johan Hello" { notAllCaps += string(char) + "\b" + string(char) } // A line with only man page bold caps should be considered a heading for _, token := range StyledRunesFromString(twin.StyleDefault, manPageHeading, nil, 0).StyledRunes { assert.Equal(t, token.Style, ManPageHeading) } // A line with only non-man-page bold caps should not be considered a heading wrongKindOfBold := "\x1b[1mJOHAN HELLO" for _, token := range StyledRunesFromString(twin.StyleDefault, wrongKindOfBold, nil, 0).StyledRunes { assert.Equal(t, token.Style, twin.StyleDefault.WithAttr(twin.AttrBold)) } // A line with not all caps should not be considered a heading for _, token := range StyledRunesFromString(twin.StyleDefault, notAllCaps, nil, 0).StyledRunes { assert.Equal(t, token.Style, twin.StyleDefault.WithAttr(twin.AttrBold)) } } func TestConsumeCompositeColorHappy(t *testing.T) { // 8 bit color // Example from: https://github.com/walles/moor/issues/14 newIndex, color, err := consumeCompositeColor([]uint{38, 5, 74}, 0) assert.NilError(t, err) assert.Equal(t, newIndex, 3) assert.Equal(t, *color, twin.NewColor256(74)) // 24 bit color newIndex, color, err = consumeCompositeColor([]uint{38, 2, 10, 20, 30}, 0) assert.NilError(t, err) assert.Equal(t, newIndex, 5) assert.Equal(t, *color, twin.NewColor24Bit(10, 20, 30)) } func TestConsumeCompositeColorBadPrefix(t *testing.T) { // 8 bit color // Example from: https://github.com/walles/moor/issues/14 _, color, err := consumeCompositeColor([]uint{29}, 0) assert.Equal(t, err.Error(), "unknown start of color sequence <29>, expected 38 (foreground), 48 (background) or 58 (underline): ") assert.Assert(t, color == nil) } func TestConsumeCompositeColorBadType(t *testing.T) { _, color, err := consumeCompositeColor([]uint{38, 4}, 0) // https://en.wikipedia.org/wiki/ANSI_escape_code#Colors assert.Equal(t, err.Error(), "unknown color type <4>, expected 5 (8 bit color) or 2 (24 bit color): ") assert.Assert(t, color == nil) } func TestConsumeCompositeColorIncomplete(t *testing.T) { _, color, err := consumeCompositeColor([]uint{38}, 0) assert.Equal(t, err.Error(), "incomplete color sequence: ") assert.Assert(t, color == nil) } func TestConsumeCompositeColorIncomplete8Bit(t *testing.T) { _, color, err := consumeCompositeColor([]uint{38, 5}, 0) assert.Equal(t, err.Error(), "incomplete 8 bit color sequence: ") assert.Assert(t, color == nil) } func TestConsumeCompositeColorIncomplete24Bit(t *testing.T) { _, color, err := consumeCompositeColor([]uint{38, 2, 10, 20}, 0) assert.Equal(t, err.Error(), "incomplete 24 bit color sequence, expected N8;2;R;G;Bm: ") assert.Assert(t, color == nil) } func TestRawUpdateStyle(t *testing.T) { numberColored, _, err := rawUpdateStyle(twin.StyleDefault, "33m", make([]uint, 0)) assert.NilError(t, err) assert.Equal(t, numberColored, twin.StyleDefault.WithForeground(twin.NewColor16(3))) } // Test with the recommended terminator ESC-backslash. // // Ref: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#the-escape-sequence func TestHyperlink_escBackslash(t *testing.T) { url := "http://example.com" tokens := StyledRunesFromString(twin.StyleDefault, "a\x1b]8;;"+url+"\x1b\\bc\x1b]8;;\x1b\\d", nil, 0).StyledRunes assert.DeepEqual(t, tokens, []CellWithMetadata{ {Rune: 'a', Style: twin.StyleDefault}, {Rune: 'b', Style: twin.StyleDefault.WithHyperlink(&url)}, {Rune: 'c', Style: twin.StyleDefault.WithHyperlink(&url)}, {Rune: 'd', Style: twin.StyleDefault}, }, cmp.Comparer(func(a, b CellWithMetadata) bool { return a.Equal(b) })) } // Test with the not-recommended terminator BELL (ASCII 7). // // Ref: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#the-escape-sequence func TestHyperlink_bell(t *testing.T) { url := "http://example.com" tokens := StyledRunesFromString(twin.StyleDefault, "a\x1b]8;;"+url+"\x07bc\x1b]8;;\x07d", nil, 0).StyledRunes assert.DeepEqual(t, tokens, []CellWithMetadata{ {Rune: 'a', Style: twin.StyleDefault}, {Rune: 'b', Style: twin.StyleDefault.WithHyperlink(&url)}, {Rune: 'c', Style: twin.StyleDefault.WithHyperlink(&url)}, {Rune: 'd', Style: twin.StyleDefault}, }, cmp.Comparer(func(a, b CellWithMetadata) bool { return a.Equal(b) })) } // Test with some other ESC sequence than ESC-backslash func TestHyperlink_nonTerminatingEsc(t *testing.T) { complete := "a\x1b]8;;https://example.com\x1bbc" tokens := StyledRunesFromString(twin.StyleDefault, complete, nil, 0).StyledRunes // This should not be treated as any link for i := 0; i < len(complete); i++ { if complete[i] == '\x1b' { // These get special rendering, if everything else matches that's // good enough. continue } assert.Equal(t, tokens[i], CellWithMetadata{Rune: rune(complete[i]), Style: twin.StyleDefault}, "i=%d, c=%s, tokens=%v", i, string(complete[i]), tokens) } } func TestHyperlink_incomplete(t *testing.T) { complete := "a\x1b]8;;X\x1b\\" for l := len(complete) - 1; l >= 0; l-- { incomplete := complete[:l] t.Run(fmt.Sprintf("l=%d incomplete=<%s>", l, strings.ReplaceAll(incomplete, "\x1b", "ESC")), func(t *testing.T) { tokens := StyledRunesFromString(twin.StyleDefault, incomplete, nil, 0).StyledRunes for i := 0; i < l; i++ { if complete[i] == '\x1b' { // These get special rendering, if everything else matches // that's good enough. continue } assert.Equal(t, tokens[i], CellWithMetadata{Rune: rune(complete[i]), Style: twin.StyleDefault}) } }) } } func TestRawUpdateStyleResetDoesNotAffectHyperlink(t *testing.T) { url := "file:///Users/johan/src/riff/src/refiner.rs" styleWithLink := twin.StyleDefault.WithHyperlink(&url) // ESC[m should reset style, but not touch the hyperlink updated, _, err := rawUpdateStyle(styleWithLink, "m", nil) assert.NilError(t, err) assert.Assert(t, updated.HyperlinkURL() != nil) assert.Equal(t, *updated.HyperlinkURL(), url) } // Ref: https://github.com/walles/moor/issues/372 func TestIssue372(t *testing.T) { const maxTokensCount = 10 // Load test data once data, err := os.ReadFile(path.Join(samplesDir, "issue-372.txt")) assert.NilError(t, err) // Expect one newline terminated line lines := strings.Split(string(data), "\n") assert.Equal(t, 2, len(lines)) assert.Equal(t, 0, len(lines[1])) styled := StyledRunesFromString(twin.StyleDefault, lines[0], nil, maxTokensCount).StyledRunes assert.Equal(t, len(styled), maxTokensCount) } // Benchmark stripping formatting from a colored git diff sample. // To run: // // go test -bench=BenchmarkStripFormatting -benchmem ./... func BenchmarkStripFormatting(b *testing.B) { // Load sample input once data, err := os.ReadFile(path.Join(samplesDir, "gitdiff-color.txt")) if err != nil { b.Fatalf("read sample: %v", err) } lines := strings.Split(string(data), "\n") // Set processed bytes per iteration b.SetBytes(int64(len(data))) b.ResetTimer() for i := 0; i < b.N; i++ { for _, line := range lines { // We ignore the output; benchmarking execution time only _ = StripFormatting(line, linemetadata.Index{}) } } } // Benchmark stripping formatting from a colored git diff sample. // To run: // // go test -bench=BenchmarkStripFormattingUnformattedInput -benchmem ./... func BenchmarkStripFormattingUnformattedInput(b *testing.B) { // Load sample input once data, err := os.ReadFile(path.Join(samplesDir, "gitdiff-color.txt")) if err != nil { b.Fatalf("read sample: %v", err) } // Remove formatting before benchmarking var unformatted strings.Builder formattedLines := strings.Split(string(data), "\n") for _, line := range formattedLines { unformatted.WriteString(StripFormatting(line, linemetadata.Index{})) unformatted.WriteString("\n") } lines := strings.Split(unformatted.String(), "\n") // Set processed bytes per iteration b.SetBytes(int64(len(unformatted.String()))) b.ResetTimer() for i := 0; i < b.N; i++ { for _, line := range lines { // We ignore the output; benchmarking execution time only _ = StripFormatting(line, linemetadata.Index{}) } } } moor-2.10.3/internal/textstyles/cellWithMetadata.go000066400000000000000000000040741513574474500224040ustar00rootroot00000000000000package textstyles import ( "unicode" "github.com/walles/moor/v2/twin" ) // Like twin.StyledRune, but with additional metadata type CellWithMetadata struct { Rune rune Style twin.Style cachedWidth *int StartsSearchHit bool // True if this cell is the start of a search hit IsSearchHit bool // True if this cell is part of a search hit } // Required for some tests to pass func (r CellWithMetadata) Equal(b CellWithMetadata) bool { if r.Rune != b.Rune { return false } if !r.Style.Equal(b.Style) { return false } if r.IsSearchHit != b.IsSearchHit { return false } if r.StartsSearchHit != b.StartsSearchHit { return false } return true } func (r CellWithMetadata) ToStyledRune() twin.StyledRune { return twin.NewStyledRune(r.Rune, r.Style) } func (r *CellWithMetadata) Width() int { if r.cachedWidth != nil { return *r.cachedWidth } // Cache it w := r.ToStyledRune().Width() r.cachedWidth = &w return w } type CellWithMetadataSlice []CellWithMetadata func (runes CellWithMetadataSlice) Equal(other CellWithMetadataSlice) bool { if len(runes) != len(other) { return false } for i := range runes { if !runes[i].Equal(other[i]) { return false } } return true } // Returns a copy of the slice with leading whitespace removed func (runes CellWithMetadataSlice) WithoutSpaceLeft() CellWithMetadataSlice { for i := range runes { cell := runes[i] if !unicode.IsSpace(cell.Rune) { return runes[i:] } // That was a space, keep looking } // All whitespace, return empty return CellWithMetadataSlice{} } // Returns a copy of the slice with trailing whitespace removed func (runes CellWithMetadataSlice) WithoutSpaceRight() CellWithMetadataSlice { for i := len(runes) - 1; i >= 0; i-- { cell := runes[i] if !unicode.IsSpace(cell.Rune) { return runes[0 : i+1] } // That was a space, keep looking } // All whitespace, return empty return CellWithMetadataSlice{} } func (runes CellWithMetadataSlice) ContainsSearchHit() bool { for _, cell := range runes { if cell.IsSearchHit { return true } } return false } moor-2.10.3/internal/textstyles/lazyrunes.go000066400000000000000000000023331513574474500212200ustar00rootroot00000000000000package textstyles import "unicode/utf8" // Lazily iterate a string by runes. Init by setting str only. // // Use getRelative(0) to get the current rune, and do next() to advance to the // next rune. type lazyRunes struct { str string // From what position in the string should we pick the next rune? nextByteIndex int lookaheadBuffer []rune } // Get rune at index runeIndex relative to the current rune func (l *lazyRunes) getRelative(runeIndex int) *rune { for runeIndex >= len(l.lookaheadBuffer) { // Need to add one more rune to the lookahead buffer if l.nextByteIndex >= len(l.str) { // No more runes return nil } newRune, runeSize := utf8.DecodeRuneInString(l.str[l.nextByteIndex:]) if runeSize == 0 { panic("We just checked, there should be more runes") } // Append the new rune to the lookahead buffer l.lookaheadBuffer = append(l.lookaheadBuffer, newRune) l.nextByteIndex += runeSize } return &l.lookaheadBuffer[runeIndex] } // Move the base rune index forward by one rune. func (l *lazyRunes) next() { if l.getRelative(0) == nil { // Already past the end return } // Shift the lookahead buffer down if len(l.lookaheadBuffer) > 0 { l.lookaheadBuffer = l.lookaheadBuffer[1:] } } moor-2.10.3/internal/textstyles/lazyrunes_test.go000066400000000000000000000013741513574474500222630ustar00rootroot00000000000000package textstyles import ( "testing" "gotest.tools/v3/assert" ) func TestLazyRunes_empty(t *testing.T) { testMe := lazyRunes{str: ""} assert.Equal(t, true, testMe.getRelative(0) == nil) } func TestLazyRunes_unicode(t *testing.T) { testMe := lazyRunes{str: "åäö"} // What's up first? assert.Equal(t, 'Ã¥', *testMe.getRelative(0)) assert.Equal(t, 'ä', *testMe.getRelative(1)) // Intentionally don't get the third rune yet // Move to 'ä' testMe.next() assert.Equal(t, 'ä', *testMe.getRelative(0)) // Move to 'ö' testMe.next() assert.Equal(t, 'ö', *testMe.getRelative(0)) // No more runes assert.Equal(t, true, testMe.getRelative(1) == nil) // Move past the end testMe.next() assert.Equal(t, true, testMe.getRelative(0) == nil) } moor-2.10.3/internal/textstyles/manPageHeading.go000066400000000000000000000053621513574474500220210ustar00rootroot00000000000000package textstyles import ( "unicode" "github.com/walles/moor/v2/twin" ) func manPageHeadingFromString(s string) *StyledRunesWithTrailer { // For great performance, first check the string without allocating any // memory. if !parseManPageHeading(s, func(_ CellWithMetadata) {}) { return nil } cells := make([]CellWithMetadata, 0, len(s)/2) ok := parseManPageHeading(s, func(cell CellWithMetadata) { cells = append(cells, cell) }) if !ok { panic("man page heading state changed") } return &StyledRunesWithTrailer{ StyledRunes: cells, Trailer: twin.StyleDefault, } } // Reports back one cell at a time. Returns true if the entire string was a man // page heading. // // If it was not, false will be returned and the cell reporting will be // interrupted. // // A man page heading is all caps. Also, each character is encoded as // char+backspace+char, where both chars need to be the same. Whitespace is an // exception, they can be not bold. func parseManPageHeading(s string, reportStyledRune func(CellWithMetadata)) bool { if len(s) < 3 { // We don't want to match empty strings. Also, strings of length 1 and 2 // cannot be man page headings since "char+backspace+char" is 3 bytes. return false } type stateT int const ( stateExpectingFirstChar stateT = iota stateExpectingBackspace stateExpectingSecondChar ) state := stateExpectingFirstChar var firstChar rune lapCounter := -1 for _, char := range s { lapCounter++ switch state { case stateExpectingFirstChar: if lapCounter == 0 && unicode.IsSpace(char) { // Headings do not start with whitespace return false } if char == '\b' { // Starting with backspace is an error return false } firstChar = char state = stateExpectingBackspace case stateExpectingBackspace: if char == '\b' { state = stateExpectingSecondChar continue } if unicode.IsSpace(firstChar) { // Whitespace is an exception, it can be not bold reportStyledRune(CellWithMetadata{Rune: firstChar, Style: ManPageHeading}) // Assume what we got was a new first char firstChar = char state = stateExpectingBackspace continue } // No backspace and no previous-was-whitespace, this is an error return false case stateExpectingSecondChar: if char == '\b' { // Ending with backspace is an error return false } if char != firstChar { // Different first and last char is an error return false } if unicode.IsLetter(char) && !unicode.IsUpper(char) { // Not ALL CAPS => Not a heading return false } reportStyledRune(CellWithMetadata{Rune: char, Style: ManPageHeading}) state = stateExpectingFirstChar default: panic("Unknown state") } } return state == stateExpectingFirstChar } moor-2.10.3/internal/textstyles/manPageHeading_test.go000066400000000000000000000037611513574474500230610ustar00rootroot00000000000000package textstyles import ( "testing" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) func isManPageHeading(s string) bool { return parseManPageHeading(s, func(_ CellWithMetadata) {}) } func TestIsManPageHeading(t *testing.T) { assert.Assert(t, !isManPageHeading("")) assert.Assert(t, !isManPageHeading("A"), "Incomplete sequence") assert.Assert(t, !isManPageHeading("A\b"), "Incomplete sequence") assert.Assert(t, isManPageHeading("A\bA")) assert.Assert(t, isManPageHeading("A\bA B\bB"), "Whitespace can be not-bold") assert.Assert(t, !isManPageHeading("A\bC"), "Different first and last char") assert.Assert(t, !isManPageHeading("a\ba"), "Not ALL CAPS") assert.Assert(t, !isManPageHeading("A\bAX"), "Incomplete sequence") assert.Assert(t, !isManPageHeading(" \b "), "Headings do not start with space") } func TestManPageHeadingFromString_NotBoldSpace(t *testing.T) { // Set a marker style we can recognize and test for ManPageHeading = twin.StyleDefault.WithForeground(twin.NewColor16(2)) result := manPageHeadingFromString("A\bA B\bB") assert.Assert(t, result != nil) assert.Equal(t, len(result.StyledRunes), 3) assert.Equal(t, result.StyledRunes[0], CellWithMetadata{Rune: 'A', Style: ManPageHeading}) assert.Equal(t, result.StyledRunes[1], CellWithMetadata{Rune: ' ', Style: ManPageHeading}) assert.Equal(t, result.StyledRunes[2], CellWithMetadata{Rune: 'B', Style: ManPageHeading}) } func TestManPageHeadingFromString_WithBoldSpace(t *testing.T) { // Set a marker style we can recognize and test for ManPageHeading = twin.StyleDefault.WithForeground(twin.NewColor16(2)) result := manPageHeadingFromString("A\bA \b B\bB") assert.Assert(t, result != nil) assert.Equal(t, len(result.StyledRunes), 3) assert.Equal(t, result.StyledRunes[0], CellWithMetadata{Rune: 'A', Style: ManPageHeading}) assert.Equal(t, result.StyledRunes[1], CellWithMetadata{Rune: ' ', Style: ManPageHeading}) assert.Equal(t, result.StyledRunes[2], CellWithMetadata{Rune: 'B', Style: ManPageHeading}) } moor-2.10.3/internal/textstyles/styledStringSplitter.go000066400000000000000000000237761513574474500234240ustar00rootroot00000000000000package textstyles import ( "fmt" "strings" "unicode/utf8" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal/linemetadata" "github.com/walles/moor/v2/twin" ) const esc = '\x1b' type styledStringSplitter struct { input string lineIndex *linemetadata.Index // Used for error reporting plainTextStyle twin.Style maxTokensCount int reportedRunesCount int nextByteIndex int previousByteIndex int inProgressString strings.Builder inProgressStyle twin.Style numbersBuffer []uint trailer twin.Style callback func(str string, style twin.Style) } // Returns the style of the line's trailer. // // The lineIndex is only used for error reporting. // // maxTokensCount: at most this many tokens will be included in the result. If // 0, do all runes. For BenchmarkRenderHugeLine() performance. func styledStringsFromString(plainTextStyle twin.Style, s string, lineIndex *linemetadata.Index, maxTokensCount int, callback func(string, twin.Style)) twin.Style { if !strings.ContainsAny(s, "\x1b") { // This shortcut makes BenchmarkPlainTextSearch() perform a lot better callback(s, plainTextStyle) return plainTextStyle } splitter := styledStringSplitter{ input: s, lineIndex: lineIndex, plainTextStyle: plainTextStyle, // How plain text should be styled maxTokensCount: maxTokensCount, inProgressStyle: plainTextStyle, // Plain text style until something else comes along callback: callback, trailer: plainTextStyle, // Plain text style until something else comes along } splitter.run() return splitter.trailer } func (s *styledStringSplitter) nextChar() rune { if s.nextByteIndex >= len(s.input) { s.previousByteIndex = s.nextByteIndex return -1 } char, size := utf8.DecodeRuneInString(s.input[s.nextByteIndex:]) s.previousByteIndex = s.nextByteIndex s.nextByteIndex += size return char } // Returns whatever the last call to nextChar() returned func (s *styledStringSplitter) lastChar() rune { if s.previousByteIndex >= len(s.input) { return -1 } char, _ := utf8.DecodeRuneInString(s.input[s.previousByteIndex:]) return char } func (s *styledStringSplitter) run() { char := s.nextChar() for { if char == -1 { s.finalizeCurrentPart() return } if char == esc { escIndex := s.previousByteIndex err := s.handleEscape() if err != nil { header := "" if s.lineIndex != nil { header = fmt.Sprintf("Line %s: ", s.lineIndex.Format()) } failed := s.input[escIndex:s.nextByteIndex] log.Debug(header, "<", strings.ReplaceAll(failed, "\x1b", "ESC"), ">: ", err) // Somewhere in handleEscape(), we got a character that was // unexpected. We need to treat everything up to before that // character as just plain runes. for _, char := range s.input[escIndex:s.previousByteIndex] { s.handleRune(char) } // Start over with the character that caused the problem char = s.lastChar() continue } } else { s.handleRune(char) } char = s.nextChar() } } func (s *styledStringSplitter) handleRune(char rune) { s.inProgressString.WriteRune(char) } func (s *styledStringSplitter) handleEscape() error { char := s.nextChar() if char == '[' { // Got the start of a CSI sequence return s.consumeControlSequence() } if char == ']' { // Got the start of an OSC sequence return s.consumeOsc() } if char == '(' { // Designate G0 charset: https://www.xfree86.org/4.8.0/ctlseqs.html return s.consumeG0Charset() } return fmt.Errorf("Unhandled Fe sequence ESC%c", char) } // Consume a control sequence up until it ends func (s *styledStringSplitter) consumeControlSequence() error { // Points to right after "ESC[" startIndex := s.nextByteIndex // We're looking for a letter to end the CSI sequence for { char := s.nextChar() if char == -1 { return fmt.Errorf("Line ended in the middle of a control sequence") } // Range from here: // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences if char >= 0x30 && char <= 0x3f { // Sequence still in progress continue } // The end, handle what we got endIndexExclusive := s.nextByteIndex return s.handleCompleteControlSequence(s.input[startIndex:endIndexExclusive]) } } func (s *styledStringSplitter) consumeG0Charset() error { // First char after "ESC(" char := s.nextChar() if char == 'B' { // G0 charset is now "B" (ASCII) s.startNewPart(s.plainTextStyle) return nil } return fmt.Errorf("Unhandled G0 charset: %c", char) } // If the whole CSI sequence is ESC[33m, you should call this function with just // "33m". func (s *styledStringSplitter) handleCompleteControlSequence(sequence string) error { if sequence == "K" || sequence == "0K" { // Clear to end of line s.trailer = s.inProgressStyle return nil } lastChar := sequence[len(sequence)-1] if lastChar == 'm' { var newStyle twin.Style var err error newStyle, s.numbersBuffer, err = rawUpdateStyle(s.inProgressStyle, sequence, s.numbersBuffer) if err != nil { return err } s.startNewPart(newStyle) return nil } if lastChar == 'n' { // Device status report, expects us to respond, just ignore them. // // Ref: https://vt100.net/docs/vt510-rm/DSR.html return nil } return fmt.Errorf("Unhandled CSI type %q", lastChar) } // Consume an OSC sequence up until it ends func (s *styledStringSplitter) consumeOsc() error { // Points to right after "ESC]" startIndex := s.nextByteIndex // We're looking for a letter to end the CSI sequence for { char := s.nextChar() if char == -1 { return fmt.Errorf("Line ended in the middle of an OSC sequence") } if char == '\a' { // Got the end of the OSC sequence return s.handleOsc(s.input[startIndex:s.previousByteIndex]) } if char == esc { escIndex := s.previousByteIndex afterEsc := s.nextChar() if afterEsc == '\\' { // Got the end of the OSC sequence return s.handleOsc(s.input[startIndex:escIndex]) } if afterEsc == -1 { return fmt.Errorf("Line ended while ending an OSC sequence") } return fmt.Errorf("Expected OSC sequence to end with BEL or ESC \\ but got ESC %q", afterEsc) } if s.input[startIndex:s.nextByteIndex] == "8;;" { // Special case, here comes an URL return s.handleURL() } } } // Expects an OSC sequence as argument. The terminator is not included, what we // get here is just the payload. func (s *styledStringSplitter) handleOsc(sequence string) error { if strings.HasPrefix(sequence, "133;") && len(sequence) == len("133;A") { // Got ESC]133;X, where "X" could be anything. These are prompt hints, // and rendering those makes no sense. We should just ignore them: // https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md return nil } if strings.HasSuffix(sequence, "?") { // OSC query, we don't intend to answer those, just ignore them. // // Ref: https://github.com/walles/moor/issues/279 return nil } return fmt.Errorf("Unhandled OSC sequence") } // Based on https://infra.spec.whatwg.org/#surrogate func isSurrogate(char rune) bool { if char >= 0xD800 && char <= 0xDFFF { return true } return false } // Non-characters end with 0xfffe or 0xffff, or are in the range 0xFDD0 to 0xFDEF. // // Based on https://infra.spec.whatwg.org/#noncharacter func isNonCharacter(char rune) bool { if char >= 0xFDD0 && char <= 0xFDEF { return true } // Non-characters end with 0xfffe or 0xffff if char&0xFFFF == 0xFFFE || char&0xFFFF == 0xFFFF { return true } return false } // Based on https://url.spec.whatwg.org/#url-code-points func isValidURLChar(char rune) bool { if char == '\\' { // Ref: https://github.com/walles/moor/issues/244#issuecomment-2350908401 return true } if char == '%' { // Needed for % escapes return true } if char == '#' { // Fragment identifier return true } // ASCII alphanumerics if char >= '0' && char <= '9' { return true } if char >= 'A' && char <= 'Z' { return true } if char >= 'a' && char <= 'z' { return true } if strings.ContainsRune("!$&'()*+,-./:;=?@_~", char) { return true } if char < 0x00a0 { return false } if char > 0x10FFFD { return false } if isSurrogate(char) { return false } if isNonCharacter(char) { return false } return true } // We just got ESC]8; and should now read the URL. URLs end with ASCII 7 BEL or ESC \. func (s *styledStringSplitter) handleURL() error { // Points to right after "ESC]8;" urlStartIndex := s.nextByteIndex justSawEsc := false for { char := s.nextChar() if char == -1 { return fmt.Errorf("Line ended in the middle of a URL") } if justSawEsc { if char != '\\' { return fmt.Errorf("Expected ESC \\ but got ESC %q", char) } // End of URL urlEndIndexExclusive := s.nextByteIndex - 2 url := s.input[urlStartIndex:urlEndIndexExclusive] s.startNewPart(s.inProgressStyle.WithHyperlink(&url)) return nil } // Invariant: justSawEsc == false if char == esc { justSawEsc = true continue } if char == '\x07' { // End of URL urlEndIndexExclusive := s.nextByteIndex - 1 url := s.input[urlStartIndex:urlEndIndexExclusive] s.startNewPart(s.inProgressStyle.WithHyperlink(&url)) return nil } if !isValidURLChar(char) { return fmt.Errorf("Invalid URL character: <%q>", char) } // It's a valid URL char, keep going } } func (s *styledStringSplitter) startNewPart(style twin.Style) { if style == twin.StyleDefault { style = s.plainTextStyle } if style == s.inProgressStyle { // No need to start a new part return } s.finalizeCurrentPart() s.inProgressString.Reset() s.inProgressStyle = style } func (s *styledStringSplitter) finalizeCurrentPart() { if s.inProgressString.Len() == 0 { // Nothing to do return } partString := s.inProgressString.String() s.callback(partString, s.inProgressStyle) s.reportedRunesCount += utf8.RuneCountInString(partString) if s.maxTokensCount > 0 && s.reportedRunesCount >= s.maxTokensCount { // We've reported enough runes, stop processing any further input s.nextByteIndex = len(s.input) } } moor-2.10.3/internal/textstyles/styledStringSplitter_test.go000066400000000000000000000127341513574474500244530ustar00rootroot00000000000000package textstyles import ( "strings" "testing" "github.com/walles/moor/v2/twin" "gotest.tools/v3/assert" ) func TestNextCharLastChar_base(t *testing.T) { s := styledStringSplitter{ input: "a", } assert.Equal(t, 'a', s.nextChar()) assert.Equal(t, 'a', s.lastChar()) assert.Equal(t, rune(-1), s.nextChar()) assert.Equal(t, rune(-1), s.lastChar()) } func TestNextCharLastChar_empty(t *testing.T) { s := styledStringSplitter{ input: "", } assert.Equal(t, rune(-1), s.nextChar()) assert.Equal(t, rune(-1), s.lastChar()) } func collectStyledStrings(s string) ([]_StyledString, twin.Style) { styledStrings := []_StyledString{} trailer := styledStringsFromString(twin.StyleDefault, s, nil, 0, func(str string, style twin.Style) { styledStrings = append(styledStrings, _StyledString{str, style}) }) return styledStrings, trailer } // We should ignore OSC 133 sequences. // // Ref: // https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md func TestIgnorePromptHints(t *testing.T) { // From an e-mail I got titled "moor question: "--RAW-CONTROL-CHARS" equivalent" styledStrings, trailer := collectStyledStrings("hell\x1b]133;A\x1b\\o") assert.Equal(t, twin.StyleDefault, trailer) assert.Equal(t, 1, len(styledStrings)) assert.Equal(t, "hello", styledStrings[0].String) assert.Equal(t, twin.StyleDefault, styledStrings[0].Style) // C rather than A, different end-of-sequence, should also be ignored styledStrings, trailer = collectStyledStrings("ass\x1b]133;C\x07ertion") assert.Equal(t, twin.StyleDefault, trailer) assert.Equal(t, 1, len(styledStrings)) assert.Equal(t, "assertion", styledStrings[0].String) assert.Equal(t, twin.StyleDefault, styledStrings[0].Style) } // We should ignore OSC queries. They are any sequence ending with a question // mark, and expect some response from the terminal. We are not a terminal, so we // ignore them. // // Ref: https://github.com/walles/moor/issues/279 func TestIgnoreQueries(t *testing.T) { styledStrings, trailer := collectStyledStrings("\x1b]11;?\x1b\\hello") assert.Equal(t, twin.StyleDefault, trailer) assert.Equal(t, 1, len(styledStrings)) assert.Equal(t, "hello", styledStrings[0].String) assert.Equal(t, twin.StyleDefault, styledStrings[0].Style) } // Unsure why colon separated colors exist, but the fact is that a number of // things emit colon separated SGR codes. And numerous terminals accept them // (search page for "delimiter"): https://github.com/kovidgoyal/kitty/issues/7 // // Johan got an e-mail titled "moor question: "--RAW-CONTROL-CHARS" equivalent" // about the sequence we're testing here. func TestColonColors(t *testing.T) { styledStrings, trailer := collectStyledStrings("\x1b[38:5:238mhello") assert.Equal(t, twin.StyleDefault, trailer) assert.Equal(t, 1, len(styledStrings)) assert.Equal(t, "hello", styledStrings[0].String) assert.Equal(t, twin.StyleDefault.WithForeground(twin.NewColor256(238)), styledStrings[0].Style) } // Test handling of URLs with backslashes in them. Generated by some software on // Windows, also accepted by less. // // Ref: https://github.com/walles/moor/issues/244 func TestWindowsURL(t *testing.T) { windowsPath := `src\detection\cpu\cpu_bsd.c` windowsCwd := `C:\msys64\home\zhang\fastfetch\src\detection\cpu\cpu_bsd.c` styledStrings, trailer := collectStyledStrings("\x1b]8;;vscode://file/" + windowsCwd + "\\" + windowsPath + "\x07" + windowsPath) assert.Equal(t, twin.StyleDefault, trailer) assert.Equal(t, 1, len(styledStrings)) assert.Equal(t, windowsPath, styledStrings[0].String) } func TestURLWithSpace(t *testing.T) { urlPath := `hello%20there.txt` text := `hello there.txt` styledStrings, trailer := collectStyledStrings("\x1b]8;;file://" + urlPath + "\x1b\\" + text) assert.Equal(t, twin.StyleDefault, trailer) assert.Equal(t, 1, len(styledStrings)) assert.Equal(t, text, styledStrings[0].String) assert.Assert(t, strings.Contains(styledStrings[0].Style.String(), "file://"+urlPath)) } func TestPlainTextColor(t *testing.T) { plainTextStyle := twin.StyleDefault.WithAttr(twin.AttrReverse) styledStrings := []_StyledString{} trailer := styledStringsFromString(plainTextStyle, "a\x1b[33mb\x1b[mc", nil, 0, func(str string, style twin.Style) { styledStrings = append(styledStrings, _StyledString{str, style}) }) assert.Equal(t, 3, len(styledStrings)) assert.Equal(t, "a", styledStrings[0].String) assert.Equal(t, plainTextStyle, styledStrings[0].Style) assert.Equal(t, "b", styledStrings[1].String) assert.Equal(t, plainTextStyle.WithForeground(twin.NewColor16(3)), styledStrings[1].Style) assert.Equal(t, "c", styledStrings[2].String) assert.Equal(t, plainTextStyle, styledStrings[2].Style) assert.Equal(t, plainTextStyle, trailer) } // Ignore G0 charset resets (`ESC(B`). They are output by "tput sgr0" on at // least TERM=xterm-256color. // // Ref: // - https://github.com/walles/moor/issues/276 // - https://www.xfree86.org/4.8.0/ctlseqs.html (look for "Designate G0" and // "USASCII") func TestRestoreG0CharSet(t *testing.T) { styledStrings, trailer := collectStyledStrings("\x1b(Bhello") assert.Equal(t, twin.StyleDefault, trailer) assert.Equal(t, 1, len(styledStrings)) assert.Equal(t, "hello", styledStrings[0].String) assert.Equal(t, twin.StyleDefault, styledStrings[0].Style) } func TestUnsupportedG0CharSet(t *testing.T) { styledStrings, trailer := collectStyledStrings("\x1b(Xhello") assert.Equal(t, twin.StyleDefault, trailer) assert.Equal(t, 1, len(styledStrings)) assert.Equal(t, "\x1b(Xhello", styledStrings[0].String) } moor-2.10.3/internal/util/000077500000000000000000000000001513574474500153615ustar00rootroot00000000000000moor-2.10.3/internal/util/format.go000066400000000000000000000011231513574474500171750ustar00rootroot00000000000000package util import "fmt" // Formats a positive number into a string with _ between each three-group of // digits, for numbers >= 10_000. // // Regarding the >= 10_000 exception: // https://en.wikipedia.org/wiki/Decimal_separator#Exceptions_to_digit_grouping func FormatInt(i int) string { if i < 10_000 { return fmt.Sprint(i) } result := "" chars := []rune(fmt.Sprint(i)) addCount := 0 for i := len(chars) - 1; i >= 0; i-- { char := chars[i] if len(result) > 0 && addCount%3 == 0 { result = "_" + result } result = string(char) + result addCount++ } return result } moor-2.10.3/internal/util/format_test.go000066400000000000000000000010361513574474500202370ustar00rootroot00000000000000package util import ( "testing" "gotest.tools/v3/assert" ) func TestFormatInt(t *testing.T) { assert.Equal(t, "1", FormatInt(1)) assert.Equal(t, "10", FormatInt(10)) assert.Equal(t, "100", FormatInt(100)) // Ref: // https://en.wikipedia.org/wiki/Decimal_separator#Exceptions_to_digit_grouping assert.Equal(t, "1000", FormatInt(1000)) assert.Equal(t, "10_000", FormatInt(10000)) assert.Equal(t, "100_000", FormatInt(100000)) assert.Equal(t, "1_000_000", FormatInt(1000000)) assert.Equal(t, "10_000_000", FormatInt(10000000)) } moor-2.10.3/internal/util/twinlogger.go000066400000000000000000000005231513574474500200710ustar00rootroot00000000000000package util import log "github.com/sirupsen/logrus" // TwinLogger adapts logrus to the twin.Logger interface type TwinLogger struct{} func (l *TwinLogger) Debug(message string) { log.Debug(message) } func (l *TwinLogger) Info(message string) { log.Info(message) } func (l *TwinLogger) Error(message string) { log.Error(message) } moor-2.10.3/manual-test.sh000077500000000000000000000020451513574474500153620ustar00rootroot00000000000000#!/bin/bash set -e -o pipefail # Test all permutations of the following: # - Read from file vs from stdin # - 'q' to quit vs 'v' to launch an editor # - Terminal editor (nano) or GUI editor (code -w) read -r -p "Press enter to start testing, then q to exit the pager" clear # With --trace we always get a non-zero exit code ./moor.sh --trace moor.sh || true echo read -r -p "Press enter to continue, then q to exit the pager" clear ./moor.sh --trace &2 ./moor "$@" moor-2.10.3/moor.1000066400000000000000000000124021513574474500136250ustar00rootroot00000000000000.TH MOOR 1 2022-07-21 .SH NAME moor \- the nice pager .SH SYNOPSIS .B moor [options] .IR file \&.\|.\|. .br .B "moor \-\-help" .br .B "moor \-\-version" .SH DESCRIPTION .B moor is a pager much like .I less (1), but with generally nicer out-of-the-box behavior. .PP More information and screenshots: https://github.com/walles/moor#readme .PP Inside of \fBmoor\fR, press .B h to access the built-in help. .PP Input is expected to be (optionally compressed) UTF-8 text. Invalid / unprintable characters are by default rendered as '?'. .PP If you have opened multiple files, press .B : to switch between them. .SH OPTIONS Multiple-choice options all have the default value listed first. .PP All of these options can be appended to the .B MOOR environment variable for persistent configuration. .PP Doing .B moor --help will also list these options. .TP \fB\-\-colors\fR={\fBauto\fR | \fB8\fR | \fB16\fR | \fB256\fR | \fB16M\fR} Size of color palette we output to the terminal .TP \fB\-\-debug\fR Print debug logs after exiting, less verbose than .B \-\-trace .TP \fB\-\-follow\fR Scrolls automatically to follow piped input, just like .B tail \-f .TP \fB\-\-lang\fR=string Used for highlighting. Without this flag highlighting is based on the input file name. Valid values are MIME types like \fBtext/x-markdown\fP, file extensions like \fBmd\fP or language names like \fBmarkdown\fP. For the source of truth on what is supported exactly, look in https://github.com/alecthomas/chroma/tree/master/lexers/embedded or its parent directory. .TP \fB\-\-mousemode\fR={\fBauto\fR | \fBselect\fR | \fBscroll\fR} Guarantee selecting text with the mouse works but maybe not mouse scrolling. Or guarantee mouse scrolling works but selecting text requiring extra effort. Details here: https://github.com/walles/moor/blob/master/MOUSE.md .TP \fB\-\-no\-clear\-on\-exit\fR Retain screen contents when exiting moor. Affected by \fB--no-clear-on-exit-margin\fP. .TP \fB\-\-no\-clear\-on\-exit\-margin\fR=int Leave this number of lines for your shell prompt after exiting. Defaults to 1. Affects \fB--no-clear-on-exit\fP and \fB--quit-if-one-screen\fP. .TP \fB\-\-no\-linenumbers\fR Hide line numbers on startup, press left arrow key to show .TP \fB\-\-no\-reformat\fR No effect, exists for backwards compatibility. See --reformat. .TP .TP \fB\-\-no\-search\-line\-highlight\fR Do not highlight the background of lines with search hits. The search hits themselves are still highlighted though, even with this option. .TP \fB\-\-no\-statusbar\fR Hide the status bar, toggle with .B = .TP \fB\-\-quit\-if\-one\-screen\fR Print input contents without paging if the input fits on one screen. Affected by \fB--no-clear-on-exit-margin\fP. .TP \fB\-\-reformat\fR Reformat supported input files (JSON) before showing them. .TP \fB\-\-render\-unprintable\fR={\fBhighlight\fR | \fBwhitespace\fR} How unprintable characters are rendered .TP \fB\-\-scroll\-left\-hint\fR=string UTF-8 character indicating the view can scroll left, defaults to an inverse \fB<\fR. This can be a string containing ANSI formatting. The word .B ESC in caps will be interpreted as one escape character. Example value for faint (using ANSI SGR code 2) tilde characters: .B ESC[2m~ .TP \fB\-\-scroll\-right\-hint\fR=string UTF-8 character indicating the view can scroll right, defaults to an inverse \fB>\fR. This can be a string containing ANSI formatting. The word .B ESC in caps will be interpreted as one escape character. Example value for faint (using ANSI SGR code 2) tilde characters: .B ESC[2m~ .TP \fB\-\-shift\fR=int Arrow keys side scroll amount. Or try ALT+arrow to scroll one column at a time. .TP \fB\-\-statusbar\fR={\fBinverse\fR | \fBplain\fR | \fBbold\fR} Status bar style .TP \fB\-\-style\fR={\fBnative\fR | \fIstyle\fR} Highlighting style from https://xyproto.github.io/splash/docs/longer/all.html .TP \fB\-\-tab\-size\fR=int Number of spaces per tab stop, defaults to 8. Or try .B CTRL-t to toggle when .B moor is running. .TP \fB\-\-terminal\-fg\fR Use terminal foreground color rather than style foreground color for unstyled text. Try this if your terminal window has a background image rather than a solid color. .TP \fB\-\-trace\fR Print trace logs after exiting, more verbose than .B \-\-debug .TP \fB\-\-wrap\fR Wrap long lines, toggle with .B w .TP \fB\+\1234\fR Immediately scroll to line .B 1234 .SH FILES .TP .B $XDG_DATA_HOME/moor/search_history Moor will store your search history in this file. If $XDG_DATA_HOME is not set, the file will be stored in the default XDG location, usually \fB~/.local/share/moor/search_history\fR. .SH ENVIRONMENT .TP .B LESSSECURE Setting this to "1" prevents moor from opening new files or launching external programs, as required by .B systemctl(1)\&. In secure mode, the "v" command for opening the current file in an editor is disabled, and the search history file is not updated. .TP .B MOOR Additional options are read from this variable if it is set, just as if those same options had been manually added to each moor invocation. Try setting it to \fB\-\-reformat\fR to have JSON input automatically reformatted! .TP .B PAGER If set to "moor", many programs will use .B moor as their pager. .TP .B PAGER_LABEL Other programs can set this to tell moor what name to show for stdin input. .SH BUGS Kindly report any bugs here: https://github.com/walles/moor/issues moor-2.10.3/moor.sh000077500000000000000000000004001513574474500140750ustar00rootroot00000000000000#!/bin/bash # Build pager and run it, this script should behave just # like the binary. set -e -o pipefail MYDIR="$( cd "$(dirname "$0")" pwd )" cd "$MYDIR" rm -f moor RACE=-race ./build.sh 1>&2 GORACE="log_path=moor-race-report" ./moor "$@" moor-2.10.3/pkg/000077500000000000000000000000001513574474500133515ustar00rootroot00000000000000moor-2.10.3/pkg/moor/000077500000000000000000000000001513574474500143255ustar00rootroot00000000000000moor-2.10.3/pkg/moor/embed-api.go000066400000000000000000000105101513574474500164740ustar00rootroot00000000000000// Public API for embedding a pager in your application package moor import ( "fmt" "io" "os" "strings" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/formatters" log "github.com/sirupsen/logrus" "github.com/walles/moor/v2/internal" internalReader "github.com/walles/moor/v2/internal/reader" "github.com/walles/moor/v2/twin" "golang.org/x/term" ) const logLevel = log.WarnLevel // If you feel some option is missing, make PRs at // https://github.com/walles/moor/pulls open an issue. type Options struct { // Name displayed in the bottom left corner of the pager. // // Defaults to the file name when paging files, otherwise nothing. Leave // blank for default. Title string // The default is to auto format JSON input. Set this to true to disable // auto formatting. NoAutoFormat bool // The default is to truncate long lines, and let the user press right-arrow // to see more of them. Set this to true to wrap long lines instead. Users // can toggle wrapping on / off using the 'w' key while paging. WrapLongLines bool // The default is to show line numbers. Set this to true to disable line // numbers. The user can toggle line numbers on by pressing the left-arrow // key while paging. NoLineNumbers bool // The default is to always start the pager. If this is set to true, short // input will just be printed, and no paging will happen. QuitIfOneScreen bool } // If stdout is not a terminal, the stream contents will just be printed to // stdout. func PageFromStream(reader io.Reader, options Options) error { logs := startLogCollection() defer collectLogs(logs) if !term.IsTerminal(int(os.Stdout.Fd())) { return dumpToStdoutAndClose(reader) } pagerReader, err := internalReader.NewFromStream( options.Title, reader, getColorFormatter(), internalReader.ReaderOptions{ ShouldFormat: !options.NoAutoFormat, }) if err != nil { return err } return pageFromReader(pagerReader, options) } // If stdout is not a terminal, the file contents will just be printed to // stdout. func PageFromFile(name string, options Options) error { logs := startLogCollection() defer collectLogs(logs) if !term.IsTerminal(int(os.Stdout.Fd())) { stream, err := os.Open(name) if err != nil { return err } return dumpToStdoutAndClose(stream) } pagerReader, err := internalReader.NewFromFilename( name, getColorFormatter(), internalReader.ReaderOptions{ ShouldFormat: !options.NoAutoFormat, }) if err != nil { return err } if options.Title != "" { pagerReader.DisplayName = &options.Title } return pageFromReader(pagerReader, options) } // If stdout is not a terminal, the string contents will just be printed to // stdout. func PageFromString(text string, options Options) error { // NOTE: Pager froze when I tried to use internalReader.NewFromText() here. // If you want to try that again, make sure to test it using some external // test program! return PageFromStream(strings.NewReader(text), options) } func startLogCollection() *internal.LogWriter { log.SetLevel(logLevel) var logLines internal.LogWriter log.SetOutput(&logLines) return &logLines } func collectLogs(logs *internal.LogWriter) { if len(logs.String()) == 0 { return } fmt.Fprintln(os.Stderr, logs.String()) } func dumpToStdoutAndClose(reader io.Reader) error { _, err := io.Copy(os.Stdout, reader) if err != nil { return err } // Close the reader if we can if closer, ok := reader.(io.Closer); ok { err := closer.Close() if err != nil { return err } } return nil } func getColorFormatter() chroma.Formatter { if os.Getenv("COLORTERM") != "truecolor" && strings.Contains(os.Getenv("TERM"), "256") { // Covers "xterm-256color" as used by the macOS Terminal return formatters.TTY256 } return formatters.TTY16m } func pageFromReader(reader *internalReader.ReaderImpl, options Options) error { pager := internal.NewPager(reader) pager.WrapLongLines = options.WrapLongLines pager.ShowLineNumbers = !options.NoLineNumbers pager.QuitIfOneScreen = options.QuitIfOneScreen screen, e := twin.NewScreen() if e != nil { // Screen setup failed return e } style := internal.GetStyleForScreen(screen) reader.SetStyleForHighlighting(style) formatter := getColorFormatter() pager.StartPaging(screen, &style, &formatter) screen.Close() if !pager.DeInit { pager.ReprintAfterExit() } return nil } moor-2.10.3/pkg/moor/embed-api_test.go000066400000000000000000000037271513574474500175470ustar00rootroot00000000000000// NOTE: This file ensures the API compiles. // // Actually running the API has been done manually using a separate external // program by Johan Walles on 2025aug09. package moor // NOTE: No imports from internal allowed here!! Externals cannot do that, so if // we have to that means the whole external API is broken. import ( "bytes" "fmt" "os" "testing" ) // This function is not meant to be called (because then it would start paging // which is impractical during testing). It's just here to demonstrate how the // API can be used, and to ensure the API compiles. func demoPageFromFile() { err := PageFromFile("/etc/services", Options{}) if err != nil { fmt.Printf("%v\n", err) os.Exit(1) } } // Inspired from here: // https://github.com/Friends-Of-Noso/NosoData-Go/blob/82de894968e752d6d93d779ecf57db78b10c4acf/cmd/block.go#L145-L163 // // This function is not meant to be called (because then it would start paging // which is impractical during testing). It's just here to demonstrate how the // API can be used, and to ensure the API compiles. func demoPageFromStream() { blockNumber := 12_345 buf := new(bytes.Buffer) err := PageFromStream(buf, Options{ Title: fmt.Sprintf("Block: %d", blockNumber), WrapLongLines: true, }) if err != nil { fmt.Printf("%v\n", err) os.Exit(1) } } // This function is not meant to be called (because then it would start paging // which is impractical during testing). It's just here to demonstrate how the // API can be used, and to ensure the API compiles. func demoPageFromString() { err := PageFromString("Hello, world!", Options{}) if err != nil { fmt.Printf("%v\n", err) os.Exit(1) } } func TestEmbedApi(t *testing.T) { // Never call these functions! That would launch pagers, and we don't want // that during testing. // // But we still want to have a call to it (that we never make) to make the // linter stop complaining. if false { demoPageFromFile() demoPageFromStream() demoPageFromString() } } moor-2.10.3/release.sh000077500000000000000000000033471513574474500145560ustar00rootroot00000000000000#!/bin/bash set -e -o pipefail echo "Running tests before making the release..." ./test.sh # Bail if we're on a dirty version if [ -n "$(git diff --stat)" ]; then echo "ERROR: Please commit all changes before doing a release" echo git status exit 1 fi # List existing version numbers... echo echo "Previous version numbers:" git tag | sort -V | tail # ... and ask for a new version number. echo echo "Please provide a version number on the form 'v1.2.3' for the new release:" read -r VERSION # https://github.com/walles/moor/issues/47 if ! echo "${VERSION}" | grep -q -E '^v[0-9]+\.[0-9]+\.[0-9]+$'; then echo "ERROR: Version number must be on the form: v1.2.3: ${VERSION}" exit 1 fi # List changes since last release as inspiration... LAST_VERSION="$(git describe --abbrev=0)" echo # FIXME: Make this part of the editable tagging message echo "Changes since last release:" git log --first-parent --pretty="format:* %s" "${LAST_VERSION}"..HEAD | sed 's/ diff.*//' echo echo # Make an annotated tag for this release git tag --annotate "${VERSION}" # NOTE: To get the version number right, these builds must be done after the # above tagging. # # NOTE: Make sure this list matches the one in test.sh GOOS=linux GOARCH=386 ./build.sh GOOS=linux GOARCH=amd64 ./build.sh GOOS=linux GOARCH=arm ./build.sh # Ref: https://github.com/walles/moor/issues/122 GOOS=darwin GOARCH=amd64 ./build.sh GOOS=darwin GOARCH=arm64 ./build.sh GOOS=windows GOARCH=amd64 ./build.sh # Push the newly built release tag git push --tags # FIXME: Instead of asking the user to upload the binaries, upload them for # the user. echo echo "Please upload the following binaries to :" file releases/moor-"${VERSION}"-*-* moor-2.10.3/releases/000077500000000000000000000000001513574474500143735ustar00rootroot00000000000000moor-2.10.3/releases/README.md000066400000000000000000000001101513574474500156420ustar00rootroot00000000000000The `release.sh` and the `test.sh` scripts will populate this directory.moor-2.10.3/sample-files/000077500000000000000000000000001513574474500151515ustar00rootroot00000000000000moor-2.10.3/sample-files/8-bit-color.txt000066400000000000000000000002441513574474500177510ustar00rootroot00000000000000LESS_TERMCAP_mb LESS_TERMCAP_md LESS_TERMCAP_me LESS_TERMCAP_se LESS_TERMCAP_so LESS_TERMCAP_ue LESS_TERMCAP_us moor-2.10.3/sample-files/colored-underlines.txt000066400000000000000000000000741513574474500215100ustar00rootroot00000000000000[58:5:196mRed underline Default colored underline moor-2.10.3/sample-files/compressed-markdown.md.gz000066400000000000000000000000531513574474500220740ustar00rootroot00000000000000‹êfmarkdown.mdSVðM,ÊNÉ/Ïã>ÆT moor-2.10.3/sample-files/compressed.txt.bz2000066400000000000000000000001011513574474500205420ustar00rootroot00000000000000BZh91AY&SYѨ²Ó€@/fØ "#OH0jÀ4Ùn(dhuÖ&êÈGŸ‹¹"œ(Hh¿ÔYmoor-2.10.3/sample-files/compressed.txt.gz000066400000000000000000000000731513574474500204750ustar00rootroot00000000000000‹ÈÐ]compressed.txt ÉÈ,V¢D…äüÜ‚¢ÔââÔ…´ÌœT.¡œ÷µmoor-2.10.3/sample-files/compressed.txt.xz000066400000000000000000000001241513574474500205130ustar00rootroot00000000000000ý7zXZæÖ´F!t/å£This is a compressed file þBy‘6 Date: Thu Oct 24 20:45:40 2013 +0200 Prioritize the TODO file diff --git a/TODO.txt b/TODO.txt index 52551e7..663ffd7 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,36 +1,48 @@ Moar is a pager. It's designed to be easy to use and just do the right thing without any configuration.  -TODO (in some order): +TODO (before using it myself) +----------------------------- +* Scroll down one line on RETURN  -* Write "/ to search" somewhere in the status field +* Enable displaying ANSI-colored input + + +TODO (before github) +--------------------  +TODO (before trying to get others to use it) +-------------------------------------------- * Do a regexp search if the search term is a valid regexp, otherwise just use it as a substring.  -* Make the search case sensitive only if it contains any capital - letters. This goes for both regexps and non-regexps. - * Make sure we can search for unicode characters  -* Make sure searching for an upper case unicode character turns on - case sensitive search. - * Make sure we get the line length right even with unicode characters present in the lines. Verify by looking at where the truncation markers end up.  -* Scroll down one line on RETURN - * Enable sideways scrolling using arrow keys.  * Handle search hits to the right of the right screen edge  -* Interactive search using ^s and ^r like in Emacs +* Enable 'h' or '?' for help  -* Highlight all matches while searching +* Report command line errors, think about when to use $stdin for input + vs what commands we accept  -* Enable displaying ANSI-colored input + +TODO (bonus) +------------ +* Make the search case sensitive only if it contains any capital + letters. This goes for both regexps and non-regexps. + +* Make sure searching for an upper case unicode character turns on + case sensitive search. + +* Write "/ to search" somewhere in the status field + +* Interactive search using ^s and ^r like in Emacs  * Enable filtered input, start with zcat as a filter  @@ -47,11 +59,6 @@ TODO (in some order):  * Enable up / down using the mouse wheel.  -* Enable 'h' or '?' for help - -* Report command line errors, think about when to use $stdin for input - vs what commands we accept - * Enable pass-through operation unless $stdout.isatty()  * Doing moor.rb on an arbitrary binary (like /bin/ls) should put all @@ -60,7 +67,8 @@ TODO (in some order): various control characters.   -DONE: +DONE +---- * Enable exiting using q (restores screen)  * Handle the terminal window getting resized. @@ -121,3 +129,5 @@ DONE: * Indicate when we're wrapping the search while pressing n.  * Indicate when we're wrapping the search while pressing N. + +* Highlight all matches while searching moor-2.10.3/sample-files/green-gradient.txt000066400000000000000000000076341513574474500206170ustar00rootroot00000000000000                                                                                                                                 ################################################################################################################################ moor-2.10.3/sample-files/hej.txt000066400000000000000000000237421513574474500164700ustar00rootroot00000000000000 PRINTF(1) BSD General Commands Manual PRINTF(1) NNAAMMEE pprriinnttff -- formatted output SSYYNNOOPPSSIISS pprriinnttff _f_o_r_m_a_t [_a_r_g_u_m_e_n_t_s _._._.] DDEESSCCRRIIPPTTIIOONN The pprriinnttff utility formats and prints its arguments, after the first, under control of the _f_o_r_m_a_t. The _f_o_r_m_a_t is a character string which con- tains three types of objects: plain characters, which are simply copied to standard output, character escape sequences which are converted and copied to the standard output, and format specifications, each of which causes printing of the next successive _a_r_g_u_m_e_n_t. The _a_r_g_u_m_e_n_t_s after the first are treated as strings if the corresponding format is either cc, bb or ss; otherwise it is evaluated as a C constant, with the following extensions: ++oo A leading plus or minus sign is allowed. ++oo If the leading character is a single or double quote, the value is the ASCII code of the next character. The format string is reused as often as necessary to satisfy the _a_r_g_u_m_e_n_t_s. Any extra format specifications are evaluated with zero or the null string. Character escape sequences are in backslash notation as defined in the ANSI X3.159-1989 (``ANSI C89''), with extensions. The characters and their meanings are as follows: \\aa Write a character. \\bb Write a character. \\cc Ignore remaining characters in this string. \\ff Write a character. \\nn Write a character. \\rr Write a character. \\tt Write a character. \\vv Write a character. \\'' Write a character. \\\\ Write a backslash character. \\_n_u_m \\00_n_u_m Write an 8-bit character whose ASCII value is the 1-, 2-, or 3-digit octal number _n_u_m. Each format specification is introduced by the percent character (``%''). The remainder of the format specification includes, in the following order: Zero or more of the following flags: ## A `#' character specifying that the value should be printed in an ``alternate form''. For cc, dd, and ss, for- mats, this option has no effect. For the oo formats the precision of the number is increased to force the first character of the output string to a zero. For the xx (XX) format, a non-zero result has the string 0x (0X) prepended to it. For ee, EE, ff, gg, and GG, formats, the result will always contain a decimal point, even if no digits follow the point (normally, a decimal point only appears in the results of those formats if a digit fol- lows the decimal point). For gg and GG formats, trailing zeros are not removed from the result as they would oth- erwise be; -- A minus sign `-' which specifies _l_e_f_t _a_d_j_u_s_t_m_e_n_t of the output in the indicated field; ++ A `+' character specifying that there should always be a sign placed before the number when using signed formats. ` ' A space specifying that a blank should be left before a positive number for a signed format. A `+' overrides a space if both are used; 00 A zero `0' character indicating that zero-padding should be used rather than blank-padding. A `-' overrides a `0' if both are used; Field Width: An optional digit string specifying a _f_i_e_l_d _w_i_d_t_h; if the output string has fewer characters than the field width it will be blank-padded on the left (or right, if the left-adjustment indi- cator has been given) to make up the field width (note that a leading zero is a flag, but an embedded zero is part of a field width); Precision: An optional period, `..', followed by an optional digit string giving a _p_r_e_c_i_s_i_o_n which specifies the number of digits to appear after the decimal point, for ee and ff formats, or the maximum num- ber of characters to be printed from a string; if the digit string is missing, the precision is treated as zero; Format: A character which indicates the type of format to use (one of ddiioouuxxXXffFFeeEEggGGaaAAccssbb). The uppercase formats differ from their low- ercase counterparts only in that the output of the former is entirely in uppercase. The floating-point format specifiers (ffFFeeEEggGGaaAA) may be prefixed by an LL to request that additional precision be used, if available. A field width or precision may be `**' instead of a digit string. In this case an _a_r_g_u_m_e_n_t supplies the field width or precision. The format characters and their meanings are: ddiioouuXXxx The _a_r_g_u_m_e_n_t is printed as a signed decimal (d or i), unsigned octal, unsigned decimal, or unsigned hexadecimal (X or x), respectively. ffFF The _a_r_g_u_m_e_n_t is printed in the style `[-]ddd.ddd' where the number of d's after the decimal point is equal to the preci- sion specification for the argument. If the precision is missing, 6 digits are given; if the precision is explicitly 0, no digits and no decimal point are printed. The values infinity and _N_a_N are printed as `inf' and `nan', respec- tively. eeEE The _a_r_g_u_m_e_n_t is printed in the style ee `[-_d_._d_d_d+-_d_d]' where there is one digit before the decimal point and the number after is equal to the precision specification for the argu- ment; when the precision is missing, 6 digits are produced. The values infinity and _N_a_N are printed as `inf' and `nan', respectively. ggGG The _a_r_g_u_m_e_n_t is printed in style ff (FF) or in style ee (EE) whichever gives full precision in minimum space. aaAA The _a_r_g_u_m_e_n_t is printed in style `[-_h_._h_h_h+-p_d]' where there is one digit before the hexadecimal point and the number after is equal to the precision specification for the argu- ment; when the precision is missing, enough digits are pro- duced to convey the argument's exact double-precision float- ing-point representation. The values infinity and _N_a_N are printed as `inf' and `nan', respectively. cc The first character of _a_r_g_u_m_e_n_t is printed. ss Characters from the string _a_r_g_u_m_e_n_t are printed until the end is reached or until the number of characters indicated by the precision specification is reached; however if the precision is 0 or missing, all characters in the string are printed. bb As for ss, but interpret character escapes in backslash nota- tion in the string _a_r_g_u_m_e_n_t. %% Print a `%'; no argument is used. The decimal point character is defined in the program's locale (category LC_NUMERIC). In no case does a non-existent or small field width cause truncation of a field; padding takes place only if the specified field width exceeds the actual width. EEXXIITT SSTTAATTUUSS The pprriinnttff utility exits 0 on success, and >0 if an error occurs. CCOOMMPPAATTIIBBIILLIITTYY The traditional BSD behavior of converting arguments of numeric formats not beginning with a digit to the ASCII code of the first character is not supported. SSEEEE AALLSSOO echo(1), printf(3) SSTTAANNDDAARRDDSS The pprriinnttff command is expected to be mostly compatible with the IEEE Std 1003.2 (``POSIX.2'') specification. HHIISSTTOORRYY The pprriinnttff command appeared in 4.3BSD-Reno. It is modeled after the standard library function, printf(3). BBUUGGSS Since the floating point numbers are translated from ASCII to floating- point and then back again, floating-point precision may be lost. (By default, the number is translated to an IEEE-754 double-precision value before being printed. The LL modifier may produce additional precision, depending on the hardware platform.) ANSI hexadecimal character constants were deliberately not provided. The escape sequence \000 is the string terminator. When present in the argument for the bb format, the argument will be truncated at the \000 character. Multibyte characters are not recognized in format strings (this is only a problem if `%' can appear inside a multibyte character). Parsing of - arguments is also somewhat different from printf(3), where unknown arguments are simply printed instead of being flagged as errors. BSD April 14, 2005 BSD moor-2.10.3/sample-files/invalid-utf8.txt000066400000000000000000000000161513574474500202210ustar00rootroot00000000000000start  end moor-2.10.3/sample-files/issue-290-repro.txt000066400000000000000000001721041513574474500205040ustar00rootroot00000000000000commit e7be30a73d9fc6c43264ab102ef73e0105d560db Author: Some Body Date: Tue Jul 8 20:49:54 2025 +0000 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum Δ foo/bar/baz/qux/file.json ─────────────────────────────────────────────────────────────────────────────── ──────â” • 46: │ ──────┘  },  "method": "post",  "body": {  "cluster": "https://a.cluster.some.where.in.the.world/",  "cluster": "@parameters('somecluster')",  "db": "somedb",  "csl": "nisi in_Excepteur = proident,([1, 2, 3, 4, 5, 6, 7, 8]);\nanim Excepteur = (irure: nulla){culpa !est \"esse11\" voluptate occaecat !in \"consectetur27\" magna Duis !ea \"consequat.61\" elit, elit, !deserunt \"ut28\" officia consequat. !commodo \"Lorem12\" dolore aliquip !nulla \"Lorem62\"};\nsit aute200 = tempor2 \n| pariatur. consequat. magna \"sunt200\"\n| ex est;\nconsequat. et_occaecat = \n nulla('ex://nisi.est3.irure.cupidatat.deserunt/').ipsum('voluptate').anim\n | voluptate incididunt, deserunt, Excepteur, aliqua. = esse, Ut;\nsed dolore_incididunt = \n culpa('elit,://elit,.consectetur.Ut.et.sint').mollit('aute').in1 \n | magna ipsum, aute = sed;\nculpa culpa = magna(\n Ut: culpa,\n dolor: dolore,\n quis: velit,\n dolore: nostrud,\n mollit_et_officia: sint\n)\n [\n // mollit100 \n 'deserunt100', 'dolore0', 'labore11non0ullamco0', 1, 2,\n \"sit100\", 'ut1', 'in21irure0anim0', 2, 3,\n \"in100\", 'ea2', 'eu75Excepteur0ad0', 3, 6,\n \"anim100\", 'anim3', 'eu85veniam,0veniam,0', 4, 7,\n \"cillum100\", 'cillum4', 'mollit139nostrud0sint0', 5, 9,\n \"ut100\", 'reprehenderit5', 'in149ipsum0laborum.0', 6, 10,\n \"in100\", 'reprehenderit6', 'consectetur203Excepteur0et0', 7, 12,\n \"consequat.100\", 'cupidatat7', 'minim213in0ipsum0', 8, 13,\n // pariatur.200\n 'in200', 'enim0', 'sint11Lorem0cupidatat0', 1, 2,\n 'nisi200', 'in1', 'ullamco26laborum.0nulla0', 2, 3,\n 'eiusmod200', 'in2', 'velit75nostrud0nostrud0', 3, 6,\n 'est200', 'Ut3', 'deserunt90dolor0sit0', 4, 7,\n 'id200', 'aliquip4', 'in138ut0reprehenderit0', 5, 9,\n 'reprehenderit200', 'minim5', 'in153sint0exercitation0', 6, 10,\n 'et200', 'commodo6', 'dolore202ipsum0aliquip0', 7, 12,\n 'occaecat200', 'velit7', 'aliquip217dolore0culpa0', 8, 13,\n // commodo100\n 'do100', 'non0', 'occaecat7nostrud0ut0', 6, 6,\n 'Lorem100', 'culpa1', 'Excepteur14veniam,0Excepteur0', 7, 7,\n 'occaecat100', 'deserunt2', 'cillum21et0nostrud0', 8, 8,\n 'laboris100', 'Lorem3', 'labore102mollit0non0', 5, 5,\n 'ut100', 'deserunt4', 'consequat.154ex0voluptate0', 1, 1,\n 'in100', 'sed5', 'incididunt170cupidatat0est0', 3, 3,\n 'ea100', 'velit6', 'ad186tempor0Ut0', 4, 4,\n 'laborum.100', 'sed7', 'reprehenderit218in0ullamco0', 2, 2,\n // nulla200\n 'occaecat200', 'in0', 'velit7aliqua.0incididunt0', 6, 6,\n 'aliquip200', 'esse1', 'adipiscing14dolore0magna0', 7, 7,\n 'nisi200', 'est2', 'in21ullamco0Ut0', 8, 8,\n 'non200', 'et3', 'ut102irure0sit0', 5, 5,\n 'magna200', 'adipiscing4', 'Duis154voluptate0tempor0', 1, 1,\n 'dolor200', 'exercitation5', 'nulla170Duis0culpa0', 3, 3,\n 'consectetur200', 'ex6', 'enim186proident,0do0', 4, 4,\n 'labore200', 'veniam,7', 'nisi218ut0laborum.0', 2, 2,\n // consectetur300\n 'anim300', 'Ut0', 'nulla7eiusmod0nulla0', 6, 8,\n 'deserunt300', 'labore1', 'aute29pariatur.0aliquip0', 7, 6,\n 'sed300', 'magna2', 'dolor51amet,0enim0', 8, 5,\n 'velit300', 'anim3', 'est87officia0nostrud0', 5, 1,\n 'aute300', 'do4', 'ex154enim0aute0', 1, 3,\n 'dolor300', 'occaecat5', 'irure170Lorem0irure0', 3, 4,\n 'velit300', 'exercitation6', 'est186incididunt0qui0', 4, 7,\n 'ex300', 'laboris7', 'sed218laboris0do0', 2, 2\n ];\n// dolore mollit'cupidatat qui\n// eiusmod velit Ut amet, Duis/laboris magna\nfugiat dolore_amet, = \n Ut('incididunt.deserunt.sunt.ipsum.amet,').ad('quis').ut_elit,\n | Ut sed != \"laboris\" in ea != \"sed\" do dolor != \"do\"\n | culpa Excepteur, tempor, eu, eu, Duis\n | deserunt minim = cupidatat(\"$.Excepteur\", Lorem)\n | dolore fugiat = minim(\"$.eiusmod\", laborum.)\n | voluptate sint = culpa(\"$.dolore\", commodo)\n | consequat. in = dolor(enim(ea(\",\\\\\\\"Duis\\\\\\\":\\\\\\\"(\\\\labore+(\\\\minim+\\\\tempor+)*)\\\"\", 1, sit)), laboris, qui(\",\\\\\\\"voluptate\\\\\\\":\\\\\\\"(\\\\irure+(\\\\in+\\\\dolore+)*)\\\"\", 1, qui))\n | aliqua. cupidatat_ut = laboris(dolor ullamco \"sint\",ad(\"ipsum dolore ipsum est:\\\\reprehenderit(\\\\laborum.+)\", 1, sint), \"eu/aute\")\n | mollit ad_occaecat = dolor(et voluptate \"ut nisi nisi dolore veniam, reprehenderit:\", ea(\"elit, aliquip aliqua. dolor irure eiusmod:\\\\laboris(\\\\officia+)\", 1, elit,), aute_cupidatat)\n | eiusmod id_dolore = sit(irure nostrud \"in amet,\", dolore(\"dolor laboris amet, elit,:\\\\aute(\\\\aliqua.+)\",1,Duis), Duis_anim )\n| sit esse_non = esse(quis consectetur \"laboris5_\", et(\"sint sit in labore: sit5_(\\\\fugiat+)\", 1, ad), velit_est)\n // | quis sed_nisi = dolor(Lorem esse \"laborum.\", qui(\"elit, fugiat sed cillum:\\\\elit,(\\\\aliquip+)\", 1, exercitation), Lorem(ad(incididunt(\"in labore eu occaecat:\\\\velit(\\\\tempor+)\", 1, eiusmod)), eu(\"consequat. aliquip ut ea aliqua. laboris:\\\\cillum(\\\\laboris+)\", 1, laboris), \"cillum/ad\"))\n | anim minim = dolor(sed mollit \"anim fugiat\", qui(\"deserunt(\\\\reprehenderit+)quis0sed0\", 1, adipiscing), \"\")\n | adipiscing exercitation = fugiat(irure(voluptate), do(\"sed\", laborum., \"cillum0quis0\"), culpa)\n | nisi do = anim(sint_nulla != \"exercitation/Ut\" in velit(elit,), minim, consequat.);\n// enim tempor laborum., eu dolor veniam, fugiat100, voluptate100, dolore200, nostrud300\nest et = \n est('mollit.proident,.dolore.aute').officia('eiusmod').minim_nostrud_veniam,\n | nulla dolore_irure(ad, *) nostrud nostrud\n | Excepteur consectetur do_id ('mollit100 id', 'eu100 ullamco', 'ut200 consectetur', 'proident,200 minim', 'nostrud200 dolor', 'et300in esse', 'Lorem300quis in')\n | pariatur. ut = deserunt(nisi deserunt \"eiusmod300in\", \"Excepteur300\", (do(\"(\\\\cillum+)\", 1, ea)))\n | laborum. sit, dolore = sed\n | exercitation do, Ut;\n// esse sed nisi aliqua. qui aliqua. enim sint\nut quis = \n do('Excepteur.magna.in.ut.aliqua.').nulla('qui').quis\n | cupidatat commodo est in \"qui|voluptate|do\"\n | et adipiscing = irure(\"\\\\sit+\", 0, labore)\n | nostrud magna = et(proident, culpa \"in12\", sed(), laboris)\n | eiusmod do(in)\n | dolor non, sit\n | Duis mollit_sunt(culpa, *) ad ad;\n// nisi dolor dolore occaecat sed-Excepteur voluptate\n// do sint anim consectetur consectetur\nesse proident, =\nirure('veniam,.ipsum').sit('eiusmod').laborum.2\n| fugiat adipiscing > dolore(2aute)\n| do incididunt ipsum \"ut\"\n| Ut tempor = in(\"(\\\\ut+)\", 1, ex)\n| id sint == \"quis-deserunt\"\n| ut in = deserunt_laborum.(non, \"proident,\", \"quis\")\n| sed quis(enim)\n//| nisi (enim !eu_reprehenderit \"sit11\" enim id !reprehenderit_consequat. \"deserunt27\" quis aute !minim_ad \"ex61\" anim do !veniam,_ipsum \"culpa28\" et laborum. !proident,_ad \"dolor12\" nisi irure !irure_magna \"non62\")\n| et Lorem_Lorem(Duis, *) tempor fugiat, cupidatat\n| aliquip nostrud=qui (\n ex\n )\n nostrud $fugiat.laboris == $ullamco.culpa\n| elit, aliqua.(anim)\n| aliquip cillum=labore (\n ad \n ) exercitation $ea.velit == $nostrud.sit\n| fugiat enim(do);\n// velit in ut eiusmod in tempor 1 laboris\nanim consectetur = \n // aute('dolore.id').nulla('tempor').enim\n // | Ut aute >= officia(24Ut)\n // | sint esse == \"quis-eu\"\n // | sint ad_sed = et(), do_dolor(veniam,, *) mollit cupidatat;\n exercitation('velit-est.commodo').Ut('in').qui\n | qui do >= commodo(1sed)\n | amet, incididunt = deserunt(fugiat)\n | anim ullamco fugiat \"anim0\"\n | cillum non, deserunt, consectetur, dolore\n | enim ut cupidatat \"ad anim eiusmod\"\n | ut Duis = tempor(\"sunt\",dolor(\"consequat. culpa (.*) et\", 1, ea))\n | sunt labore_non = cillum() exercitation do, enim(et, 1occaecat), mollit\n | dolore proident,_magna > 4\n | ex ex_occaecat_incididunt = magna_reprehenderit(officia, *) nostrud ad, ullamco;\n// eu incididunt laborum. est ea eu 1 consectetur\nconsequat. irure_proident, =\n nisi('veniam,.incididunt').dolore('dolore').irure\n | incididunt ['veniam,'] >= pariatur.(1do) Ut ['occaecat'] <= veniam,()\n | proident, labore(laborum._eu)\n | ut sed = do(sint_id adipiscing \"Duis\", \"anim\", et(non_aute Lorem \"ullamco\", \"aliqua.\", \"Duis\"))\n | aliqua. deserunt_veniam, = nisi(officia_cillum_esse in_velit \"ut0;\", \"in\", Duis(nulla_ea_in eiusmod \"aliqua.\", \"in\", \"sint\"))\n | irure enim_exercitation = aliquip(\n Ut == \"eu\",\n quis('veniam,.ea').aliqua.('nostrud').laborum.(Lorem_deserunt), //elit,\n commodo == \"dolor\",\n dolor('nisi.dolor').nulla('Excepteur').consequat.(reprehenderit_in, in_reprehenderit), //pariatur.\n ullamco_Excepteur \n )\n | elit, qui != \"minim\" occaecat dolor_ut != \"fugiat\" non laboris != magna_voluptate\n | est \n ipsum_elit,_ea >= 1exercitation-5 sit\n consectetur_dolor >= 1Duis-10 quis\n est_sit >= 1pariatur.-15\n | consectetur consectetur_nulla= anim(veniam, == \"veniam,\", reprehenderit(Lorem_consequat., \" \")[1], incididunt_sint)\n | Ut labore_Ut= reprehenderit(non == \"in\", officia(id_deserunt, \" \")[0], Duis_Duis)\n | culpa dolor_amet,_incididunt_sunt_incididunt = in(esse_nulla_laboris_anim__occaecat_ / 60 / 24, 2)\n | ut irure_velit = ad(\"\\\\deserunt+(\\\\officia+)\", 1, qui_amet,_incididunt)\n | labore id_in = ad(\"\\\\reprehenderit+(\\\\nisi+)\", 1, dolore_Ut)\n | esse nostrud_Ut = dolore(cupidatat(Excepteur_dolor), Ut_veniam,, laborum._dolor)\n | ut irure_fugiat = amet,(aliqua.(id == \"ad\", velit(\";(\\\\nostrud+-\\\\proident,+-\\\\laborum.+-\\\\adipiscing+)\", 1, proident,_esse_dolor), ullamco_ullamco))\n | veniam, laboris_velit(['incididunt'], *) aliquip cupidatat_ullamco, Ut(['dolore'], Duis_tempor);\n// eiusmod amet, sit\npariatur. sint_sunt_ullamco =\nex('esse.pariatur.3.sint.esse.occaecat').officia('ex').sit\n| sint (laboris == 121004 laboris consectetur amet, \"ea ea, irure aute amet, occaecat quis cupidatat dolore qui non in non officia officia \") commodo (ipsum == 121001 Ut nostrud irure \"occaecat-pariatur.\" aliqua. ex dolor \"ipsum Ut ut\") dolor proident, == 62786 qui et == 62784 incididunt (qui id (122007, 121012) esse dolore nulla \"elit, ex sint commodo ut dolor id occaecat. ut nulla dolor incididunt aliqua., do in anim sint.\") exercitation (adipiscing == 121004 Lorem nostrud ad \"ut dolore eu consectetur quis dolor anim incididunt 17\") ullamco (nostrud == 121004 tempor dolore adipiscing \"sed enim dolore culpa in incididunt amet, cupidatat proident, 17 enim ut proident, Lorem\")\n| nulla minim != \"esse\"\n | Duis est_deserunt = cillum(officia(\"\\\"dolor\\\":(\\\\Lorem*)\", 1, sunt))\n | aliquip incididunt = amet,(aliquip(proident,), in(\"(.|\\\\n)*sint:\\\\sed*(\\\\mollit*)\", 2, aliquip), eu)\n | elit, dolore = labore(\"(.|\\\\n)*officia:\\\\labore*(\\\\consequat.*-\\\\deserunt*-\\\\anim*-\\\\sit*-\\\\aute*)\", 2, id)\n | labore amet, = ullamco(labore(aute), voluptate, proident,)\n | Excepteur irure = deserunt_proident,(\"irure\", velit)\n | non dolor = et(\"(.|\\\\n)*pariatur.:\\\\sint*(\\\\Excepteur*\\\\/\\\\ad*)\", 2, culpa)\n | laborum. exercitation_sunt = Ut(magna(\"sunt\\\\ex*(\\\\veniam,+)\",1, dolor))\n | Excepteur Excepteur = sint(\"(.|\\\\n)*enim\\nenim:\\\\sunt*\\\\cillum*\\\\naute:\\\\incididunt*(\\\\in*)\", 2, veniam,)\n | deserunt dolor_nulla = \n ad(\n dolor(Duis(\"(.|\\\\n)*exercitation\\nquis:\\\\do*(\\\\culpa*)\\\\nullamco:\\\\eu*(\\\\in*)\\\\nlaborum.:\\\\officia*(\\\\ullamco*)\\\\nqui:\\\\magna*(\\\\aliqua.*)\", 5, in)),\n dolore(\"(.|\\\\n)*Ut\\ncillum:\\\\Duis*(\\\\sit*)\\\\nenim:\\\\mollit*(\\\\nulla*)\\\\ndo:\\\\Excepteur*(\\\\nostrud*)\\\\noccaecat:\\\\sit*(\\\\amet,*)\", 5, fugiat),\n velit(\"(.|\\\\n)*eu\\nvoluptate:\\\\ut*(\\\\laboris*)\\\\nvoluptate:\\\\laboris*(\\\\sed*)\\\\nexercitation:\\\\consectetur*(\\\\dolore*)\", 4, consectetur)\n )\n | sunt nulla_sint = nulla(ut_ex)\n | Excepteur magna=commodo (\n nisi \n ) dolor $ea.esse == $quis.in\n | id reprehenderit =ut (\n ipsum // Duis enim magna tempor dolor consectetur laborum. in\n )\n enim $deserunt.nulla_irure == $ex.commodo_officia_consectetur proident, $esse.Duis == $officia.culpa\n | aute Excepteur_quis = do(proident,(sit_culpa), voluptate, ea_pariatur.)\n | nostrud labore_deserunt = adipiscing\n | deserunt consequat._laboris(irure(in), *) est anim, tempor_deserunt;\nea aliquip = \npariatur.\n| amet, in == \"in\"\n| sit deserunt = magna (\n labore('id.incididunt.pariatur..nisi.aliqua.').eu('ea').Ut_sit\n | id labore, esse, officia, adipiscing\n) irure exercitation\n| est et = sunt, Ut = ad, sunt = anim, qui_et = cupidatat, Excepteur_aliqua. = consectetur(\"$.magna\", nostrud)\n, dolor_culpa = qui(\"$.sunt\", ex), aliquip_consequat. = deserunt(\"$.et\", minim), officia_Excepteur = Ut, magna_ipsum = minim, tempor;\ndolore dolor_cillum = \nculpa //Excepteur Duis reprehenderit magna eiusmod, sed nisi\n| tempor exercitation=eiusmod (\n incididunt_officia // Excepteur ipsum'aliquip id ipsum reprehenderit\n ) quis $sint.cillum == $nostrud.dolor\n| deserunt labore == 10036 nostrud reprehenderit == 0\n| officia irure(minim)\n| proident, aliqua. = officia (\n ad_officia\n )\n Ut $officia.nulla == $tempor.ipsum_aute dolor $nulla.incididunt == $consequat..reprehenderit_dolore\n| incididunt proident, =exercitation (\n elit, // cupidatat nostrud sit officia dolor irure commodo consectetur\n )\n esse $aliqua..pariatur. == $consectetur.in cupidatat $sed.eu == $deserunt.in laborum. $nulla.ut == $esse.laborum.\n| voluptate incididunt=est (\n officia // fugiat cupidatat voluptate labore\n )\n velit $id.id == $fugiat.sit laborum. $esse.sit == $ut.proident,\n| irure proident, = anim (\n sint_enim_exercitation\n) cillum tempor eu $tempor.ullamco == $Duis.pariatur._reprehenderit\n| dolor consequat. = \n dolore(\n elit, % 2 == 0 velit (in == \"commodo\"), nulla - 1, // sint Duis\n cupidatat % 2 == 1 aute (et == \"Ut\"), incididunt + 1,\n 0\n )\n| consequat. anim = sed (\nlaborum.\n ) ut tempor ipsum $dolor.dolore == $deserunt.in\n// labore dolor quis\n| ipsum incididunt = \n cupidatat(\n ad_Excepteur != \"adipiscing\" anim Excepteur(veniam,_deserunt),\n \"est sed Duis sint\",\n officia_fugiat == \"nisi\" sed (nisi(id) < ut(do)),\n \"nostrud non velit\",\n pariatur. == \"ut\" elit, et == \"Ut\" amet, tempor(dolore),\n \"in nostrud ullamco sint\",\n nostrud_irure > 4,\n \"non dolor\",\n Duis(sed_deserunt_mollit) enim pariatur.(irure_aliquip) cupidatat dolore(Ut_Excepteur),\n \"laborum. officia\",\n (do_Lorem == id aliquip cillum == enim) voluptate pariatur.,\n \"cupidatat eiusmod\",\n velit(ullamco) dolor labore == \"eiusmod\" sunt voluptate(et),\n \"elit, ut: eu\",\n \"ullamco/amet,\")\n // sed consequat.\n | ullamco sit = \n pariatur.(\n // Duis culpa non: irure culpa ut laboris adipiscing dolor Duis\n laboris_cillum == \"nulla\",\n \"in proident, occaecat\",\n // tempor pariatur. eiusmod in: quis magna qui laboris culpa sit, veniam, nulla cupidatat anim occaecat officia\n proident, == \"sunt nostrud commodo ex\",\n \"aliqua. velit mollit qui\",\n // magna laboris laboris sunt in eu aliqua._pariatur._amet,\n et velit \"magna_ex_aliqua.\",\n \"sunt_laboris_amet,\",\n // ea fugiat:\n // -----------------------------------------------\n // ipsum voluptate officia cillum do est officia nostrud\n ((dolore == \"voluptate\" enim veniam, == \"ut\") in (nostrud == \"veniam, dolore: cupidatat\") mollit (eu != 10036)) Duis dolore == 1,\n \"exercitation ea: voluptate in nulla, occaecat fugiat eu 10036\",\n // non esse deserunt/laborum. aliqua. cillum qui sit\n ((ipsum == \"qui\" do ex == \"enim\") laboris (Lorem == \"magna ullamco\") amet, (eiusmod != 10036)) elit, Duis == 1,\n \"reprehenderit laboris: reprehenderit et in, qui mollit do 10036\",\n // occaecat veniam, labore eiusmod id do culpa proident, elit, ea, minim sit non\n (in == 10036 aute ad == 1 adipiscing in != \"et/est\"),\n \"dolor ex sit dolor ipsum proident,\",\n // ut eu labore ea (labore100/esse200)\n // ---------------------------------------------\n // adipiscing dolore qui (dolor quis) non fugiat100/est200 : magna consectetur sunt sed incididunt aliquip dolore reprehenderit dolore incididunt\n exercitation == 121001 ullamco ullamco_velit == \"consequat.\" ea (quis == \"laborum. pariatur.\" eu amet, == \"ut aliqua. occaecat\") fugiat (labore_fugiat('eiusmod', id(), occaecat(elit,)) <= 30) do (consectetur == \"eiusmod\" Lorem aliqua. !eiusmod \"dolor09\") est ((Excepteur(dolore_adipiscing) est ( amet,(aliquip_laborum._consequat.) > labore(sit))) eiusmod sit(non_ullamco_proident,) amet, (reprehenderit nostrud consectetur(incididunt) > tempor(ullamco))),\n \"laboris velit-aliqua.: aute elit, - commodo qui do pariatur. 17\",\n // consectetur deserunt elit, et ipsum -->\n enim == 121004 enim irure dolor \"ullamco in ut in voluptate et elit, occaecat 17\" adipiscing (occaecat_veniam,('aliqua.', labore(), proident,(consequat.)) <= 2),\n \"laborum. aliquip esse amet, ut nostrud\",\n // Lorem laborum. adipiscing proident, amet, id dolore 17, Duis magna commodo\n dolore == 121004 in sunt sint \"ex consectetur est et ipsum occaecat proident, laborum. 17\" tempor (dolore_irure('laborum.', cupidatat(), officia(esse)) > 2) in (in(eu_dolore) velit commodo(adipiscing_aute_in)),\n \"non Ut-quis: laboris tempor - laborum. laboris commodo 17 cillum sint adipiscing mollit nostrud/amet, dolore: minim ipsum\",\n // dolor Excepteur cupidatat sit proident, voluptate ipsum 17, cillum ea -->\n nulla == 121004 cupidatat aute est \"id proident, reprehenderit enim consequat. eu dolore cupidatat 17, magna cillum labore reprehenderit\" deserunt officia_do == \"laboris\",\n \"aliquip do-culpa: sint sed, cupidatat amet, aute enim eu laborum. do consequat. sint\",\n // id esse ex voluptate do dolor 17, in deserunt cupidatat -->\n tempor == 121004 aliquip ut deserunt \"consequat. Ut dolore dolore sint ut ullamco occaecat 17\" sunt (ipsum_Duis('nisi', occaecat(), dolore(consectetur)) > 2) minim ((sunt(laborum._nulla) anim ( dolore(proident,_dolore_ex) > ullamco(sint))) commodo nostrud(dolore_pariatur._aliquip)),\n \"dolore occaecat-dolore: in nisi - consectetur velit nulla 17 in voluptate dolor anim/nisi commodo: do ipsum\",\n // reprehenderit aliqua. Ut labore cillum, aliqua. nulla\n commodo == 121004 nulla amet, consequat. \"laborum. aute eu in, adipiscing reprehenderit consequat. laborum. ad\" cillum occaecat_enim == \"eiusmod\",\n \"laboris culpa-fugiat: nulla aliquip\",\n // enim magna cillum in:\n // ----------------------------------\n // Lorem veniam, veniam, id commodo, ullamco veniam, labore irure nulla, aliquip aliqua. in et in ipsum\n (ad_anim == \"sint\" dolore cupidatat !anim \"minim Ut in ad cupidatat ut\" Ut labore == \"et\" officia (adipiscing(officia_pariatur.) sint (ut(dolor_tempor_veniam,) < aliqua.(incididunt))) voluptate reprehenderit(dolor_do_velit) cupidatat (aliqua.(deserunt) < id(consectetur))),\n \"exercitation anim sed\",\n // sit aliquip culpa ut nisi, irure anim sit veniam, minim, cillum minim officia pariatur. elit, occaecat qui ea nisi ut pariatur. (ea dolor adipiscing anim nisi dolore)\n (qui enim \"nisi ea reprehenderit\" do (exercitation_laboris == \"sit\" ullamco (fugiat_ullamco != 0 aliquip dolor_sit != 10036)) labore magna == \"consectetur\" voluptate consectetur(proident,_Lorem) adipiscing elit,(dolor_amet,_cupidatat)), // voluptate velit\n \"culpa sed enim\",\n // nostrud proident, veniam, irure\n // ------------------------------------------\n // officia esse nisi sint adipiscing ut consequat. irure in Duis proident, dolore in adipiscing\n (reprehenderit_voluptate == 10036 deserunt Ut_labore == 1 occaecat deserunt_non exercitation \"voluptate aliquip deserunt\"),\n \"Lorem ea elit, commodo ullamco aute non proident, officia in incididunt\",\n // fugiat cupidatat qui sunt Lorem nisi nostrud proident, nisi pariatur. occaecat reprehenderit enim eiusmod voluptate incididunt id anim qui nostrud\n ((mollit occaecat \"voluptate amet, sunt elit,\" laborum. aliquip == 62784) Ut consequat. == \"cillum\" tempor exercitation_velit == 0 eu Ut != \"tempor\" dolor (sunt(Duis) < ad(laboris))),\n \"reprehenderit sit dolore adipiscing\",\n // cupidatat incididunt ut amet, reprehenderit proident, consequat. ad ullamco non cillum. sed laborum. sed ipsum in velit nisi anim nostrud eu Lorem\n (aliquip Ut (122007, 121012) esse laboris_velit == \"cupidatat\" esse (incididunt == \"in\" Excepteur (ut(officia_quis) consectetur ( sed(sunt_aute_ipsum) > veniam,(enim))) ) ut pariatur._laboris == 0 eu sunt_aute != \"et\" eu (anim(eiusmod) < cillum(qui))),\n \"culpa: laborum. et adipiscing nulla\", // cupidatat velit culpa ad, dolore in nulla occaecat occaecat veniam,\n // dolor mollit voluptate deserunt, aliquip/adipiscing consequat. labore adipiscing aliquip dolor in\n ullamco == 121001 fugiat in_in == \"id\" officia (reprehenderit == \"veniam, consequat.\" fugiat deserunt == \"sit sit eiusmod\") aliquip (dolore == \"amet,\") consectetur ((consequat.(aliqua._laboris) labore ( Excepteur(veniam,_nisi_consequat.) > labore(elit,))) tempor esse(aliquip_minim_aliqua.) velit exercitation) non consequat._ipsum == 0 consequat. (fugiat_Ut != \"irure\"),\n \"irure Excepteur-ut: in dolor - sint exercitation\",\n // consectetur nostrud nisi proident, et eiusmod Duis nostrud quis nostrud: dolor commodo\n ((qui enim \"est velit cupidatat nulla\") anim ut == \"quis\" dolor laboris != \"Excepteur\" laborum. reprehenderit_qui == 1 cupidatat Excepteur_enim == \"reprehenderit\") est (eu == 62784 aliquip eu == \"Excepteur\" proident, exercitation_magna == 1 ullamco veniam,_reprehenderit == \"in\"),\n \"labore dolore sit 10036\",\n // Duis veniam, Excepteur ex reprehenderit occaecat enim et minim sit: veniam, deserunt enim\n consequat. == 121001 incididunt fugiat_ut == \"eu\" sunt (consequat. == \"dolore consequat.\" irure dolor == \"sit in velit\") esse ((Duis(minim_et) ut ( aute(officia_aute_magna) > eiusmod(in))) ut quis(ipsum_ut_tempor) tempor deserunt) eu officia_dolor == \"minim\",\n \"consectetur officia laborum. 10036\", // ullamco dolor enim do tempor anim\n // sed in dolore sit nostrud\n //--------------------------------\n // consequat. Excepteur Excepteur amet, anim 10038\n (ullamco == \"Duis\" non sit == \"fugiat\" labore ea == \"minim\" ut (minim et_culpa \"exercitation\" esse consectetur consequat. \"aliquip\") dolore sunt == 0), \n \"exercitation enim laborum. 10038\",\n // et Excepteur\n // ---------------------------\n // velit ut qui nostrud nisi ullamco sed\n (nulla == \"Ut\" velit eu == \"qui\" qui deserunt == \"ea\") nisi (qui == \"ad exercitation in consectetur\"),\n \"id minim\",\n // aliquip\n // ------------------\n (cillum == \"ipsum\"),\n \"ut nostrud anim\",\n // incididunt eu fugiat\n //---------------------------\n // Excepteur cillum pariatur. in eiusmod\n ((dolore nisi \"ipsum dolor enim est\" in incididunt consequat. \"ut ut non\") proident, minim != \"proident,\" dolore laborum. !pariatur. \"dolor ad aute dolor\" sint (id(Ut) < aliquip(ipsum))) dolore irure == \"nulla\",\n \"et consectetur\",\n // mollit consequat. Lorem pariatur. eu\n (eu in \"tempor eu\" ad in != \"eu\"),\n \"cillum dolore\",\n // sit eu cillum ullamco velit esse, Lorem consequat. sunt in amet,\n dolor == 121001 voluptate commodo_occaecat == \"fugiat\" ea (ea == \"eu\" veniam, labore != \"laboris\") veniam, Excepteur == \"consequat.\" dolor ea == \"0\" proident, elit,(Duis_cupidatat),\n \"exercitation anim amet, irure in, consequat. Excepteur occaecat\",\n // cupidatat aute ex sed in pariatur., consequat. dolore nostrud sit exercitation\n in == 62786 occaecat ((Duis == \"Excepteur\" esse proident, != \"Lorem\") laborum. (in(commodo_enim) esse ( Excepteur(esse_quis_quis) > amet,(dolore))) id ut(id_velit_labore)) sunt do_occaecat == \"sunt\" labore (sint cillum \"reprehenderit consequat.\" tempor veniam, laboris \"consectetur commodo\"),\n \"nisi dolor commodo, veniam, ex culpa officia. officia dolore amet, Ut mollit veniam, aute in\",\n // Ut ut amet, eu consectetur voluptate dolore et consectetur, dolor nisi sint dolore in\n irure == 62786 enim (cillum == \"pariatur.\" tempor irure != \"consequat.\") sunt sunt_tempor == \"magna\",\n \"eiusmod enim amet, quis, nostrud Duis veniam,, laboris quis\",\n // nulla ex aute pariatur. magna adipiscing dolor Duis Duis anim consequat. sit, Lorem mollit id ipsum fugiat adipiscing officia ipsum\n (fugiat_tempor == \"dolore\" qui dolor == \"magna\" ut (ut(magna) < aliqua.(Duis))),\n \"pariatur. est\",\n // anim Excepteur in laboris\n // -------------------------------\n (quis == \"magna\" non ex == \"0\" laborum. (minim Duis \"mollit\" ea exercitation in \"Duis\") qui proident, == \"aliquip et\"), \n \"consequat. minim 24ut esse Excepteur, reprehenderit exercitation Lorem nostrud Ut\", // in Ut aute\n ((eiusmod == \"proident,\" consectetur aliqua. == \"anim\") dolore dolor == \"0\" ut (enim amet, \"et\" Duis amet, ullamco \"Lorem\") ea (fugiat(ullamco) in veniam,_sunt == \"id\") cupidatat (cupidatat == 0 non officia == 10036) voluptate ((cillum == \"amet,\" veniam, nostrud != \"adipiscing\"))), \n \"officia in 24nisi dolore et, sint Duis reprehenderit nulla ad\", // id qui\n // aliquip aliquip eiusmod veniam,\n ((veniam, == \"dolor/consequat.\" in velit == \"cupidatat\" ad (est(aliqua._Lorem) nostrud laborum._voluptate == \"ut\") dolor eu == \"veniam,\" dolore (irure == 0 cillum Lorem == 10036) minim (exercitation !sed \"laborum.\" mollit sunt !incididunt \"et#\" sunt reprehenderit !veniam, \"\\\"eiusmod\\\":\\\"dolore\\\"\"))),\n \"exercitation Lorem reprehenderit\", // laborum. nisi in fugiat ex Lorem sunt non eu do, sed minim sint in\n // tempor Ut eiusmod ea labore fugiat aliqua. minim Lorem\n // --------------------------------------------\n Excepteur ut \"laborum. magna Duis ut\",\n \"consequat. laboris voluptate sunt minim - in est Lorem non\",\n in == \"amet,\" enim (tempor == 0 reprehenderit eu == 10036) id reprehenderit == \"non\",\n \"ex commodo voluptate ex ea\",\n \"nostrud/dolore\"\n)\n| irure (magna != \"aliqua./et\" Lorem dolor != \"proident,/anim\") \n| dolor (incididunt != \"incididunt in nostrud\" nostrud sed != \"amet,/dolore\")\n| exercitation laborum.(culpa)\n| pariatur. \n aliquip,\n minim,\n sed,\n laboris,\n sint,\n Lorem,\n minim_ut_laboris,\n adipiscing,\n sint,\n dolore,\n dolore,\n Excepteur_amet,,\n laborum._eu_velit,\n magna(Duis_consectetur_proident,),\n dolor(ullamco_laborum.),\n ullamco(Excepteur_cillum),\n amet,,\n id,\n nulla,\n anim_commodo_in,\n aliqua.,\n laborum.,\n ex,\n sed,\n est,\n eiusmod,\n sunt,\n est,\n mollit,\n consequat.,\n irure,\n ipsum,\n incididunt,\n quis,\n minim,\n consequat.,\n dolore,\n nisi,\n consequat.,\n voluptate,\n cupidatat,\n eiusmod,\n dolore_dolore,\n in_amet,,\n sit_est,\n amet,,\n sint_culpa,\n reprehenderit,\n do,\n non_irure,\n dolor_dolor,\n pariatur._fugiat,\n ut_aliquip,\n sint_culpa\n | dolore Duis == \"sit irure 24ipsum deserunt magna, non reprehenderit dolor irure do\"\n nisi exercitation == \"deserunt in 24eiusmod nulla sit, consectetur consequat. Ut et officia\"\n sed sint == \"amet, anim ullamco Lorem do, ipsum ullamco laboris\"\n commodo dolor == \"deserunt irure laborum. enim, minim aliqua. Lorem, pariatur. eu\"\n qui cupidatat == \"aliquip laborum.-amet,: aliqua. elit, - irure cillum\"\n aliqua. voluptate == \"reprehenderit minim in ex\"\n nisi in == \"in Lorem-sed: ut sed - dolor officia aliquip ipsum 17\"\n aute aliquip == \"velit Ut-quis: nulla nisi - nostrud cillum aliquip 17 voluptate in labore do/commodo anim: dolore pariatur.\"\n proident, aute == \"et sunt-esse: exercitation dolore - reprehenderit exercitation nostrud 17 tempor amet, commodo voluptate consectetur/amet, deserunt: enim amet,\"\n | est Lorem = id (\n incididunt_mollit\n | veniam, Ut = culpa(dolor)\n )\n laborum. $eiusmod.eiusmod == $occaecat.sit\n | nulla Duis = eiusmod (\n exercitation_ea\n )\n fugiat $est.ad == $fugiat.proident,\n | voluptate in(aute)\n | reprehenderit\n ut,\n ea,\n occaecat,\n sunt,\n culpa,\n labore,\n labore,\n in,\n eiusmod,\n pariatur.,\n nisi,\n adipiscing,\n culpa,\n deserunt,\n cillum,\n tempor,\n anim,\n culpa,\n non,\n do,\n anim,\n consequat.,\n incididunt,\n sed_do,\n esse,\n eiusmod,\n do,\n ullamco_ullamco,\n sunt,\n labore,\n minim_ea,\n non;\nelit, sunt_velit = consequat.('sint.sunt').veniam,('Duis').deserunt2\n | velit cupidatat > ut (2aute)\n | ullamco Excepteur(est) qui consequat. == \"anim\" sint esse(ut)\n | laborum. Lorem aute \"dolore\"\n | consequat. officia == \"et-consectetur\"\n | anim ad(ipsum)\n // | Excepteur (veniam, !ea_irure \"Duis11\" in non !amet,_sed \"fugiat27\" eu velit !quis_aliquip \"pariatur.61\")\n | mollit elit,_Excepteur(Ut, *) culpa reprehenderit, amet,\n | esse laborum._commodo = aute_est(pariatur.) dolor magna;\nesse dolor_laboris = \n tempor('sint.commodo').sunt('exercitation').consequat.2\n | nulla eu > ea(2commodo)\n | aliquip commodo(nisi) nulla nisi == \"nisi\" sit sit(veniam,)\n | sunt ex commodo \"reprehenderit\"\n | sit labore == \"voluptate-commodo\"\n | Lorem Excepteur(velit)\n // | id (in !irure_qui \"ullamco11\" sunt elit, !deserunt_Lorem \"aliqua.27\" non adipiscing !eu_do \"Excepteur61\")\n | non ut_enim(Duis, *) in Duis, mollit\n // | ut culpa = irure_culpa(et) ex quis, irure, et\n | consectetur non_non_ut200 = sunt(dolor cillum (officia200) officia (fugiat == 1 enim dolor == 2), sed([3, 4]), in([1,2]))\n| voluptate id = ipsum voluptate_irure sed dolore\n | sed eiusmod_culpa = tempor(ut_dolore(in_Excepteur, id_enim)[0])\n | Lorem Lorem_deserunt = pariatur.(quis tempor (esse200), mollit_consectetur(ad_sunt_voluptate200, eu_Duis)[0], nostrud_fugiat)\n// esse_anim deserunt [1,2,3], eu_sunt commodo ut 4\n | aute sed_eu = non(sit(sit_in) cillum tempor in (qui200), dolor_in(culpa([1,2,3,4]), nulla_consectetur)[0], ut_aute);\n // | in irure_deserunt = sit(aliqua._quis(commodo_exercitation, voluptate)[0])\n // | reprehenderit irure_nostrud = labore(sint ea \"Ut12\", aliqua._non(nisi_labore_exercitation200, fugiat)[0], esse_deserunt);\nnon_non\n| fugiat consectetur = in (\n ex_esse\n )\n in officia\n| esse\n dolor,\n elit,,\n qui,\n minim,\n sunt,\n cillum,\n minim,\n velit,\n est,\n aliquip,\n ut,\n amet,,\n cillum,\n ut,\n dolor,\n dolor,\n tempor,\n Excepteur,\n anim,\n do,\n Duis,\n anim,\n proident,,\n exercitation_culpa,\n ea,\n //ipsum,\n enim,\n veniam,_cillum,\n laborum.,\n irure,\n nisi_est,\n amet,,\n ullamco(in_aute)\n| quis et != \"amet,20-0101-0802-08Ut0\"\n| anim minim = reprehenderit(\n est == \"pariatur. exercitation-adipiscing: elit, eiusmod - enim nisi commodo magna 17\",\n \"laboris cupidatat\",\n exercitation == \"elit, veniam,-sit: officia nulla - magna veniam, consectetur 17 commodo nostrud deserunt fugiat/aliqua. fugiat: irure qui\",\n \"ut Duis\",\n exercitation == \"anim nulla-nulla: incididunt aliquip - exercitation exercitation sed 17 id Lorem id amet, sed/mollit anim: nulla sed\",\n \"ad exercitation\",\n dolor == \"ut occaecat eiusmod eiusmod dolor, voluptate ut nostrud\" eiusmod laboris == \"amet, reprehenderit 24fugiat pariatur. quis, cupidatat non sunt in do\",\n \"aliquip\",\n aute == \"est dolor sed sunt\" ad eiusmod == \"proident, consectetur-occaecat: nisi amet, - dolore non\",\n \"et\",\n cillum == do et dolore == \"nulla aliqua. 24est laboris in, sed Duis qui labore laborum.\",\n \"irure\",\n \"sit\"\n )\n| ipsum ad(ullamco_fugiat) quis cillum(laboris_eiusmod) sint reprehenderit\n| esse in != eu_deserunt"  }, moor-2.10.3/sample-files/issue-372.txt000066400000000000000000000004021513574474500173470ustar00rootroot00000000000000 mmaann [--aaddhhoo] [--tt | --ww] [--MM _m_a_n_p_a_t_h] [--PP _p_a_g_e_r] [--SS _m_a_n_s_e_c_t] [--mm _a_r_c_h[:_m_a_c_h_i_n_e]] [--pp [_e_p_r_t_v]] [_m_a_n_s_e_c_t] _p_a_g_e _._._. moor-2.10.3/sample-files/json.json000066400000000000000000000003611513574474500170150ustar00rootroot00000000000000{"menu": { "id": "file", "value": "File", "popup": { "menuitem": [ {"value": "New", "onclick": "CreateNewDoc()"}, {"value": "Open", "onclick": "OpenDoc()"}, {"value": "Close", "onclick": "CloseDoc()"} ] } }}moor-2.10.3/sample-files/large-git-log-patch-no-color.txt000066400000000000000000555125211513574474500232070ustar00rootroot00000000000000commit 3a164ae699b2676a6648622fd4be2d427ca8e587 (HEAD -> master, origin/master, origin/HEAD) Merge: dcfd089 cb95be3 Author: Mathias Ã…hsberg Date: Thu Mar 9 10:35:38 2017 +0100 Merge pull request #682 from walles/walles/unused Re-enable some Unused warnings commit cb95be3d87edc40f7bb4a7502d989803f33f500a (walles/walles/unused, walles/unused) Author: Johan Walles Date: Wed Mar 8 21:52:57 2017 +0100 Disable an unused Lint rule diff --git config/quality/lint/lint.xml config/quality/lint/lint.xml index 71a7793..e8fe111 100644 --- config/quality/lint/lint.xml +++ config/quality/lint/lint.xml @@ -20,7 +20,6 @@ - commit dcfd0893a07e7703aeb5fb61a9affb0b5f3f6c87 (walles/checks) Merge: 0303e7d a20852c Author: Mathias Ã…hsberg Date: Wed Mar 8 21:52:00 2017 +0100 Merge pull request #681 from liato/feature/upgrade-build-tools Upgrade android build tools commit 11f299972fdd6154872c68aebc4947eb816d3551 Author: Johan Walles Date: Wed Mar 8 21:47:56 2017 +0100 Re-enable some Unused warnings Before Android Studio 2.3.0 having these enabled triggered internal errors in Lint. Those errors have now been fixed and the checks can be re-enabled. diff --git bankdroid-legacy/src/main/res/values-sv/strings.xml bankdroid-legacy/src/main/res/values-sv/strings.xml index 36f71c5..e3f6dfa 100644 --- bankdroid-legacy/src/main/res/values-sv/strings.xml +++ bankdroid-legacy/src/main/res/values-sv/strings.xml @@ -2,16 +2,12 @@ Användarnamn Lösenord Kortnummer - Kontonummer - Kontrollkod E-post poäng Kort ID Saldo Ã…Ã…Ã…Ã…MMDDNNNN - Nyckel - Bitcoin-adress Ogiltig bitcoin-adress. Allmän pension @@ -24,7 +20,6 @@ Inga konton funna Ogiltigt användarnamn. Ogiltigt kortnummer. - Banken är för närvarande stängd. Serverfel. Var god försök igen om en stund. Kunde ej uppdatera transaktionsdata. Var vänlig försök igen senare. diff --git bankdroid-legacy/src/main/res/values/strings.xml bankdroid-legacy/src/main/res/values/strings.xml index a80426d..89167f4 100644 --- bankdroid-legacy/src/main/res/values/strings.xml +++ bankdroid-legacy/src/main/res/values/strings.xml @@ -5,16 +5,12 @@ Password Extras Card number - Account number - Control code E-mail points Card ID Balance YYYYMMDDNNNN - Key - Bitcoin address Invalid bitcoin address. Public pension @@ -27,7 +23,6 @@ No accounts found Invalid username. Invalid card number. - The bank is currently closed. Server error. Please try again later. "There was a problem updating the transaction details. Please try again later." diff --git config/quality/lint/lint.xml config/quality/lint/lint.xml index c3565a5..71a7793 100644 --- config/quality/lint/lint.xml +++ config/quality/lint/lint.xml @@ -47,8 +47,6 @@ - - commit a20852c17d74128d8071b7e6139115794d09cbe2 (origin/feature/upgrade-build-tools, feature/upgrade-build-tools) Author: Mathias AÌŠhsberg Date: Thu Mar 2 21:40:58 2017 +0100 Fixes travis build diff --git .travis.yml .travis.yml index 6c53f33..dadb73b 100644 --- .travis.yml +++ .travis.yml @@ -10,14 +10,14 @@ cache: env: matrix: - - ANDROID_TARGET=android-24 ANDROID_ABI=armeabi-v7a + - ANDROID_TARGET=android-25 ANDROID_ABI=armeabi-v7a android: components: - tools - platform-tools - - build-tools-24.0.3 - - android-24 + - build-tools-25.0.1 + - android-25 - extra-android-m2repository script: ./gradlew assembleDebug check commit e6e1e4fdec9bf27aadef0d5c22d35a56c2253708 Author: Mathias AÌŠhsberg Date: Thu Mar 2 21:33:49 2017 +0100 Upgrade android build tools diff --git app/build.gradle app/build.gradle index 11832d8..f73b061 100644 --- app/build.gradle +++ app/build.gradle @@ -38,14 +38,14 @@ ext { } android { - compileSdkVersion 24 - buildToolsVersion "24.0.3" + compileSdkVersion 25 + buildToolsVersion "25.0.1" useLibrary 'org.apache.http.legacy' defaultConfig { applicationId "com.liato.bankdroid" minSdkVersion 9 - targetSdkVersion 24 + targetSdkVersion 25 versionCode 224 + gitVersionCode versionName gitVersionName } @@ -87,7 +87,7 @@ dependencies { compile project(':bankdroid-core') compile 'com.jakewharton:butterknife:6.1.0' compile 'com.jakewharton.timber:timber:4.3.1' - compile "com.android.support:appcompat-v7:24.2.1" + compile "com.android.support:appcompat-v7:25.2.0" compile 'com.google.collections:google-collections:1.0' compile('com.crashlytics.sdk.android:crashlytics:2.6.5@aar') { transitive = true; diff --git app/src/main/java/com/liato/bankdroid/MainActivity.java app/src/main/java/com/liato/bankdroid/MainActivity.java index 98c8a2b..78d7ff8 100644 --- app/src/main/java/com/liato/bankdroid/MainActivity.java +++ app/src/main/java/com/liato/bankdroid/MainActivity.java @@ -59,7 +59,7 @@ public class MainActivity extends LockableActivity { protected static boolean showHidden = false; - private static Bank selectedBank = null; + private Bank selectedBank = null; private static Account selectedAccount = null; @@ -241,7 +241,7 @@ public class MainActivity extends LockableActivity { final Button btnHide = (Button) root.findViewById(R.id.btnHide); final Button btnUnhide = (Button) root.findViewById(R.id.btnUnhide); final Button btnWWW = (Button) root.findViewById(R.id.btnWWW); - if (selectedBank.getHideAccounts()) { + if (parent.selectedBank.getHideAccounts()) { btnHide.setVisibility(View.GONE); btnUnhide.setVisibility(View.VISIBLE); btnUnhide.setOnClickListener(this); @@ -250,7 +250,7 @@ public class MainActivity extends LockableActivity { btnUnhide.setVisibility(View.GONE); btnHide.setOnClickListener(this); } - if (selectedBank.isWebViewEnabled()) { + if (parent.selectedBank.isWebViewEnabled()) { btnWWW.setOnClickListener(this); } else { btnWWW.setVisibility(View.GONE); @@ -270,29 +270,29 @@ public class MainActivity extends LockableActivity { case R.id.btnHide: case R.id.btnUnhide: this.dismiss(); - selectedBank.toggleHideAccounts(); - DBAdapter.save(selectedBank, context); + parent.selectedBank.toggleHideAccounts(); + DBAdapter.save(parent.selectedBank, context); parent.refreshView(); return; case R.id.btnWWW: - if (selectedBank != null && selectedBank.isWebViewEnabled()) { + if (parent.selectedBank != null && parent.selectedBank.isWebViewEnabled()) { //Uri uri = Uri.parse(selectedBank.getURL()); //Intent intent = new Intent(Intent.ACTION_VIEW, uri); final Intent intent = new Intent(context, WebViewActivity.class); - intent.putExtra("bankid", selectedBank.getDbId()); + intent.putExtra("bankid", parent.selectedBank.getDbId()); context.startActivity(intent); } this.dismiss(); return; case R.id.btnEdit: final Intent intent = new Intent(context, BankEditActivity.class); - intent.putExtra("id", selectedBank.getDbId()); + intent.putExtra("id", parent.selectedBank.getDbId()); context.startActivity(intent); this.dismiss(); return; case R.id.btnRefresh: this.dismiss(); - new DataRetrieverTask(parent, selectedBank.getDbId()).execute(); + new DataRetrieverTask(parent, parent.selectedBank.getDbId()).execute(); return; case R.id.btnRemove: this.dismiss(); @@ -307,7 +307,7 @@ public class MainActivity extends LockableActivity { public void onClick(final DialogInterface dialog, final int id) { final DBAdapter db = new DBAdapter(context); - db.deleteBank(selectedBank.getDbId()); + db.deleteBank(parent.selectedBank.getDbId()); dialog.cancel(); parent.refreshView(); } diff --git bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/FieldBuilder.java bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/FieldBuilder.java index 4d1ce55..80b12c7 100644 --- bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/FieldBuilder.java +++ bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/FieldBuilder.java @@ -154,7 +154,7 @@ public class FieldBuilder { @Override public void validate(String value) throws IllegalArgumentException { - if (isRequired()) { + if (required) { if (value == null || value.trim().isEmpty()) { throw new IllegalArgumentException(String.format("%s is required", getLabel())); } @@ -168,7 +168,7 @@ public class FieldBuilder { if (!isLocale()) { return null; } - String propertyKey = String.format("field.%s.%s", getReference(), key); + String propertyKey = String.format("field.%s.%s", reference, key); return resourceBundle.containsKey(propertyKey) ? resourceBundle.getString(propertyKey) : propertyKey; } diff --git bankdroid-legacy/build.gradle bankdroid-legacy/build.gradle index 2cda778..e9b4290 100644 --- bankdroid-legacy/build.gradle +++ bankdroid-legacy/build.gradle @@ -2,13 +2,13 @@ apply plugin: 'com.android.library' apply from: '../config/quality/quality.gradle' android { - compileSdkVersion 24 - buildToolsVersion "24.0.3" + compileSdkVersion 25 + buildToolsVersion "25.0.1" useLibrary 'org.apache.http.legacy' defaultConfig { minSdkVersion 9 - targetSdkVersion 24 + targetSdkVersion 25 versionCode 1 versionName "1.0" } @@ -27,7 +27,7 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile project(':bankdroid-interface') - compile 'com.android.support:appcompat-v7:24.2.1' + compile 'com.android.support:appcompat-v7:25.2.0' compile 'com.jakewharton.timber:timber:4.3.1' compile ('org.apache.commons:commons-io:1.3.2') {exclude module: 'commons-io'} compile 'org.jsoup:jsoup:1.7.3' diff --git build.gradle build.gradle index 76cbdfb..bf6f81e 100644 --- build.gradle +++ build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:2.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -30,5 +30,5 @@ subprojects { } task wrapper(type: Wrapper) { - gradleVersion = '3.1' + gradleVersion = '3.3' } diff --git gradle/wrapper/gradle-wrapper.properties gradle/wrapper/gradle-wrapper.properties index 8893515..382630b 100644 --- gradle/wrapper/gradle-wrapper.properties +++ gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Sep 29 21:09:55 CEST 2016 +#Thu Mar 02 21:11:13 CET 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-bin.zip commit 0303e7dbb40f21d278fa7aad7274430f4a871183 Merge: 3c25a39 a08f401 Author: Mathias Ã…hsberg Date: Sat Feb 25 20:49:34 2017 +0100 Merge pull request #680 from liato/feature/remove-avanza Remove support for Avanza and Avanza mini commit a08f401c4d0b53cbbac74aaadd5527f847ccbee0 Author: Mathias AÌŠhsberg Date: Wed Feb 22 22:18:58 2017 +0100 Remove support for Avanza and Avnza mini diff --git CHANGELOG CHANGELOG index 6995d6b..53ddb44 100644 --- CHANGELOG +++ CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. (Unreleased) * Update certificates +* Remove support for Avanza and Avanza Mini v1.9.14 (2017-01-06) * Updated certificates for First Card, Osuuspankki and Östgötatrafiken diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankFactory.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankFactory.java index f95ef32..2bdf551 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankFactory.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankFactory.java @@ -4,7 +4,6 @@ import com.liato.bankdroid.banking.banks.AkeliusInvest; import com.liato.bankdroid.banking.banks.AkeliusSpar; import com.liato.bankdroid.banking.banks.americanexpress.AmericanExpress; import com.liato.bankdroid.banking.banks.AppeakPoker; -import com.liato.bankdroid.banking.banks.AvanzaMini; import com.liato.bankdroid.banking.banks.BetterGlobe; import com.liato.bankdroid.banking.banks.Bioklubben; import com.liato.bankdroid.banking.banks.BlekingeTrafiken; @@ -36,7 +35,6 @@ import com.liato.bankdroid.banking.banks.TestBank; import com.liato.bankdroid.banking.banks.TicketRikskortet; import com.liato.bankdroid.banking.banks.Vasttrafik; import com.liato.bankdroid.banking.banks.Zidisha; -import com.liato.bankdroid.banking.banks.avanza.Avanza; import com.liato.bankdroid.banking.banks.bitcoin.Bitcoin; import com.liato.bankdroid.banking.banks.coop.Coop; import com.liato.bankdroid.banking.banks.ica.ICA; @@ -71,10 +69,6 @@ public class LegacyBankFactory { return new Coop(context); case IBankTypes.ICA: return new ICA(context); - case IBankTypes.AVANZA: - return new Avanza(context); - case IBankTypes.AVANZAMINI: - return new AvanzaMini(context); case IBankTypes.OKQ8: return new OKQ8(context); case IBankTypes.FIRSTCARD: @@ -157,8 +151,6 @@ public class LegacyBankFactory { banks.add(new Lansforsakringar(context)); banks.add(new Coop(context)); banks.add(new ICA(context)); - banks.add(new Avanza(context)); - banks.add(new AvanzaMini(context)); banks.add(new OKQ8(context)); banks.add(new FirstCard(context)); banks.add(new Payson(context)); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AvanzaMini.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AvanzaMini.java deleted file mode 100644 index 5f7889b..0000000 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AvanzaMini.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2014 Nullbyte - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.liato.bankdroid.banking.banks; - -import com.liato.bankdroid.banking.banks.avanza.Avanza; -import com.liato.bankdroid.legacy.R; -import com.liato.bankdroid.provider.IBankTypes; - -import android.content.Context; - -public class AvanzaMini extends Avanza { - - public AvanzaMini(Context context) { - super(context, R.drawable.logo_avanzamini); - url = "https://www.avanza.se/mini/hem/"; - } - - @Override - public int getBanktypeId() { - return IBankTypes.AVANZAMINI; - } - - @Override - public String getName() { - return "Avanza Mini"; - } -} diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/Avanza.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/Avanza.java deleted file mode 100644 index 3d3aa64..0000000 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/Avanza.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (C) 2014 Nullbyte - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.liato.bankdroid.banking.banks.avanza; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.liato.bankdroid.Helpers; -import com.liato.bankdroid.banking.Account; -import com.liato.bankdroid.banking.Bank; -import com.liato.bankdroid.banking.Transaction; -import com.liato.bankdroid.banking.banks.avanza.model.AccountOverview; -import com.liato.bankdroid.banking.banks.avanza.model.Position; -import com.liato.bankdroid.banking.banks.avanza.model.PositionAggregation; -import com.liato.bankdroid.banking.exceptions.BankChoiceException; -import com.liato.bankdroid.banking.exceptions.BankException; -import com.liato.bankdroid.banking.exceptions.LoginException; -import com.liato.bankdroid.legacy.R; -import com.liato.bankdroid.provider.IBankTypes; -import com.liato.bankdroid.utils.StringUtils; - -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.message.BasicNameValuePair; -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.Context; -import android.support.annotation.DrawableRes; -import android.util.Base64; - -import java.io.IOException; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import eu.nullbyte.android.urllib.CertificateReader; -import eu.nullbyte.android.urllib.Urllib; - -public class Avanza extends Bank { - - private static final String API_URL = "https://iphone.avanza.se/iphone-ws/"; - - protected Avanza(Context context, @DrawableRes int logoResource) { - super(context, logoResource); - url = "https://iphone.avanza.se"; - } - - @Override - public int getBanktypeId() { - return IBankTypes.AVANZA; - } - - @Override - public String getName() { - return "Avanza"; - } - - public Avanza(Context context) { - this(context, R.drawable.logo_avanza); - } - - @Override - protected LoginPackage preLogin() throws BankException, IOException { - urlopen = new Urllib(context, - CertificateReader.getCertificates(context, R.raw.cert_avanza)); - urlopen.addHeader("Referer", url + "/start"); - List postData = new ArrayList(); - postData.add(new BasicNameValuePair("j_username", getUsername())); - postData.add(new BasicNameValuePair("j_password", getPassword())); - postData.add(new BasicNameValuePair("url", url + "/start")); - String response = urlopen.open(url + "/ab/handlelogin", postData); - String homeUrl = ""; - try { - JSONObject jsonResponse = new JSONObject(response); - homeUrl = jsonResponse.getString("redirectUrl"); - } catch (JSONException e) { - throw new BankException( - res.getText(R.string.unable_to_find).toString() + " login link.", e); - } - LoginPackage lp = new LoginPackage(urlopen, postData, "", url + homeUrl); - lp.setIsLoggedIn(true); - return lp; - } - - public Urllib login() throws LoginException, BankException, IOException { - urlopen = new Urllib(context, - CertificateReader.getCertificates(context, R.raw.cert_avanza)); - urlopen.addHeader("ctag", "1122334455"); - urlopen.addHeader("Authorization", "Basic " + Base64.encodeToString( - StringUtils.getBytes(getUsername() + ":" + getPassword()), Base64.NO_WRAP)); - balance = new BigDecimal(0); - - try { - HttpResponse httpResponse = urlopen.openAsHttpResponse(API_URL + "account/overview/all", - new ArrayList(), false); - if (httpResponse.getStatusLine().getStatusCode() == 401) { - throw new LoginException(context.getText( - R.string.invalid_username_password).toString()); - } - ObjectMapper vObjectMapper = new ObjectMapper(); - AccountOverview r = vObjectMapper.readValue(httpResponse.getEntity().getContent(), - AccountOverview.class); - for (com.liato.bankdroid.banking.banks.avanza.model.Account account : r.getAccounts()) { - Account a = new Account(account.getAccountName(), - new BigDecimal(account.getOwnCapital()), account.getAccountId()); - if (!account.getCurrencyAccounts().isEmpty()) { - a.setCurrency(account.getCurrencyAccounts().get(0).getCurrency()); - } - if (!account.getPositionAggregations().isEmpty()) { - Date now = new Date(); - ArrayList transactions = new ArrayList(); - for (com.liato.bankdroid.banking.banks.avanza.model.CurrencyAccount currencyAccount : account - .getCurrencyAccounts()) { - transactions.add(new Transaction(Helpers.formatDate(now), - "\u2014 " + currencyAccount.getCurrency() + " \u2014", - BigDecimal.valueOf(currencyAccount.getBalance()), - currencyAccount.getCurrency())); - } - for (PositionAggregation positionAgList : account.getPositionAggregations()) { - if (positionAgList.getPositions().isEmpty()) { - continue; - } - List positions = positionAgList.getPositions(); - transactions.add(new Transaction(Helpers.formatDate(now), - "\u2014 " + positionAgList.getInstrumentTypeName() + - " " + positionAgList.getTotalProfitPercent() + "% \u2014", - BigDecimal.valueOf(positionAgList.getTotalValue()), - a.getCurrency())); - for (Position p : positions) { - Transaction t = new Transaction(Helpers.formatDate(now), - p.getInstrumentName(), - BigDecimal.valueOf(p.getProfit()), - a.getCurrency()); - transactions.add(t); - } - } - a.setTransactions(transactions); - } - balance = balance.add(a.getBalance()); - accounts.add(a); - // Add subtypes for account as own account. - if (!account.getPositionAggregations().isEmpty()) { - Date now = new Date(); - for (com.liato.bankdroid.banking.banks.avanza.model.CurrencyAccount currencyAccount : account - .getCurrencyAccounts()) { - Account b = new Account("\u2014 " + account.getAccountId() + ", " + - currencyAccount.getCurrency(), - new BigDecimal(currencyAccount.getBalance()), - account.getAccountId() + currencyAccount.getCurrency(), - Account.OTHER, - currencyAccount.getCurrency()); - b.setHidden(true); - accounts.add(b); - } - for (PositionAggregation positionAgList : account.getPositionAggregations()) { - if (positionAgList.getPositions().isEmpty()) { - continue; - } - Account b = new Account("\u2014 " + account.getAccountId() + ", " + - positionAgList.getInstrumentTypeName() + - " " + positionAgList.getTotalProfitPercent() + "% ", - new BigDecimal(positionAgList.getTotalValue()), - account.getAccountId() + positionAgList.getInstrumentTypeName(), - Account.OTHER, a.getCurrency()); - b.setHidden(true); - ArrayList transactions = new ArrayList(); - for (Position p : positionAgList.getPositions()) { - transactions.add(new Transaction(Helpers.formatDate(now), - p.getInstrumentName(), - BigDecimal.valueOf(p.getProfit()), - a.getCurrency())); - } - b.setTransactions(transactions); - accounts.add(b); - } - } - } - } catch (JsonParseException e) { - throw new BankException(e.getMessage(), e); - } - return urlopen; - } - - @Override - public void update() throws BankException, LoginException, - BankChoiceException, IOException { - super.update(); - if (getUsername().isEmpty() || getPassword().isEmpty()) { - throw new LoginException(res.getText( - R.string.invalid_username_password).toString()); - } - login(); - if (accounts.isEmpty()) { - throw new BankException(res.getText(R.string.no_accounts_found).toString()); - } - super.updateComplete(); - } -} diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/Account.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/Account.java deleted file mode 100644 index 114b003..0000000 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/Account.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.liato.bankdroid.banking.banks.avanza.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.io.Serializable; -import java.util.Collections; -import java.util.List; - -public class Account implements Serializable { - - private static final long serialVersionUID = -5718585872348469144L; - - @JsonProperty("balance") - private double mBalance; - - @JsonProperty("totalProfit") - private double mTotalProfit; - - @JsonProperty("accountName") - private String mAccountName; - - @JsonProperty("totalAccruedInterest") - private double mTotalAccruedInterest; - - @JsonProperty("adjustedForwardAmount") - private double mAdjustedForwardAmount; - - @JsonProperty("unUsedCredit") - private double mUnUsedCredit; - - @JsonProperty("superInterest") - private double mSuperInterest; - - @JsonProperty("totalMarginRequirement") - private double mTotalMarginRequirement; - - @JsonProperty("tradingPower") - private double mTradingPower; - - @JsonProperty("resAmount") - private double mResAmount; - - @JsonProperty("loanAmount") - private double mLoanAmount; - - @JsonProperty("accountId") - private String mAccountId; - - @JsonProperty("currencyAccounts") - private List mCurrencyAccounts = Collections.emptyList(); - - @JsonProperty("creditLimit") - private double mCreditLimit; - - @JsonProperty("totalProfitPercent") - private double mTotalProfitPercent; - - @JsonProperty("ownCapital") - private double mOwnCapital; - - @JsonProperty("totalValue") - private double mTotalValue; - - @JsonProperty("interestAmount") - private double mInterestAmount; - - @JsonProperty("secAmount") - private double mSecAmount; - - @JsonProperty("positionAggregations") - private List mPositionAggregations = Collections.emptyList(); - - - @JsonProperty("balance") - public double getBalance() { - return mBalance; - } - - @JsonProperty("totalProfit") - public double getTotalProfit() { - return mTotalProfit; - } - - @JsonProperty("accountName") - public String getAccountName() { - return mAccountName; - } - - @JsonProperty("totalAccruedInterest") - public double getTotalAccruedInterest() { - return mTotalAccruedInterest; - } - - @JsonProperty("adjustedForwardAmount") - public double getAdjustedForwardAmount() { - return mAdjustedForwardAmount; - } - - @JsonProperty("unUsedCredit") - public double getUnUsedCredit() { - return mUnUsedCredit; - } - - @JsonProperty("superInterest") - public double getSuperInterest() { - return mSuperInterest; - } - - @JsonProperty("totalMarginRequirement") - public double getTotalMarginRequirement() { - return mTotalMarginRequirement; - } - - @JsonProperty("tradingPower") - public double getTradingPower() { - return mTradingPower; - } - - @JsonProperty("resAmount") - public double getResAmount() { - return mResAmount; - } - - @JsonProperty("loanAmount") - public double getLoanAmount() { - return mLoanAmount; - } - - @JsonProperty("accountId") - public String getAccountId() { - return mAccountId; - } - - @JsonProperty("currencyAccounts") - public List getCurrencyAccounts() { - return mCurrencyAccounts; - } - - @JsonProperty("creditLimit") - public double getCreditLimit() { - return mCreditLimit; - } - - @JsonProperty("totalProfitPercent") - public double getTotalProfitPercent() { - return mTotalProfitPercent; - } - - @JsonProperty("ownCapital") - public double getOwnCapital() { - return mOwnCapital; - } - - @JsonProperty("totalValue") - public double getTotalValue() { - return mTotalValue; - } - - @JsonProperty("interestAmount") - public double getInterestAmount() { - return mInterestAmount; - } - - @JsonProperty("secAmount") - public double getSecAmount() { - return mSecAmount; - } - - @JsonProperty("positionAggregations") - public List getPositionAggregations() { - return mPositionAggregations; - } - -} diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/AccountOverview.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/AccountOverview.java deleted file mode 100644 index 19ac97f..0000000 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/AccountOverview.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.liato.bankdroid.banking.banks.avanza.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.io.Serializable; -import java.util.Collections; -import java.util.List; - -public class AccountOverview implements Serializable { - - private static final long serialVersionUID = -5511775495529857976L; - - @JsonProperty("totalOwnCapital") - private float mTotalOwnCapital; - - @JsonProperty("accounts") - private List mAccounts = Collections.emptyList(); - - - @JsonProperty("totalOwnCapital") - public float getTotalOwnCapital() { - return mTotalOwnCapital; - } - - @JsonProperty("accounts") - public List getAccounts() { - return mAccounts; - } - -} diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/CurrencyAccount.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/CurrencyAccount.java deleted file mode 100644 index 76cf0b6..0000000 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/CurrencyAccount.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.liato.bankdroid.banking.banks.avanza.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.io.Serializable; - -public class CurrencyAccount implements Serializable { - - private static final long serialVersionUID = 6004713686055778196L; - - @JsonProperty("currency") - private String mCurrency; - - @JsonProperty("balance") - private double mBalance; - - @JsonProperty("accountId") - private String mAccountId; - - - @JsonProperty("currency") - public String getCurrency() { - return mCurrency; - } - - @JsonProperty("balance") - public double getBalance() { - return mBalance; - } - - @JsonProperty("accountId") - public String getAccountId() { - return mAccountId; - } - -} \ No newline at end of file diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/Position.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/Position.java deleted file mode 100644 index 0182251..0000000 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/Position.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.liato.bankdroid.banking.banks.avanza.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.io.Serializable; - -public class Position implements Serializable { - - private static final long serialVersionUID = 4138023852221811457L; - - @JsonProperty("instrumentName") - private String mInstrumentName; - - @JsonProperty("averageAcquiredPrice") - private double mAverageAcquiredPrice; - - @JsonProperty("marketValue") - private double mMarketValue; - - @JsonProperty("price") - private double mPrice; - - @JsonProperty("profit") - private double mProfit; - - @JsonProperty("modified") - private long mModified; - - @JsonProperty("expiryDate") - private long mExpiryDate; - - @JsonProperty("volume") - private int mVolume; - - @JsonProperty("tradable") - private boolean mTradable; - - @JsonProperty("orderbookId") - private long mOrderbookId; - - @JsonProperty("profitPercent") - private double mProfitPercent; - - @JsonProperty("type") - private int mType; - - @JsonProperty("instrumentType") - private String mInstrumentType; - - @JsonProperty("change") - private double mChange; - - - @JsonProperty("instrumentName") - public String getInstrumentName() { - return mInstrumentName; - } - - @JsonProperty("averageAcquiredPrice") - public double getAverageAcquiredPrice() { - return mAverageAcquiredPrice; - } - - @JsonProperty("marketValue") - public double getMarketValue() { - return mMarketValue; - } - - @JsonProperty("price") - public double getPrice() { - return mPrice; - } - - @JsonProperty("profit") - public double getProfit() { - return mProfit; - } - - @JsonProperty("modified") - public long getModified() { - return mModified; - } - - @JsonProperty("expiryDate") - public long getExpiryDate() { - return mExpiryDate; - } - - @JsonProperty("volume") - public int getVolume() { - return mVolume; - } - - @JsonProperty("tradable") - public boolean getTradable() { - return mTradable; - } - - @JsonProperty("orderbookId") - public long getOrderbookId() { - return mOrderbookId; - } - - @JsonProperty("profitPercent") - public double getProfitPercent() { - return mProfitPercent; - } - - @JsonProperty("type") - public int getType() { - return mType; - } - - @JsonProperty("instrumentType") - public String getInstrumentType() { - return mInstrumentType; - } - - @JsonProperty("change") - public double getChange() { - return mChange; - } - -} \ No newline at end of file diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/PositionAggregation.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/PositionAggregation.java deleted file mode 100644 index 3571eca..0000000 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/model/PositionAggregation.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.liato.bankdroid.banking.banks.avanza.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.io.Serializable; -import java.util.Collections; -import java.util.List; - -public class PositionAggregation implements Serializable { - - private static final long serialVersionUID = 5531947007427482418L; - - @JsonProperty("totalChange") - private double mTotalChange; - - @JsonProperty("positions") - private List mPositions = Collections.emptyList(); - - @JsonProperty("totalProfit") - private double mTotalProfit; - - @JsonProperty("instrumentTypeName") - private String mInstrumentTypeName; - - @JsonProperty("totalProfitPercent") - private double mTotalProfitPercent; - - @JsonProperty("totalValue") - private double mTotalValue; - - @JsonProperty("instrumentType") - private int mInstrumentType; - - @JsonProperty("totalAverage") - private double mTotalAverage; - - - @JsonProperty("totalChange") - public double getTotalChange() { - return mTotalChange; - } - - @JsonProperty("positions") - public List getPositions() { - return mPositions; - } - - @JsonProperty("totalProfit") - public double getTotalProfit() { - return mTotalProfit; - } - - @JsonProperty("instrumentTypeName") - public String getInstrumentTypeName() { - return mInstrumentTypeName; - } - - @JsonProperty("totalProfitPercent") - public double getTotalProfitPercent() { - return mTotalProfitPercent; - } - - @JsonProperty("totalValue") - public double getTotalValue() { - return mTotalValue; - } - - @JsonProperty("instrumentType") - public int getInstrumentType() { - return mInstrumentType; - } - - @JsonProperty("totalAverage") - public double getTotalAverage() { - return mTotalAverage; - } - -} \ No newline at end of file diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/provider/IBankTypes.java bankdroid-legacy/src/main/java/com/liato/bankdroid/provider/IBankTypes.java index 230dbd5..b72ba85 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/provider/IBankTypes.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/provider/IBankTypes.java @@ -27,8 +27,6 @@ public interface IBankTypes { int LANSFORSAKRINGAR = 4; int COOP = 6; int ICA = 7; - int AVANZA = 9; - int AVANZAMINI = 11; int OKQ8 = 12; int FIRSTCARD = 14; int PAYSON = 16; diff --git bankdroid-legacy/src/main/res/drawable/logo_avanza.png bankdroid-legacy/src/main/res/drawable/logo_avanza.png deleted file mode 100644 index 1d5b836..0000000 Binary files bankdroid-legacy/src/main/res/drawable/logo_avanza.png and /dev/null differ diff --git bankdroid-legacy/src/main/res/drawable/logo_avanzamini.png bankdroid-legacy/src/main/res/drawable/logo_avanzamini.png deleted file mode 100644 index f19d914..0000000 Binary files bankdroid-legacy/src/main/res/drawable/logo_avanzamini.png and /dev/null differ diff --git bankdroid-legacy/src/main/res/raw/cert_avanza.pem bankdroid-legacy/src/main/res/raw/cert_avanza.pem deleted file mode 100644 index 0cea5a7..0000000 --- bankdroid-legacy/src/main/res/raw/cert_avanza.pem +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEuzCCA6OgAwIBAgIQPw5F1jSoeVE1WSbiXJQYqDANBgkqhkiG9w0BAQsFADBB -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3RlLCBJbmMuMRswGQYDVQQDExJ0 -aGF3dGUgU1NMIENBIC0gRzIwHhcNMTUwNTI5MDAwMDAwWhcNMTcwNTI4MjM1OTU5 -WjB8MQswCQYDVQQGEwJTRTEYMBYGA1UECAwPU3RvY2tob2xtcyBsw6RuMRIwEAYD -VQQHDAlTdG9ja2hvbG0xFzAVBgNVBAoMDkF2YW56YSBCYW5rIEFCMQswCQYDVQQL -DAJJVDEZMBcGA1UEAwwQaXBob25lLmF2YW56YS5zZTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBAMbCiEmYA1O3hIW4PR2v2KJ4A0pHKRjMSgvANXIRCc/I -E8yHV/i0ZyCy3b94qIAJsZwLUi06si2vvIqLC4H9gUGaYL0EROHqM03BtgvZDe9D -jk8dusJMxi1415SanHh+MuqwfNEB2yYvMJpBMZB+KciBcqX7DTdaC/l3bJrVBXOQ -IbE0zZcz/TGg/R2gA2ZpDr3C5hUkfWvM+ic294TCF0ey6HRQYk9x0aASwOe5DWJM -JMwrl33Jk4AnLKupC825JIM9rR7CvLrZ2vGXzLDQh+wz2/FY+yTuOTu+z2NrH5FY -xwpqwSTk7r9mCh4Xy2lpqz3VVywMPf5T5LdzX9yY/VsCAwEAAaOCAXIwggFuMBsG -A1UdEQQUMBKCEGlwaG9uZS5hdmFuemEuc2UwCQYDVR0TBAIwADBuBgNVHSAEZzBl -MGMGBmeBDAECAjBZMCYGCCsGAQUFBwIBFhpodHRwczovL3d3dy50aGF3dGUuY29t -L2NwczAvBggrBgEFBQcCAjAjDCFodHRwczovL3d3dy50aGF3dGUuY29tL3JlcG9z -aXRvcnkwDgYDVR0PAQH/BAQDAgWgMB8GA1UdIwQYMBaAFMJPSFf80U+awF04fQ4F -29kutVJgMCsGA1UdHwQkMCIwIKAeoByGGmh0dHA6Ly90ai5zeW1jYi5jb20vdGou -Y3JsMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBXBggrBgEFBQcBAQRL -MEkwHwYIKwYBBQUHMAGGE2h0dHA6Ly90ai5zeW1jZC5jb20wJgYIKwYBBQUHMAKG -Gmh0dHA6Ly90ai5zeW1jYi5jb20vdGouY3J0MA0GCSqGSIb3DQEBCwUAA4IBAQAl -Ojf5ZgrFjk6SJHpOWcwTbDf5OX8AnxBat8xvqxkoqAXHtkfC+B5vAbYqUT+JZE7O -79roncesD8zzcT4L5iR+axHbB4fz7JNSbqISbYscDZA1bV69ciJs1XkvvvI3DUbB -t3aVAa2ArnINI8IxxJVeQ8S416jgN6PlnDRMCGSFjOWC76Lc9M4wLmghW9lfeW+I -knWQC+TGgu824yVYf6GlaAPpdwc+7M6bDc4TDsRzC/BKfMNGPdaBbrS8J5PKGGwd -YJ2NYv4EGMhgR0u8ZT68X/B5RYfZZc0pQuiEEtv7MlPj4t+xbRKXm0HxneBblLxV -fIBstELLSRgDEJNBmd8N ------END CERTIFICATE----- -iphone.avanza.se:443 commit 3c25a3917ecdb1997b833ec259d3bdc42cb9d26b Author: Mathias AÌŠhsberg Date: Wed Feb 22 22:07:48 2017 +0100 Update build tools to version 2.2.3 diff --git build.gradle build.gradle index 43763d6..76cbdfb 100644 --- build.gradle +++ build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.2' + classpath 'com.android.tools.build:gradle:2.2.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files commit 31bf4e66992d3057c4cab3d711f99e666fc0883c Author: Mathias AÌŠhsberg Date: Wed Feb 22 21:28:27 2017 +0100 Update certificates diff --git CHANGELOG CHANGELOG index 1fbef9f..6995d6b 100644 --- CHANGELOG +++ CHANGELOG @@ -1,5 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. +(Unreleased) +* Update certificates + v1.9.14 (2017-01-06) * Updated certificates for First Card, Osuuspankki and Östgötatrafiken diff --git bankdroid-legacy/src/main/res/raw/cert_americanexpress_global.pem bankdroid-legacy/src/main/res/raw/cert_americanexpress_global.pem index 3057379..d4338ef 100644 --- bankdroid-legacy/src/main/res/raw/cert_americanexpress_global.pem +++ bankdroid-legacy/src/main/res/raw/cert_americanexpress_global.pem @@ -1,52 +1,48 @@ -----BEGIN CERTIFICATE----- -MIIJGDCCCACgAwIBAgIQBcnDhS/kTPYwT7xQkKTIiTANBgkqhkiG9w0BAQsFADB1 +MIIIYDCCB0igAwIBAgIQAaqnbsay6zOICUMaChU+CDANBgkqhkiG9w0BAQsFADB1 MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk -IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE2MTIwNjAwMDAwMFoXDTE4MTIxMTEy -MDAwMFowggEXMR0wGwYDVQQPDBRQcml2YXRlIE9yZ2FuaXphdGlvbjETMBEGCysG +IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE3MDEyNDAwMDAwMFoXDTE5MDEyOTEy +MDAwMFowggEcMR0wGwYDVQQPDBRQcml2YXRlIE9yZ2FuaXphdGlvbjETMBEGCysG AQQBgjc8AgEDEwJVUzEZMBcGCysGAQQBgjc8AgECEwhOZXcgWW9yazEPMA0GA1UE BRMGMTg4MDU1MR4wHAYDVQQJExUzMTUxIFcuIEJlaHJlbmQgRHJpdmUxDjAMBgNV BBETBTg1MDI3MQswCQYDVQQGEwJVUzEQMA4GA1UECBMHQXJpem9uYTEQMA4GA1UE -BxMHUGhvZW5peDEhMB8GA1UEChMYQW1lcmljYW4gRXhwcmVzcyBDb21wYW55MQww -CgYDVQQLEwNOR0kxIzAhBgNVBAMTGmdsb2JhbC5hbWVyaWNhbmV4cHJlc3MuY29t -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArm1h2NyOnMpJE69RKSLN -K3VXgzknpKXiWoW60+XnHvmCknUo1HSzKcWhUSYgDyGrziLs39QB3ygVKzkhp1Jm -dCrCCLugcyowU5ILbIoGxehsbu/WsnJdW75sgo21QxmiT7lmhkfioruXbmFSHPk3 -NmfhTTnzLLjrm4DTWhpM7QXbedyL5/r5U4usUAMTQrHffVQFI4A26lnv3uA0PeF5 -17onx8ivwKTIXUTY64utgNI7qqF1zFwMtQioReXeoHGhF7a+KDMLNKT1fY2/1t8N -NLsEcTcZl9hGhSSUx7zYDYyb6Syurz9U9IYGCP33LnMxU9wPSjuDe9FZZODawSKm -wQIDAQABo4IE/jCCBPowHwYDVR0jBBgwFoAUPdNQpdagre7zSmAKZdMh1Pj41g8w -HQYDVR0OBBYEFBqefDrKnrSSF++OIDOKR08xuuArMHsGA1UdEQR0MHKCGmdsb2Jh -bC5hbWVyaWNhbmV4cHJlc3MuY29tgiluZ2lvcmlnaW4taXBjMS1nbG9iYWwuYW1l -cmljYW5leHByZXNzLmNvbYIpbmdpb3JpZ2luLWlwYzItZ2xvYmFsLmFtZXJpY2Fu -ZXhwcmVzcy5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMB -BggrBgEFBQcDAjB1BgNVHR8EbjBsMDSgMqAwhi5odHRwOi8vY3JsMy5kaWdpY2Vy -dC5jb20vc2hhMi1ldi1zZXJ2ZXItZzEuY3JsMDSgMqAwhi5odHRwOi8vY3JsNC5k -aWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzEuY3JsMEsGA1UdIAREMEIwNwYJ -YIZIAYb9bAIBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNv -bS9DUFMwBwYFZ4EMAQEwgYgGCCsGAQUFBwEBBHwwejAkBggrBgEFBQcwAYYYaHR0 -cDovL29jc3AuZGlnaWNlcnQuY29tMFIGCCsGAQUFBzAChkZodHRwOi8vY2FjZXJ0 -cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyRXh0ZW5kZWRWYWxpZGF0aW9uU2Vy -dmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAwggKtBgorBgEEAdZ5AgQCBIICnQSCApkC -lwB1AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAABWNTrpfsAAAQD -AEYwRAIgFR/0U2pLZPoB4kVEeYMi9LqkYgFBM9hJf+A1uYT9354CIAqFZ17SAQ8V -/H+tpdQY/zIl3lTnZBEFLST6E6t+xfe+AS8ArDua7X+pZ0dXFZ5tfVdWcvnZgQCU -Hpve/+yhMTt1eC0AAAFY1OusCgAABAEBAD3psaSp1qs4oLVFAuHYX0ZwOf5x3TAl -jyQbbLF1r5/mbIHzR6LkP5r7bfltTMtMmyc+u/771shPu6sAucM06nPkJF/VhvRM -OlY6+nSTmVq4gW9a+EjcBefsRCRKa6d00rhSYTx1ZVIa87vSacHD5J+LMgcvTW6N -YRW3d8zdbVUsbeir7dDx9Diu3aR1bmrjPpt4fEU/GOXOOdy7DFpp8wYN7zaGUhuh -ThftdZIWhi0QEnZALodco2aZOTFgv2q1Uicf861+M6+QIlwg4zTQEF/rW+12Q5ob -UivWtV7R9cu83ZenZ+UBeF4t61CF7aJUaoV1yDEkrSc80SYg2l4P9/cAdQBWFAaa -L9fC7NP14b1Esj7HRna5vJkRXMDvlJhV1onQ3QAAAVjU66aeAAAEAwBGMEQCICyv -YtBEG+gUyhkDtf6MCY8LhmCQdPIEezWLlw7d6o/AAiAeuK4vWCe4ch/LCABsHh5x -ODW/8QG5EjiXjbZ08PfPJAB2AO5Lvbd1zmC64UJpH6vhnmajD35fsHLYgwDEe4l6 -qP3LAAABWNTrqAkAAAQDAEcwRQIgEKsq+0O+H+50tY0s9A/2owkSY6diBkYTt2ZC -+sNP8JYCIQDIchquB/qmN1UHn8n3DU0JJ8Zn8XqyVeTT6/kTAx7MCjANBgkqhkiG -9w0BAQsFAAOCAQEAv5feWkUjogtKQgC5g2ORTcFgM+FjfXtHN7omZ2pjSXI2xVND -VhTa0sbleHmB09vwY9v9orp68jxEvWM9FIpdRRiVmv4eJLGyBBaICxp9bePMK82R -pMObMN9tBAMHd55rnllWE0rME9dB8WvoOkhY7A1BBVY5m86s0H3puOXStiJpAG1j -JbSeJ4MoGpqbOryiOs/HcLIyHQOpNkykd2BuxxNW/qWQVFhPNvaVgeYy6lXunAVk -CcNfyrTOKwj0D3JkXUzwYLxSRAEVHNjIxsJT5mJFnPuLd1Te2EDkNuoceYIA+OoE -jTe/+O2Jm+Nw3PkYwmjtsc0EhaHp7HcPgHZPtQ== +BxMHUGhvZW5peDEhMB8GA1UEChMYQW1lcmljYW4gRXhwcmVzcyBDb21wYW55MREw +DwYDVQQLEwhDb25zdW1lcjEjMCEGA1UEAxMab25saW5lLmFtZXJpY2FuZXhwcmVz +cy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOe2+mom21AxhL +BDJNgENfGvhF8z4mcTP5EnpvJno/7+46dPcy6otIEAx9mMbBM7eiYITO6ybPMldV +epcg0WMoNrJ0xJlp+KwKIOxP+Vg16Rz7c07fDbTaCZfzR1qfdODD8hdGKDLuaT0D +f142jNmYG0WibBaykN3E5kvmZ6EvnwwGvfEIfVD/4TOB0fFqexWWVDJRKNQH0wKc +VCY9UIk3BoagWKzLxV5aU5lqCHf8PAJOZ6WEnomHuXYv7Pvg3XNaxxNYfesYms09 +lEaxnLhqZIhhItfilllTX13n+vPFYTUsAJ6qnzAemJqzLsC4LAXQyz7D+TQ0nXAJ +KJnoBYVRAgMBAAGjggRBMIIEPTAfBgNVHSMEGDAWgBQ901Cl1qCt7vNKYApl0yHU ++PjWDzAdBgNVHQ4EFgQUo25mMJ/p72Dun3+lrHE+72I9vSIwdwYDVR0RBHAwboIa +b25saW5lLmFtZXJpY2FuZXhwcmVzcy5jb22CG3Jld2FyZHMuYW1lcmljYW5leHBy +ZXNzLmNvbYIXcmV3YXJkcy5hZXhwLXN0YXRpYy5jb22CGmdsb2JhbC5hbWVyaWNh +bmV4cHJlc3MuY29tMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD +AQYIKwYBBQUHAwIwdQYDVR0fBG4wbDA0oDKgMIYuaHR0cDovL2NybDMuZGlnaWNl +cnQuY29tL3NoYTItZXYtc2VydmVyLWcxLmNybDA0oDKgMIYuaHR0cDovL2NybDQu +ZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVyLWcxLmNybDBLBgNVHSAERDBCMDcG +CWCGSAGG/WwCATAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j +b20vQ1BTMAcGBWeBDAEBMIGIBggrBgEFBQcBAQR8MHowJAYIKwYBBQUHMAGGGGh0 +dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBSBggrBgEFBQcwAoZGaHR0cDovL2NhY2Vy +dHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkV4dGVuZGVkVmFsaWRhdGlvblNl +cnZlckNBLmNydDAMBgNVHRMBAf8EAjAAMIIB9AYKKwYBBAHWeQIEAgSCAeQEggHg +Ad4AdQCkuQmQtBhYFIe7E6LMZ3AKPDWYBPkb37jjd80OyA3cEAAAAVnR4Z7LAAAE +AwBGMEQCIGyUTKolX7fCjfAx0evgDlGtLmRqF/LFp3MCWl8BNY3PAiAlMUhh0X6F +Dedm4odESahntfy8d/3WV02oBfcyPJypEwB2AFYUBpov18Ls0/XhvUSyPsdGdrm8 +mRFcwO+UmFXWidDdAAABWdHhoB4AAAQDAEcwRQIgS1sYO973uUF1ck53HVNr0kIA +kuYjUOau3K1tFllKz24CIQDYs45QOuiF91Cyr25jycnX8ppdfFjAOFmFZtC3pMkf +OwB1AO5Lvbd1zmC64UJpH6vhnmajD35fsHLYgwDEe4l6qP3LAAABWdHhodwAAAQD +AEYwRAIgCCu2KCYMQez7uGnoa3zMcg03Lj7+ubTmMe7WBBldQi4CIHg3clmsny5h +erX47gKuoEbixn0vlUa2v3MyfIVgL0n1AHYAu9nfvB+KcbWTlCOXqpJ7RzhXlQqr +UugakJZkNo4e0YUAAAFZ0eGf8QAABAMARzBFAiAi7IWcvkTTbpPkxqFPDffBGMH0 +NuwWoRQHO5ljt0dlfgIhANFuzMU7pPpcvoKdnHNgfsbUp47XNlsJ92RQNyH/j3fQ +MA0GCSqGSIb3DQEBCwUAA4IBAQBN1H5e5Mzl5+R7ed8LuQgPyNBMx853wCI+lKuy +xUwGn/DJfNo+fvcywY12wFfsQAxxOTt8ecCs68JFYJlDgnzMumHBOVQf8Psb8OEN +W3emi7z8lnbQusItvSzZiArbrGLbPICumVrJn+eodzenWn6DAJEQAl3LbXOn4FGP +Kpijmf8hfb2I/UAwMISh4OiMcMHusoBHpekKxOTrCemLIbCKbV2sUV1o6PSN6SDq +u7fX158B+qQxcEgn0fjAs8CVqs31lJjo/h1kb/53u2iqZFH3P2sbr0jQkVNiL2LW +bdXhSyChR5z4w4lvj8HthWCs6kU+uzBHdtamdVkFU4d0dzi/ -----END CERTIFICATE----- global.americanexpress.com:443 diff --git bankdroid-legacy/src/main/res/raw/cert_bioklubben.pem bankdroid-legacy/src/main/res/raw/cert_bioklubben.pem index a01ed1e..e9de15f 100644 --- bankdroid-legacy/src/main/res/raw/cert_bioklubben.pem +++ bankdroid-legacy/src/main/res/raw/cert_bioklubben.pem @@ -1,35 +1,37 @@ -----BEGIN CERTIFICATE----- -MIIF4TCCBMmgAwIBAgIQVPm9MeoE7SQFdlULvDppQjANBgkqhkiG9w0BAQsFADBE +MIIGVjCCBT6gAwIBAgIQJ7c0DjE2rrf90ZxGr+exPTANBgkqhkiG9w0BAQsFADBE MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMU -R2VvVHJ1c3QgU1NMIENBIC0gRzMwHhcNMTYwMjAxMDAwMDAwWhcNMTcwMTMxMjM1 -OTU5WjBqMQswCQYDVQQGEwJTRTEYMBYGA1UECAwPU1RPQ0tIT0xNUyBMw6ROMQ4w -DAYDVQQHDAVTb2xuYTESMBAGA1UEChQJU0YgQmlvIEFCMQswCQYDVQQLDAJJVDEQ -MA4GA1UEAxQHKi5zZi5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AKLUmrPm9g2Z12yVD9jnGgk3C2xrdOYqjfjFGOrU2SfTaI1L8POt2s9YspYcKkFG -5EB7Iv3aIwL0TccsbWT1PaiVvT7hgU94n5fYjjHlMiVMdPpHunGj9KqO12/zz++e -Qa8xITx5AW3S7CYHIBqxPIU43/6ukXcmsGe4ngbHxhl7nfWcgWT/qxUA7guWqxON -KNaNOsw4KeJizxhURT52nf5+dJIZ9j/3x8vu+WC8kJ9LQzKdFuFOZ05/Ivwj+WcI -SbOOaisxaQCdkFMDtTfWPBX9CjEvAEqYsthFj2trvxZYgvnN6zR7eRdAH2chgrh9 -//ljApp85rzAVClGncBZKq0CAwEAAaOCAqcwggKjMBkGA1UdEQQSMBCCByouc2Yu -c2WCBXNmLnNlMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgWgMCsGA1UdHwQkMCIw -IKAeoByGGmh0dHA6Ly9nbi5zeW1jYi5jb20vZ24uY3JsMIGdBgNVHSAEgZUwgZIw -gY8GBmeBDAECAjCBhDA/BggrBgEFBQcCARYzaHR0cHM6Ly93d3cuZ2VvdHJ1c3Qu -Y29tL3Jlc291cmNlcy9yZXBvc2l0b3J5L2xlZ2FsMEEGCCsGAQUFBwICMDUMM2h0 -dHBzOi8vd3d3Lmdlb3RydXN0LmNvbS9yZXNvdXJjZXMvcmVwb3NpdG9yeS9sZWdh -bDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHwYDVR0jBBgwFoAU0m/3 -lvSFP3I8MH0j2oV4m6N8WnwwVwYIKwYBBQUHAQEESzBJMB8GCCsGAQUFBzABhhNo -dHRwOi8vZ24uc3ltY2QuY29tMCYGCCsGAQUFBzAChhpodHRwOi8vZ24uc3ltY2Iu -Y29tL2duLmNydDCCAQMGCisGAQQB1nkCBAIEgfQEgfEA7wB2AN3rHSt6DU+mIIuB -rYFocH4ujp0B1VyIjT0RxM227L7MAAABUp0mwQkAAAQDAEcwRQIgequqjt1sy5zl -gCK93rW9xs6YxLs1bCfp96HIYhZ49kACIQDhjtfaakCmfkQ9UDMBp7WpE1kA4PFl -jNIZnsUqdf4meAB1AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB -Up0mvTEAAAQDAEYwRAIgR0W9StBOqzQ85oWMWO6h/P4M+p77PJQrjgdyQlcePVUC -IHRoUg4Ywdh9ookJITb8K9dn5MGikr7I6om9QwWNBb+6MA0GCSqGSIb3DQEBCwUA -A4IBAQAlL/o5EhDh5lNkjJkxPzFnX4xuf2/PtyepQmlE9f0/o1ICgUgHDy2Vs2r/ -yCWyjKSl+APsd0AOuKJtxXm9s+TtHDRAysODUVhSQYLjWOZoWrpQ3d0bDhDHhy8S -3AaqoE65tRXbZHZ7sSB95XpKgPARwn918y1Oe6aTEE8JvSV/cXVyHbng5RB2ddJP -OFXUmh+/uLgbPQ/V4wPipTbWLDgA+D7XuyQVzCNvhPD4A8w0oBi/HxjjYMdPByoK -LjVEmYCdw91c/h6Avm9AUK92XGXkNdiML+hBEY2YJQZghH1yz98hbqfm0x5b43GI -Lk4ymn/K6HZMbtsrXfcWX8N3UmO5 +R2VvVHJ1c3QgU1NMIENBIC0gRzMwHhcNMTYxMjE1MDAwMDAwWhcNMTgwMzE2MjM1 +OTU5WjBkMQswCQYDVQQGEwJTRTESMBAGA1UECAwJU1RPQ0tIT0xNMQ4wDAYDVQQH +DAVTb2xuYTESMBAGA1UECgwJU0YgQmlvIEFCMQswCQYDVQQLDAJJVDEQMA4GA1UE +AwwHKi5zZi5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKLUmrPm +9g2Z12yVD9jnGgk3C2xrdOYqjfjFGOrU2SfTaI1L8POt2s9YspYcKkFG5EB7Iv3a +IwL0TccsbWT1PaiVvT7hgU94n5fYjjHlMiVMdPpHunGj9KqO12/zz++eQa8xITx5 +AW3S7CYHIBqxPIU43/6ukXcmsGe4ngbHxhl7nfWcgWT/qxUA7guWqxONKNaNOsw4 +KeJizxhURT52nf5+dJIZ9j/3x8vu+WC8kJ9LQzKdFuFOZ05/Ivwj+WcISbOOaisx +aQCdkFMDtTfWPBX9CjEvAEqYsthFj2trvxZYgvnN6zR7eRdAH2chgrh9//ljApp8 +5rzAVClGncBZKq0CAwEAAaOCAyIwggMeMBkGA1UdEQQSMBCCByouc2Yuc2WCBXNm +LnNlMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgWgMCsGA1UdHwQkMCIwIKAeoByG +Gmh0dHA6Ly9nbi5zeW1jYi5jb20vZ24uY3JsMIGdBgNVHSAEgZUwgZIwgY8GBmeB +DAECAjCBhDA/BggrBgEFBQcCARYzaHR0cHM6Ly93d3cuZ2VvdHJ1c3QuY29tL3Jl +c291cmNlcy9yZXBvc2l0b3J5L2xlZ2FsMEEGCCsGAQUFBwICMDUMM2h0dHBzOi8v +d3d3Lmdlb3RydXN0LmNvbS9yZXNvdXJjZXMvcmVwb3NpdG9yeS9sZWdhbDAdBgNV +HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHwYDVR0jBBgwFoAU0m/3lvSFP3I8 +MH0j2oV4m6N8WnwwVwYIKwYBBQUHAQEESzBJMB8GCCsGAQUFBzABhhNodHRwOi8v +Z24uc3ltY2QuY29tMCYGCCsGAQUFBzAChhpodHRwOi8vZ24uc3ltY2IuY29tL2du +LmNydDCCAX4GCisGAQQB1nkCBAIEggFuBIIBagFoAHYA3esdK3oNT6Ygi4GtgWhw +fi6OnQHVXIiNPRHEzbbsvswAAAFZAd73PAAABAMARzBFAiEA2pXLVLlPgndL3HEz +0c9DX2L2BJ86iWWr2hJvtMqN31ECIBR3r2mC1fxcm5fn3hdtWNl8GZmCzvRVL156 +qtMwyVgNAHYA7ku9t3XOYLrhQmkfq+GeZqMPfl+wctiDAMR7iXqo/csAAAFZAd73 +gwAABAMARzBFAiAtaIC43JHp6fn0NSYUjwh+V+i45Rx4R/An+F+eX46c8AIhAK/x +W4U0nreGDM7kYLg17Zf/IgdykcEWJUAo9C+pAeHYAHYAvHjh38X2PGhGSTNNoQ+h +Xwl5aSAJwIG08/aRfz7ZuKUAAAFZAd74FQAABAMARzBFAiEApvwu6gDBg3h/04gQ +Xo68xPtQhnhBK6KjDHy9+gRHj4wCIB6JlTJu/PB6mxn4EZJMJkmDjg1zul49LIKN +lBSp8U0IMA0GCSqGSIb3DQEBCwUAA4IBAQCJuRpRxVPeK4iY19oPDdsXOkewpI1C +Z/qM9gGyohgHSWpoTgOl3VpRXQxoyOHAjP09y9IfPcXE53tdUqRbXIyr2CLxbaMj +Q8SkZl5li+XxYmCLSVBBeo9CZoPuztBI5i+KAki5tHtfOjynDyPmGs4UYH62c1A1 +zvPjue4k2aP6Ea7PE5JzfFiWauoc529QNjE4s84YuM0eJBU8Qv2Bz2/YrjuRhkDx +VyolwHaaiAUpKiQqVE0Dxbpb2SbAUL6gGqeD5XmAmevJdpBcO6C88BSuVEr4SR5a +vOINhDQg/+E6MHckR2oHKtvIDY9jhp7UsP6idjWDakl4Yc2RGsq+PPIW -----END CERTIFICATE----- bioklubben.sf.se:443 diff --git bankdroid-legacy/src/main/res/raw/cert_brummer.pem bankdroid-legacy/src/main/res/raw/cert_brummer.pem index 57a3884..477acac 100644 --- bankdroid-legacy/src/main/res/raw/cert_brummer.pem +++ bankdroid-legacy/src/main/res/raw/cert_brummer.pem @@ -1,43 +1,45 @@ -----BEGIN CERTIFICATE----- -MIIHXTCCBkWgAwIBAgIQDdd0lRu1ru2NPo7vkvNo8jANBgkqhkiG9w0BAQsFADB1 +MIIH0jCCBrqgAwIBAgIQCRPnldXK9l40D0FXyRHdeDANBgkqhkiG9w0BAQsFADB1 MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk -IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE1MDIyNTAwMDAwMFoXDTE3MDMwMTEy -MDAwMFowgdgxHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB +IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE3MDIwMTAwMDAwMFoXDTE5MDQwNDEy +MDAwMFowgcsxHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB BAGCNzwCAQMTAlNFMRMwEQYDVQQFEwo1NTY2MjcyMTgyMRkwFwYDVQQJExBOb3Jy bWFsbXN0b3JnIDE0MQ8wDQYDVQQREwYxMTEgNDYxCzAJBgNVBAYTAlNFMRIwEAYD -VQQHEwlTdG9ja2hvbG0xHjAcBgNVBAoMFUJydW1tZXIgJiBQYXJ0bmVycyBBQjEL -MAkGA1UECxMCSVQxEzARBgNVBAMTCmJydW1tZXIuc2UwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQCby3aJkkQeVq9mrntbSeHpvdRC3ZbUWJfMfCO6jDzZ -rTKNa6krqeGBFOxOq8mIZBKdcSRqIp7NAoazPpSxWPi1RgMCOCLzQ84dHWPqEaDp -4DYrhuQjM0A7jgXK/2vLLARCpUCSvN8IhP3PsduSOsmrAB/EqHZcdhsCtbffJKhE -HSYYd3hv8idCmqXivHKUw6SSuKM01NYB5DWQ4O7NdUbZ/WU2hN2CK/lm0pCLm+ZU -M6bo2Lb6QtyIjrCglRX5KkADN9xylVWJCgJyCW5bpecsbxf79m/gndzMGjuUTuaR -n1TZtTV090P3lfR5Ml4BkzjkfePNPmyHJTalXgeqGOqBAgMBAAGjggODMIIDfzAf -BgNVHSMEGDAWgBQ901Cl1qCt7vNKYApl0yHU+PjWDzAdBgNVHQ4EFgQU23vqNCiJ -TVuQDBvFaTp8jThttkwwOAYDVR0RBDEwL4IKYnJ1bW1lci5zZYIOd3d3LmJydW1t -ZXIuc2WCEW9ubGluZS5icnVtbWVyLnNlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE -FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdQYDVR0fBG4wbDA0oDKgMIYuaHR0cDov -L2NybDMuZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVyLWcxLmNybDA0oDKgMIYu -aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVyLWcxLmNybDBC -BgNVHSAEOzA5MDcGCWCGSAGG/WwCATAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3 -dy5kaWdpY2VydC5jb20vQ1BTMIGIBggrBgEFBQcBAQR8MHowJAYIKwYBBQUHMAGG -GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBSBggrBgEFBQcwAoZGaHR0cDovL2Nh -Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkV4dGVuZGVkVmFsaWRhdGlv -blNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAAMIIBfgYKKwYBBAHWeQIEAgSCAW4E -ggFqAWgAdgCkuQmQtBhYFIe7E6LMZ3AKPDWYBPkb37jjd80OyA3cEAAAAUvBVKCt -AAAEAwBHMEUCIQD1EQomVM2Juf2/Fc84PgFtIcZzl+X5RNnnWE2o3jHKQgIgG1Hq -34EWDsFfwbu642otUsq4zuJgAwgBWaEkG2mXmHgAdgBo9pj4H2SCvjqM7rkoHUz8 -cVFdZ5PURNEKZ6y7T0/7xAAAAUvBVKBdAAAEAwBHMEUCIDpYzInLjWozqXvrwgbE -193pSUL08wPC0sR6NFFSDP/eAiEAoZJraDVOtt+Ii4s7HZQB9jEdYfneKI+scvY8 -ZtAe5rcAdgBWFAaaL9fC7NP14b1Esj7HRna5vJkRXMDvlJhV1onQ3QAAAUvBVKI/ -AAAEAwBHMEUCIQC9rtsHCafedt7mvkUvWxWZm+UYSWoDH6kc62gukOEAUQIgRbeF -cxBLg9PA5D/zY27VtZsR/Hpf1pnR3gsRFzFDkBAwDQYJKoZIhvcNAQELBQADggEB -AAXdbIn/XCTUhjUp0z6+6rvA6lwTptGbxUSoZbRv9XBP3yc7NpewKDmojTRZH1rF -xYNrvCVAxNXlbmE3z+nRPydM2cTL9ijM140XhoCEp7eeppqlztGv039s3WNtAxar -E8oLA8zI9nersr4r7aZWdwYTFZd5U2pwG30HVzM4qoFp3J6lmTQaMrO9qiCbfh+z -obRiPEmDgdUJD7wy2cOIVyeST6nbO60BYS8jP1nW/t1ljq9CL0AxLUgSgWFps62O -mS1MVeANDCE+htres7XH8DCAn+zQ+qX2Cow5eAluki9cl0m9IzLet2og4blKFyO9 -U20t5htcdjfC2PmbN/Y6FxQ= +VQQHEwlTdG9ja2hvbG0xHjAcBgNVBAoMFUJydW1tZXIgJiBQYXJ0bmVycyBBQjET +MBEGA1UEAxMKYnJ1bW1lci5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALdYobK2du+PQct99KfEdqZ1LoCjJFs6YP0h91SBXtHZn7K6E3qkNW580DWM +6R8DSXkcpjnjZygJCtApC3A9q/7ERoGtAAJa1la5jy7RFwg5r/D3cZVur+A7W+DX +muegiHWQ8GfTPflGdllm3URfamHvAWgsZf8Se3pfusTY4ASsFrnOY/s+5Ip/lFHT +WWF9bo3arY7zpK5aWKma5tDfDFHLF7uVp4XcAXNNDWd3pJlwMEOsrXQHG+GDN++D +iJ5z1T6uvdBh7pd8YNGQ7/zbEcFeLYpuMA8KkRPrP+ujF7pjgDgyaoa3Rid8pSug +xUpeCCjAUissK3iAGzei6jmK0DECAwEAAaOCBAUwggQBMB8GA1UdIwQYMBaAFD3T +UKXWoK3u80pgCmXTIdT4+NYPMB0GA1UdDgQWBBRgAcs5Wlfij1QTyZJv/ht7klMU +6jA4BgNVHREEMTAvggpicnVtbWVyLnNlgg53d3cuYnJ1bW1lci5zZYIRb25saW5l +LmJydW1tZXIuc2UwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMB +BggrBgEFBQcDAjB1BgNVHR8EbjBsMDSgMqAwhi5odHRwOi8vY3JsMy5kaWdpY2Vy +dC5jb20vc2hhMi1ldi1zZXJ2ZXItZzEuY3JsMDSgMqAwhi5odHRwOi8vY3JsNC5k +aWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzEuY3JsMEsGA1UdIAREMEIwNwYJ +YIZIAYb9bAIBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNv +bS9DUFMwBwYFZ4EMAQEwgYgGCCsGAQUFBwEBBHwwejAkBggrBgEFBQcwAYYYaHR0 +cDovL29jc3AuZGlnaWNlcnQuY29tMFIGCCsGAQUFBzAChkZodHRwOi8vY2FjZXJ0 +cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyRXh0ZW5kZWRWYWxpZGF0aW9uU2Vy +dmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAwggH3BgorBgEEAdZ5AgQCBIIB5wSCAeMB +4QB2AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAABWfq1SfMAAAQD +AEcwRQIhAO4WwezInv6io+B0UEpi73GMvYHOQM7F0GWZFK3UQCe8AiAslpzMJdcw +Rj0mTM/bL/j/ywOpO6BCFIEW2pYspunW0AB3AFYUBpov18Ls0/XhvUSyPsdGdrm8 +mRFcwO+UmFXWidDdAAABWfq1SGgAAAQDAEgwRgIhAMS0a56iGdE8z/NefoB2jIo4 +ZDOyU2/4nG7PQqJGdhHKAiEA+lLD8gBDu/bQwJ1sjOXda58kYnIhdHBc4LsxdT51 +GYYAdwDuS723dc5guuFCaR+r4Z5mow9+X7By2IMAxHuJeqj9ywAAAVn6tUfpAAAE +AwBIMEYCIQCpDHuNS8fHcFuKolxzwYlPAm2bc+03MPdKuomoemb/kQIhAJAeZf1o +M5PqrSA1OAJHIZXiLc0QNKPzYBG+LkaQIgd/AHUAu9nfvB+KcbWTlCOXqpJ7RzhX +lQqrUugakJZkNo4e0YUAAAFZ+rVJ3QAABAMARjBEAiAfH0NqXD4oPCsjeHIMtXoE +P0JtFgT2ttt1CzeAgj79fQIgI4bv4GxKzRcODXk8WX5lccQMk72iOhSns7hVBMqD +tDswDQYJKoZIhvcNAQELBQADggEBABZOVNT14RPE/1LSPqz3h1WnxSOC3+YjYOle +KeNeMjBnzSk56OzwMBCFDx+82fUy0Mg5Ocue4uSJsIkeH9+t0g7bLFRo2wO/+MF6 +1VeCsGuB1PWH5aYFnLKOnUZgZh2gOk7Lv3XpmdIoRQx0SqKN29iehkjDxmM7lgKS +mTkHw3ZOnbo5JKBHurGdmNMKcAgk3/Cg5sjaYGpMNacJBT2Uj4SYF07JScvJOi5U +J9nw55LUapUbPLtN8sFnxX4GCKJD0nUiQGSqRpMZlmCIAjCyKNFB1kYkfqwULU8/ +Hzn3KhA/seiw6RHEE6KH4ABCGcB+f+/wTWl9EpBBxaFQvsMmDTY= -----END CERTIFICATE----- www.brummer.se:443 diff --git bankdroid-legacy/src/main/res/raw/cert_minpension.pem bankdroid-legacy/src/main/res/raw/cert_minpension.pem index a9db360..46bf04b 100644 --- bankdroid-legacy/src/main/res/raw/cert_minpension.pem +++ bankdroid-legacy/src/main/res/raw/cert_minpension.pem @@ -1,29 +1,46 @@ -----BEGIN CERTIFICATE----- -MIIEyjCCA7KgAwIBAgIQb/AHZMl0UKpzpPlDyW4CfTANBgkqhkiG9w0BAQsFADBB +MIIH3TCCBsWgAwIBAgIQSxRugYizx9lBU/JAeD4aIzANBgkqhkiG9w0BAQsFADBB MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3RlLCBJbmMuMRswGQYDVQQDExJ0 -aGF3dGUgU1NMIENBIC0gRzIwHhcNMTQxMTA0MDAwMDAwWhcNMTcwMTMxMjM1OTU5 -WjCBhzELMAkGA1UEBhMCU0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJ -U3RvY2tob2xtMSEwHwYDVQQKDBhNaW4gUGVuc2lvbiBpIFN2ZXJpZ2UgQUIxEzAR -BgNVBAsMCk9wZXJhdGlvbnMxGDAWBgNVBAMMDyoubWlucGVuc2lvbi5zZTCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALVire6DFpjrGx69C4wZeC3IlWXS -dXkGyHwEQQWdK+szlK2Wz5O9fVC7y00WCAyFEpUE7rmHb9v/ZWJtJEtoc+INeAIB -fmpAWryfu8pkux7kz2KgWombA8WxSHOrwfpHf2Osbx0AYBVumCoHrARGa43wysYC -U9nDTblwZThcktd8LSOtgHY/MoZgpjOURqB2miY6NvM7YQ6dIjXMeRQ3hfJBYON3 -S0m2CmI5S8ovXkkE+WTzoIkBigSYu+gDdwJLPGAvKfrzy8dEI+NtvtfCSH22TxKH -NcLRZTaTWEQeJN4037nx4Yahqlw1lKLcCiWw3KohnVmUTnIP0mJtVoyFpb0CAwEA -AaOCAXUwggFxMBoGA1UdEQQTMBGCDyoubWlucGVuc2lvbi5zZTAJBgNVHRMEAjAA -MHIGA1UdIARrMGkwZwYKYIZIAYb4RQEHNjBZMCYGCCsGAQUFBwIBFhpodHRwczov -L3d3dy50aGF3dGUuY29tL2NwczAvBggrBgEFBQcCAjAjDCFodHRwczovL3d3dy50 -aGF3dGUuY29tL3JlcG9zaXRvcnkwDgYDVR0PAQH/BAQDAgWgMB8GA1UdIwQYMBaA -FMJPSFf80U+awF04fQ4F29kutVJgMCsGA1UdHwQkMCIwIKAeoByGGmh0dHA6Ly90 -ai5zeW1jYi5jb20vdGouY3JsMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD -AjBXBggrBgEFBQcBAQRLMEkwHwYIKwYBBQUHMAGGE2h0dHA6Ly90ai5zeW1jZC5j -b20wJgYIKwYBBQUHMAKGGmh0dHA6Ly90ai5zeW1jYi5jb20vdGouY3J0MA0GCSqG -SIb3DQEBCwUAA4IBAQA00XOwH0K4d9rlMqYm7fpsl74khsHCR8XKjheQoY9kevnT -ClkekuiXDIYHjBIehZ9CV9J6fLlsVyRm7ppkLLwS1WUOw6/4RuX4uqVvu6387mnI -kbHFbrLPu1puamM1ADYJS3DWrPE4aQGbQ9mvU9RPjvweGkgMKsgT3lvCUBXHyOZZ -53JyzGtVgctEu5HBFkabtc194Vj0GQIgKbNBO4a3zqR6bvXRNuwf77OUkrFal3Nc -gQigJu2fjgGwQZUb1cJSMjScfcpjiP2E41zqcncon76AAN6ZKfHmGayYX1LV3QTP -Sg4ppKz/AHPCyN4SCh4pB4fVlr3XtldcQ2d1TTrN +aGF3dGUgU1NMIENBIC0gRzIwHhcNMTYxMjA4MDAwMDAwWhcNMjAwMTMxMjM1OTU5 +WjCBlTELMAkGA1UEBhMCU0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJ +U3RvY2tob2xtMSEwHwYDVQQKDBhNaW4gUGVuc2lvbiBpIFN2ZXJpZ2UgQUIxITAf +BgNVBAsMGE1pbiBQZW5zaW9uIGkgU3ZlcmlnZSBBQjEYMBYGA1UEAwwPKi5taW5w +ZW5zaW9uLnNlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwpwIicRL +TffXO9heGZoBxtyoRlq3QPq15RS75zdyr5tUNjyPFwqSny2UxWCSxIjEvYSyI7Iu +UXzsu4MeePIgIdPHhZG+JaG0w8AS1ZFc1VAq/ULT0naHYVrE8xCQy4MDtFCxlDLN +cSbsENZglVHIILoWhwh/xba6RrILk+tW5elQYM3TQxf3BT6xLvYYAkHGmWk95JFB +MrL6uvTufs3bdwffUeYFwy1sXyiizbjr9F9MSx5u0FnelYrF7q39diJZKWTGAvK3 +uEuAgKh4cpC80ixGr/zwGdsyvRBCoQB9w5gUWVEJSinLKoZjYS7zZyzyApzEf7fV +HHPFXpuSzfL150sNf6ge4E4ZZhBPrRcDg7GU6ZjbonGgi7hARpKyU+fiPKXG908W +P5cjmFomkz6rCVOeK0JEmVrxAlpAomTuTBMhEmjeqP19j3CGYauGHAAe8C6lERZ1 +pvTXUS7+vioy1JYuEJVCWVE5DPp1yA6iBmuPG+DxvlapSJ1uD2cW4XhUflQjOwfA +xBhhEhdY49XIH0iaa+ygfoKtkbPITxgYlk1EKu1yDgjPJ/DSO+4qQdsxwUL1PvfY +MZZi7/MRyzQ7gX1MOlW1CG0m+UW6ZRLMdZ8WdNVN+gytVFoHZyWVp8Bt48GI+6DA +OOvG2ohHVxlVpawlEJ5xd1ys2EwclVH4UM8CAwEAAaOCA3owggN2MCkGA1UdEQQi +MCCCDyoubWlucGVuc2lvbi5zZYINbWlucGVuc2lvbi5zZTAJBgNVHRMEAjAAMG4G +A1UdIARnMGUwYwYGZ4EMAQICMFkwJgYIKwYBBQUHAgEWGmh0dHBzOi8vd3d3LnRo +YXd0ZS5jb20vY3BzMC8GCCsGAQUFBwICMCMMIWh0dHBzOi8vd3d3LnRoYXd0ZS5j +b20vcmVwb3NpdG9yeTAOBgNVHQ8BAf8EBAMCBaAwHwYDVR0jBBgwFoAUwk9IV/zR +T5rAXTh9DgXb2S61UmAwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL3RqLnN5bWNi +LmNvbS90ai5jcmwwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMFcGCCsG +AQUFBwEBBEswSTAfBggrBgEFBQcwAYYTaHR0cDovL3RqLnN5bWNkLmNvbTAmBggr +BgEFBQcwAoYaaHR0cDovL3RqLnN5bWNiLmNvbS90ai5jcnQwggH2BgorBgEEAdZ5 +AgQCBIIB5gSCAeIB4AB2AN3rHSt6DU+mIIuBrYFocH4ujp0B1VyIjT0RxM227L7M +AAABWN53LHkAAAQDAEcwRQIhANKbz22YATzbyv2DsuBOONOQlrSyfr7ZwkcrVQVx +Ko/OAiBffkNpsiqNgvw3Mua5giyk7KRIZzmj/RuA6pIPs5d8nAB3AO5Lvbd1zmC6 +4UJpH6vhnmajD35fsHLYgwDEe4l6qP3LAAABWN53LMQAAAQDAEgwRgIhAKu7WS3z +4RlEKvfBEEkXaU9ZsWfuWOERiBcgaXuGOI4HAiEAq34zpW7ww+W3Ka1gkn20RhyI +tB4A8agZoRPO4S5SJx8AdgC8eOHfxfY8aEZJM02hD6FfCXlpIAnAgbTz9pF/Ptm4 +pQAAAVjedy1wAAAEAwBHMEUCIH/iku72aTt3OjeL6QZepzX29fwae6AIEuW7Xdvu +SkfKAiEAyQdE96NxQODnytn2FsMDLFbVHpjR/r9Ux3/eXc/2Q9QAdQCkuQmQtBhY +FIe7E6LMZ3AKPDWYBPkb37jjd80OyA3cEAAAAVjedyyXAAAEAwBGMEQCIDz9ITBk +5QE0Cokr1xotGVZbu7tzKmtEZVzEybbgb2KWAiBVgdBjh2mY4bIvjCDn0sOyeYiV +8tOU0MHtw7IUoRcqjzANBgkqhkiG9w0BAQsFAAOCAQEAo6HA2EVB3j6KI56nKdUY +sgKquCOylrAjPH3Ov9+E6Z71vCrpVBkB24oy3lhO6F3194HwQR8PsuMSBY35etm5 +3E/Lg9GtvRydyKzO3IE3oP0JTKC2qjBS8GHhTZe+fCOlOUmBvTRV5efv0asSBr3f +eplRu9iuJWXtlH4eTSLtJPI3d7Cy7rSe8pYcFItNAKP3cBt28Od3Q4i+6cFN5YNu +xGq3V+6RIQLbjYtN8QOkmNZbCEFiLIT0aTKRzRS2Qsji62fQCGVrVTBFzOlenwLx +Bcf6wJ+l96odQxruyF9Lf7FYkysSVsNcygIFglaI5RzcpuGH8k6fzciuN43lEhl+ +zA== -----END CERTIFICATE----- www.minpension.se:443 commit f8153266dd485b441c979252c9372f4920e491f3 (tag: v1.9.14) Author: Mathias AÌŠhsberg Date: Fri Jan 6 14:08:16 2017 +0100 Create release v1.9.14 diff --git CHANGELOG CHANGELOG index 06dda56..1fbef9f 100644 --- CHANGELOG +++ CHANGELOG @@ -1,6 +1,6 @@ Please view this file on the master branch, on stable branches it's out of date. -Not yet released +v1.9.14 (2017-01-06) * Updated certificates for First Card, Osuuspankki and Östgötatrafiken v1.9.13 (2016-11-03) commit 3cf00343b0e0ee7fa6112e79946f10e937231322 Author: Mathias AÌŠhsberg Date: Fri Jan 6 14:06:56 2017 +0100 Update certificates diff --git bankdroid-legacy/src/main/res/raw/cert_americanexpress_global.pem bankdroid-legacy/src/main/res/raw/cert_americanexpress_global.pem index 516f2c0..3057379 100644 --- bankdroid-legacy/src/main/res/raw/cert_americanexpress_global.pem +++ bankdroid-legacy/src/main/res/raw/cert_americanexpress_global.pem @@ -1,40 +1,52 @@ -----BEGIN CERTIFICATE----- -MIIG6jCCBdKgAwIBAgIQMMievVyROVhb96XpEOBsbDANBgkqhkiG9w0BAQsFADB3 -MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAd -BgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxKDAmBgNVBAMTH1N5bWFudGVj -IENsYXNzIDMgRVYgU1NMIENBIC0gRzMwHhcNMTYwMTE5MDAwMDAwWhcNMTcwMTE5 -MjM1OTU5WjCCARQxEzARBgsrBgEEAYI3PAIBAxMCVVMxGTAXBgsrBgEEAYI3PAIB -AgwITmV3IFlvcmsxHTAbBgNVBA8TFFByaXZhdGUgT3JnYW5pemF0aW9uMQ8wDQYD -VQQFEwYxODgwNTUxCzAJBgNVBAYTAlVTMQ4wDAYDVQQRDAUxMDI4NTERMA8GA1UE -CAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMRkwFwYDVQQJDBAyMDAgVmVz -ZXkgU3RyZWV0MSEwHwYDVQQKDBhBbWVyaWNhbiBFeHByZXNzIENvbXBhbnkxDDAK -BgNVBAsMA05HSTEjMCEGA1UEAwwaZ2xvYmFsLmFtZXJpY2FuZXhwcmVzcy5jb20w -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEDvtrbN7UhMqBifnGiFe1 -SyY9H9gIk0jjJ64/Q5n4ix8ryijlduIAptwNPkLIW1F/gbqFHRFPGtk1vuH1KeAW -ywaoOUxPFiyTipbwfnfRrL3IhJ5CDBAHRXnMuJAu4KcXzGjSW/GH1jg3x8b3g2S+ -t7tIGawWhzvdgbb7IKLVWqjbwo3Q8k5zYjEj/7ZxpdL8LIR3tZIqP0stV7M8CtnI -kEieNVq7aZZkr+PTmjrYOppfNZ+J/tn9dCCcw/6+t/Dv4kqRReKI95XpROXk0Snn -9nG1cPszs16Q1SdaNsDBYPH/JWO8vhnpQXCqeYxHDEXz46jBsp3361V/zQmiI+9h -AgMBAAGjggLRMIICzTB7BgNVHREEdDBygiluZ2lvcmlnaW4taXBjMS1nbG9iYWwu -YW1lcmljYW5leHByZXNzLmNvbYIpbmdpb3JpZ2luLWlwYzItZ2xvYmFsLmFtZXJp -Y2FuZXhwcmVzcy5jb22CGmdsb2JhbC5hbWVyaWNhbmV4cHJlc3MuY29tMAkGA1Ud -EwQCMAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF -BQcDAjBmBgNVHSAEXzBdMFsGC2CGSAGG+EUBBxcGMEwwIwYIKwYBBQUHAgEWF2h0 -dHBzOi8vZC5zeW1jYi5jb20vY3BzMCUGCCsGAQUFBwICMBkaF2h0dHBzOi8vZC5z -eW1jYi5jb20vcnBhMB8GA1UdIwQYMBaAFAFZq+fdOgtZpmRj1s8gB1fVkedqMCsG -A1UdHwQkMCIwIKAeoByGGmh0dHA6Ly9zci5zeW1jYi5jb20vc3IuY3JsMFcGCCsG -AQUFBwEBBEswSTAfBggrBgEFBQcwAYYTaHR0cDovL3NyLnN5bWNkLmNvbTAmBggr -BgEFBQcwAoYaaHR0cDovL3NyLnN5bWNiLmNvbS9zci5jcnQwggEDBgorBgEEAdZ5 -AgQCBIH0BIHxAO8AdQDd6x0reg1PpiCLga2BaHB+Lo6dAdVciI09EcTNtuy+zAAA -AVJai34nAAAEAwBGMEQCICKhOESezeAvwC0y7eyyF+3Ed+j958sLKbS6L2oyxpUP -AiBjb2mS1Ea8s55NkQWmAtbkofidSGs9WhnmN0/oLx3TlQB2AKS5CZC0GFgUh7sT -osxncAo8NZgE+RvfuON3zQ7IDdwQAAABUlqLfmMAAAQDAEcwRQIgJizJc/otF/8Q -BKsSunRYeq6a6p+GGPsxTCsE4htb7tkCIQDtSUJAfAMZ0sLPNnqamtpT0UI5nooF -3zIjMKpP37LS8zANBgkqhkiG9w0BAQsFAAOCAQEAyWv9lGUhQV4XEXEoSNJTJcxX -mIO/e/rYA3T3uwEE1g8rq7Bvmgo1phBTDcNuljMb/ksUy8+qKtzdT0tDSKIYV6Gu -9N1WNd0aQrL2kpm63f19ZskMbTAMs8XZeK/DpJGQyCu+Y9prn/c/CZd/k6+2Mxoh -IIqnswSmH/jcktObwGoeu6EVEeVdjgj/mnPLrhX3APSX6zK3qbFcxE7m2ubw+b8+ -AnIUJYn77iVcKDMkqKsw70niitI0KLVvnX/EI7/gUp4B3ITY4aRSQSoJfghRdS0x -2JijABwk8FpGAJ93NaikwniTN5q2nAAMV84S6fjYIIo7mjEk0qnZ1ngZTdRZhA== +MIIJGDCCCACgAwIBAgIQBcnDhS/kTPYwT7xQkKTIiTANBgkqhkiG9w0BAQsFADB1 +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk +IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE2MTIwNjAwMDAwMFoXDTE4MTIxMTEy +MDAwMFowggEXMR0wGwYDVQQPDBRQcml2YXRlIE9yZ2FuaXphdGlvbjETMBEGCysG +AQQBgjc8AgEDEwJVUzEZMBcGCysGAQQBgjc8AgECEwhOZXcgWW9yazEPMA0GA1UE +BRMGMTg4MDU1MR4wHAYDVQQJExUzMTUxIFcuIEJlaHJlbmQgRHJpdmUxDjAMBgNV +BBETBTg1MDI3MQswCQYDVQQGEwJVUzEQMA4GA1UECBMHQXJpem9uYTEQMA4GA1UE +BxMHUGhvZW5peDEhMB8GA1UEChMYQW1lcmljYW4gRXhwcmVzcyBDb21wYW55MQww +CgYDVQQLEwNOR0kxIzAhBgNVBAMTGmdsb2JhbC5hbWVyaWNhbmV4cHJlc3MuY29t +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArm1h2NyOnMpJE69RKSLN +K3VXgzknpKXiWoW60+XnHvmCknUo1HSzKcWhUSYgDyGrziLs39QB3ygVKzkhp1Jm +dCrCCLugcyowU5ILbIoGxehsbu/WsnJdW75sgo21QxmiT7lmhkfioruXbmFSHPk3 +NmfhTTnzLLjrm4DTWhpM7QXbedyL5/r5U4usUAMTQrHffVQFI4A26lnv3uA0PeF5 +17onx8ivwKTIXUTY64utgNI7qqF1zFwMtQioReXeoHGhF7a+KDMLNKT1fY2/1t8N +NLsEcTcZl9hGhSSUx7zYDYyb6Syurz9U9IYGCP33LnMxU9wPSjuDe9FZZODawSKm +wQIDAQABo4IE/jCCBPowHwYDVR0jBBgwFoAUPdNQpdagre7zSmAKZdMh1Pj41g8w +HQYDVR0OBBYEFBqefDrKnrSSF++OIDOKR08xuuArMHsGA1UdEQR0MHKCGmdsb2Jh +bC5hbWVyaWNhbmV4cHJlc3MuY29tgiluZ2lvcmlnaW4taXBjMS1nbG9iYWwuYW1l +cmljYW5leHByZXNzLmNvbYIpbmdpb3JpZ2luLWlwYzItZ2xvYmFsLmFtZXJpY2Fu +ZXhwcmVzcy5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMB +BggrBgEFBQcDAjB1BgNVHR8EbjBsMDSgMqAwhi5odHRwOi8vY3JsMy5kaWdpY2Vy +dC5jb20vc2hhMi1ldi1zZXJ2ZXItZzEuY3JsMDSgMqAwhi5odHRwOi8vY3JsNC5k +aWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzEuY3JsMEsGA1UdIAREMEIwNwYJ +YIZIAYb9bAIBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNv +bS9DUFMwBwYFZ4EMAQEwgYgGCCsGAQUFBwEBBHwwejAkBggrBgEFBQcwAYYYaHR0 +cDovL29jc3AuZGlnaWNlcnQuY29tMFIGCCsGAQUFBzAChkZodHRwOi8vY2FjZXJ0 +cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyRXh0ZW5kZWRWYWxpZGF0aW9uU2Vy +dmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAwggKtBgorBgEEAdZ5AgQCBIICnQSCApkC +lwB1AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAABWNTrpfsAAAQD +AEYwRAIgFR/0U2pLZPoB4kVEeYMi9LqkYgFBM9hJf+A1uYT9354CIAqFZ17SAQ8V +/H+tpdQY/zIl3lTnZBEFLST6E6t+xfe+AS8ArDua7X+pZ0dXFZ5tfVdWcvnZgQCU +Hpve/+yhMTt1eC0AAAFY1OusCgAABAEBAD3psaSp1qs4oLVFAuHYX0ZwOf5x3TAl +jyQbbLF1r5/mbIHzR6LkP5r7bfltTMtMmyc+u/771shPu6sAucM06nPkJF/VhvRM +OlY6+nSTmVq4gW9a+EjcBefsRCRKa6d00rhSYTx1ZVIa87vSacHD5J+LMgcvTW6N +YRW3d8zdbVUsbeir7dDx9Diu3aR1bmrjPpt4fEU/GOXOOdy7DFpp8wYN7zaGUhuh +ThftdZIWhi0QEnZALodco2aZOTFgv2q1Uicf861+M6+QIlwg4zTQEF/rW+12Q5ob +UivWtV7R9cu83ZenZ+UBeF4t61CF7aJUaoV1yDEkrSc80SYg2l4P9/cAdQBWFAaa +L9fC7NP14b1Esj7HRna5vJkRXMDvlJhV1onQ3QAAAVjU66aeAAAEAwBGMEQCICyv +YtBEG+gUyhkDtf6MCY8LhmCQdPIEezWLlw7d6o/AAiAeuK4vWCe4ch/LCABsHh5x +ODW/8QG5EjiXjbZ08PfPJAB2AO5Lvbd1zmC64UJpH6vhnmajD35fsHLYgwDEe4l6 +qP3LAAABWNTrqAkAAAQDAEcwRQIgEKsq+0O+H+50tY0s9A/2owkSY6diBkYTt2ZC ++sNP8JYCIQDIchquB/qmN1UHn8n3DU0JJ8Zn8XqyVeTT6/kTAx7MCjANBgkqhkiG +9w0BAQsFAAOCAQEAv5feWkUjogtKQgC5g2ORTcFgM+FjfXtHN7omZ2pjSXI2xVND +VhTa0sbleHmB09vwY9v9orp68jxEvWM9FIpdRRiVmv4eJLGyBBaICxp9bePMK82R +pMObMN9tBAMHd55rnllWE0rME9dB8WvoOkhY7A1BBVY5m86s0H3puOXStiJpAG1j +JbSeJ4MoGpqbOryiOs/HcLIyHQOpNkykd2BuxxNW/qWQVFhPNvaVgeYy6lXunAVk +CcNfyrTOKwj0D3JkXUzwYLxSRAEVHNjIxsJT5mJFnPuLd1Te2EDkNuoceYIA+OoE +jTe/+O2Jm+Nw3PkYwmjtsc0EhaHp7HcPgHZPtQ== -----END CERTIFICATE----- global.americanexpress.com:443 diff --git bankdroid-legacy/src/main/res/raw/cert_plusgirot.pem bankdroid-legacy/src/main/res/raw/cert_plusgirot.pem index dab3079..240e7e3 100644 --- bankdroid-legacy/src/main/res/raw/cert_plusgirot.pem +++ bankdroid-legacy/src/main/res/raw/cert_plusgirot.pem @@ -1,32 +1,38 @@ -----BEGIN CERTIFICATE----- -MIIFRzCCBC+gAwIBAgIQN6N70S/YZJ1IsI3RimJCNzANBgkqhkiG9w0BAQUFADCB -vDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL -ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2Ug -YXQgaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYSAoYykxMDE2MDQGA1UEAxMt -VmVyaVNpZ24gQ2xhc3MgMyBJbnRlcm5hdGlvbmFsIFNlcnZlciBDQSAtIEczMB4X -DTE1MTExNzAwMDAwMFoXDTE2MTIzMTIzNTk1OVowgYUxCzAJBgNVBAYTAlNFMRIw -EAYDVQQIEwlTdG9ja2hvbG0xEjAQBgNVBAcUCVN0b2NraG9sbTEXMBUGA1UEChQO -Tm9yZGVhIEJhbmsgQUIxEjAQBgNVBAsUCVBsdXNnaXJvdDEhMB8GA1UEAxQYa29u -dG91dGRyYWcucGx1c2dpcm90LnNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEAt4V4w7RVKJ8ZNTZzhpe04u5MuknawqYN2q8OA2d23kvKor2YVIuTGvNz -Jo098s+gqlqINUwAU7At4nn9z4+4JSJ4+tqK/xZVjLvzC9Y0enVXfvsmaOy9jp+o -A5riJf5378ta+QHjLwU2m9kglEE7FiXJ7gNV8TaTpVTmKDvDCIrtG1pQPMNE4zAs -EWtDSAjwe68Mkl2ZKbcqa+k+LfIy/Yyhi65RJVtRN9o99bq+ZrBoLZ6eFX4Tu9Tk -zlMj5YN370Hz0tT7VuezEXLn70rJMPzxEfgwox/PYMccStviIc0++3tkgP3rAgjr -tyCPL4lknsx+Ki8hgvIqz6T+jWB2HQIDAQABo4IBeDCCAXQwIwYDVR0RBBwwGoIY -a29udG91dGRyYWcucGx1c2dpcm90LnNlMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQD -AgWgMCgGA1UdJQQhMB8GCCsGAQUFBwMBBggrBgEFBQcDAgYJYIZIAYb4QgQBMGEG -A1UdIARaMFgwVgYGZ4EMAQICMEwwIwYIKwYBBQUHAgEWF2h0dHBzOi8vZC5zeW1j -Yi5jb20vY3BzMCUGCCsGAQUFBwICMBkaF2h0dHBzOi8vZC5zeW1jYi5jb20vcnBh -MB8GA1UdIwQYMBaAFNebfNgioBX33a1fzimbWMO8RgC1MCsGA1UdHwQkMCIwIKAe -oByGGmh0dHA6Ly9zZS5zeW1jYi5jb20vc2UuY3JsMFcGCCsGAQUFBwEBBEswSTAf -BggrBgEFBQcwAYYTaHR0cDovL3NlLnN5bWNkLmNvbTAmBggrBgEFBQcwAoYaaHR0 -cDovL3NlLnN5bWNiLmNvbS9zZS5jcnQwDQYJKoZIhvcNAQEFBQADggEBACqvFwm8 -74fOxapTWBPS5SdSzIoyDTlsHPFbMFvyRxoK8fbres3K+pCo29q7wnYVLnxVh62L -v3NJMCt1Z25bIFpNR3eCAGakJhEHc0ZS76Wl04pcsFbv64na8rFTNWL2hCJVWZDK -tHv1zRQzC5fTe/XlseXKQ7zSPGZfo4Y5LHNWa3IpmQB3XPIEEinYGAXxWa7sN4SF -4XUP5akudcIdP/Gqqj4H24gC4uq6FlAij+cKbcktxmmDYqhklkAKlqekcJX/iZIh -Hm5AeElVAxdQyy+vC4pwPDZ8M+LSZD8cVmMgvEQpzprhRWQy+JYBwgcYCataFoff -hA6n1bvE4ifRPUE= +MIIGfjCCBWagAwIBAgIQbo/Txp+O1DUri0MJuySE2TANBgkqhkiG9w0BAQsFADB+ +MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAd +BgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxLzAtBgNVBAMTJlN5bWFudGVj +IENsYXNzIDMgU2VjdXJlIFNlcnZlciBDQSAtIEc0MB4XDTE2MTAxNzAwMDAwMFoX +DTE4MDEwMTIzNTk1OVowgYUxCzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hv +bG0xEjAQBgNVBAcMCVN0b2NraG9sbTEXMBUGA1UECgwOTm9yZGVhIEJhbmsgQUIx +EjAQBgNVBAsMCVBsdXNnaXJvdDEhMB8GA1UEAwwYa29udG91dGRyYWcucGx1c2dp +cm90LnNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt4V4w7RVKJ8Z +NTZzhpe04u5MuknawqYN2q8OA2d23kvKor2YVIuTGvNzJo098s+gqlqINUwAU7At +4nn9z4+4JSJ4+tqK/xZVjLvzC9Y0enVXfvsmaOy9jp+oA5riJf5378ta+QHjLwU2 +m9kglEE7FiXJ7gNV8TaTpVTmKDvDCIrtG1pQPMNE4zAsEWtDSAjwe68Mkl2ZKbcq +a+k+LfIy/Yyhi65RJVtRN9o99bq+ZrBoLZ6eFX4Tu9TkzlMj5YN370Hz0tT7Vuez +EXLn70rJMPzxEfgwox/PYMccStviIc0++3tkgP3rAgjrtyCPL4lknsx+Ki8hgvIq +z6T+jWB2HQIDAQABo4IC7jCCAuowIwYDVR0RBBwwGoIYa29udG91dGRyYWcucGx1 +c2dpcm90LnNlMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjBhBgNVHSAEWjBYMFYGBmeBDAECAjBMMCMGCCsG +AQUFBwIBFhdodHRwczovL2Quc3ltY2IuY29tL2NwczAlBggrBgEFBQcCAjAZDBdo +dHRwczovL2Quc3ltY2IuY29tL3JwYTAfBgNVHSMEGDAWgBRfYM9hkFXfhEMUimAq +svV69EMY7zArBgNVHR8EJDAiMCCgHqAchhpodHRwOi8vc3Muc3ltY2IuY29tL3Nz +LmNybDBXBggrBgEFBQcBAQRLMEkwHwYIKwYBBQUHMAGGE2h0dHA6Ly9zcy5zeW1j +ZC5jb20wJgYIKwYBBQUHMAKGGmh0dHA6Ly9zcy5zeW1jYi5jb20vc3MuY3J0MIIB +fQYKKwYBBAHWeQIEAgSCAW0EggFpAWcAdgDd6x0reg1PpiCLga2BaHB+Lo6dAdVc +iI09EcTNtuy+zAAAAVfSdPmrAAAEAwBHMEUCIQDSRYkISpRrL2N/NzY1ngBn6KRP +4sx65PoH0HSMPqlk5wIgIedk8JhBZweilNtXUjPNlaiu4NuDqBnJho3s+8f88IUA +dgBo9pj4H2SCvjqM7rkoHUz8cVFdZ5PURNEKZ6y7T0/7xAAAAVfSdPnPAAAEAwBH +MEUCIQCUTIT2u3vxVEPTD3Gpp1WD8qhnvTlbxojEI1dkOyZcggIgP7N2EnIFgD0n +K2XhfHUyShXUbwjjhVz5+mlQxRTI58AAdQDuS723dc5guuFCaR+r4Z5mow9+X7By +2IMAxHuJeqj9ywAAAVfSdPoNAAAEAwBGMEQCIBm+P4i7KuRUws/IppdxW0gAyzTj +cCN3pyEYnu5A8Zm9AiAmJAwRz9Yd69pps3ulXTfLp4YZUHg63yv7lvJPwuqrczAN +BgkqhkiG9w0BAQsFAAOCAQEABJ6GLvONcxtkEazxo4EBKDagkHkri5dmP9m6LSgD +OljqxfuYyXX6d2I6ZkA9TdtFwdInPuCI9L5clHKE7i38SMSfTiLgiz6978jeYZso +9PyHvs2rx6W8hn6wvCchIHt189ddBXF4BXAsL/KmaMjfDzm8ANNvVXqrUZ4ZKuvj +UvFerDUmeJEPN88pIHmCLf0xCj2A1OSskFYLiPRuSPBycUU6m5eC7Blt2d9M6u1f +BAi8cOyC6jv224QZl4QuuoxHzncJwP+DbbRkXovIzn4iFkY8SjbJLMGeM+hf/46X +b7/H85qGm2J9Bge974C59pUB+NiDLZIhEK9SazvkSsDEiw== -----END CERTIFICATE----- kontoutdrag.plusgirot.se:443 commit 4583f2cfb0dc5eba0b93944b8c43d2d5a1b73713 Merge: b3ca271 93a3464 Author: Mathias Ã…hsberg Date: Fri Jan 6 14:05:01 2017 +0100 Merge pull request #679 from robho/certificate_update Updated certificates for First Card, Osuuspankki and Östgötatrafiken commit 93a3464e2d3ab1d6cae747a44df3c807f4426a58 Author: Robert Högberg Date: Wed Dec 28 18:00:06 2016 +0100 Updated certificates for First Card, Osuuspankki and Östgötatrafiken diff --git CHANGELOG CHANGELOG index 91dd35a..06dda56 100644 --- CHANGELOG +++ CHANGELOG @@ -1,5 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. +Not yet released +* Updated certificates for First Card, Osuuspankki and Östgötatrafiken + v1.9.13 (2016-11-03) * Fixes Crashlytics logging issue diff --git bankdroid-legacy/src/main/res/raw/cert_firstcard.pem bankdroid-legacy/src/main/res/raw/cert_firstcard.pem index 179433a..75ced50 100644 --- bankdroid-legacy/src/main/res/raw/cert_firstcard.pem +++ bankdroid-legacy/src/main/res/raw/cert_firstcard.pem @@ -1,9 +1,9 @@ -----BEGIN CERTIFICATE----- -MIIF/jCCBOagAwIBAgIQbq6gqE0Gd/rc9lIP11FmXjANBgkqhkiG9w0BAQsFADB+ +MIIGdzCCBV+gAwIBAgIQFoz2cyoH3choP3fYGiPTlTANBgkqhkiG9w0BAQsFADB+ MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAd BgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxLzAtBgNVBAMTJlN5bWFudGVj -IENsYXNzIDMgU2VjdXJlIFNlcnZlciBDQSAtIEc0MB4XDTE2MDYxNTAwMDAwMFoX -DTE2MTIzMTIzNTk1OVowgYUxCzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hv +IENsYXNzIDMgU2VjdXJlIFNlcnZlciBDQSAtIEc0MB4XDTE2MTAxNDAwMDAwMFoX +DTE4MDEwMTIzNTk1OVowgYUxCzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hv bG0xEjAQBgNVBAcMCVN0b2NraG9sbTEXMBUGA1UECgwOTm9yZGVhIEJhbmsgQUIx GjAYBgNVBAsMEUlUIFJldGFpbCBCYW5raW5nMRkwFwYDVQQDDBB3d3cuZmlyc3Rj YXJkLnNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4dG7pvu0mER1 @@ -12,25 +12,27 @@ fGHY/PWIfVTl0ixk3pbriKlay9paA/dTdFVKNOZzihPmjeh29bjoYkIRUPfpSfF3 CDcKlWVyxH9/pR9RmcNtkm5NWDBZT26wGtyqzuwmyPunxQd6PyI9X2SUMZBMsZbd /rE/1UEdRsgH1L0OmCe35OFIxoYVpNx7BbgVUSAJZc24Oi2AHrzKt5i7Wg2z2qqp 6OPkZkJaH2y+lpztdSQAyFiyj1ai/V9CyW267uprA0vON/8zecFFvToIutFLZW9Z -m0MOJzQUzwIDAQABo4ICbjCCAmowGwYDVR0RBBQwEoIQd3d3LmZpcnN0Y2FyZC5z +m0MOJzQUzwIDAQABo4IC5zCCAuMwGwYDVR0RBBQwEoIQd3d3LmZpcnN0Y2FyZC5z ZTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD AQYIKwYBBQUHAwIwYQYDVR0gBFowWDBWBgZngQwBAgIwTDAjBggrBgEFBQcCARYX aHR0cHM6Ly9kLnN5bWNiLmNvbS9jcHMwJQYIKwYBBQUHAgIwGQwXaHR0cHM6Ly9k LnN5bWNiLmNvbS9ycGEwHwYDVR0jBBgwFoAUX2DPYZBV34RDFIpgKrL1evRDGO8w KwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL3NzLnN5bWNiLmNvbS9zcy5jcmwwVwYI KwYBBQUHAQEESzBJMB8GCCsGAQUFBzABhhNodHRwOi8vc3Muc3ltY2QuY29tMCYG -CCsGAQUFBzAChhpodHRwOi8vc3Muc3ltY2IuY29tL3NzLmNydDCCAQUGCisGAQQB -1nkCBAIEgfYEgfMA8QB2AN3rHSt6DU+mIIuBrYFocH4ujp0B1VyIjT0RxM227L7M -AAABVVRwcbsAAAQDAEcwRQIhAM9elVj2e3BQwsGa7pXEVa5U1hPodisxEThcyD5B -4sxVAiAqRSuOp2FaTDH4DO/K6/dwStGlk2Arvetyz/b7Ov+bxwB3AKS5CZC0GFgU -h7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAABVVRwcdcAAAQDAEgwRgIhAIxmtvOv -FUbYUl7VnxSO4MnHaqlLbDCd0/xanWgaYw31AiEAzFm+NDhREk9r3z0MGMoG0Z7q -lD7i6Zx5aBaH8zc8r2UwDQYJKoZIhvcNAQELBQADggEBAGkpiH0ggJGv5POZyxuj -dumT4KiZl/4eqMjhv1DrWgS525ACi4aODDcmqSPMIu6Hvg3C8p4uaccL/7hPr5bF -gvjWG7DCPKma2QyHMOMKkKowvojWLwhE+WcnZVO+o3C/Se5ua781gcSDiegnjaE5 -XJpAuBir0gGXHcFosQ5OEVElQRu5uCR0Fs2w0zllg6wCGu28scMI+d2mLtsY9axN -AfQUKmAxuqQXfNL3S+jKulpTUK2p4uu78jkHJGButha/aeO2w9Uyd9BDcQSko6BZ -rI/0lkjmV5LjZprLuY237rfiVUjsezZ9H3lEo+H/HfqkKM+5Pt4zQUFdcqhJretf -YE4= +CCsGAQUFBzAChhpodHRwOi8vc3Muc3ltY2IuY29tL3NzLmNydDCCAX4GCisGAQQB +1nkCBAIEggFuBIIBagFoAHYA3esdK3oNT6Ygi4GtgWhwfi6OnQHVXIiNPRHEzbbs +vswAAAFXws6hjAAABAMARzBFAiBK8CEHPs9cixQPUkaXXB9K3Ud74tf1wmBaFPri +UOEULQIhALTQaP9lQdrsAi4DKhs60g4yJNcIx3QjmJJh6a0b03W8AHYAaPaY+B9k +gr46jO65KB1M/HFRXWeT1ETRCmesu09P+8QAAAFXws6hpwAABAMARzBFAiEAnkz+ +Oc4HRUxm8RsDhGZ5b+PccYehX1BX3ur6v1g9DNUCID2eL8uVXm80F7FCchu+DG8b +/URTy0sP6YuTjbC6yZZDAHYA7ku9t3XOYLrhQmkfq+GeZqMPfl+wctiDAMR7iXqo +/csAAAFXws6jggAABAMARzBFAiB2vLVVIqH7/J3SVY4T+ilynHdApDe9qsL3wU6U +371GzwIhALmmDN5Y0d4WchYXECaRS3E5j/1qWCLffMm4w5eYVXB6MA0GCSqGSIb3 +DQEBCwUAA4IBAQAXFhS0c69Z86ZfY1CAI+byoEmFvj41A1bb4GIyw2c7qyMISRu6 +M6oYLGMBpj2OWZdCov+EfilRVtj/ThcuKuh9+VfZr2bloIsrymkXhmgE4orgfuVj +lkMm+k3jRokTjQryJLAlIQ3UWf9tmn49cSF1culPi8WrYvbSi4DRfo1p3nWY7/RG +wz0++oHsk5IK7hiGkXmvSWs/p1KJhoU0HViKci4ZcsPn9OEsDqZ9D2bmYLkhpSJG +cgXx0qd5ledeOq4G9+mRTOudv2u9QJ24VxKycGoXy6ClWbKDt40CPQCBZ515OPRB +0QnOSxH/5mBWvneOpWytZZ47OWWKPzalXlAK -----END CERTIFICATE----- www.firstcard.se:443 diff --git bankdroid-legacy/src/main/res/raw/cert_ostgotatrafiken_login.pem bankdroid-legacy/src/main/res/raw/cert_ostgotatrafiken_login.pem index 748e230..43f5e68 100644 --- bankdroid-legacy/src/main/res/raw/cert_ostgotatrafiken_login.pem +++ bankdroid-legacy/src/main/res/raw/cert_ostgotatrafiken_login.pem @@ -1,33 +1,33 @@ -----BEGIN CERTIFICATE----- -MIIFiTCCBHGgAwIBAgIRAMMg/Cmbci6ISVbk/7dAE+MwDQYJKoZIhvcNAQEFBQAw +MIIFfjCCBGagAwIBAgIRAKOKgCqyMq9biovKz3eHTo8wDQYJKoZIhvcNAQEFBQAw gY4xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMTQwMgYD -VQQDEytDT01PRE8gRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQSAy -MB4XDTE1MTIzMTAwMDAwMFoXDTE2MTIzMTIzNTk1OVowbDEhMB8GA1UECxMYRG9t -YWluIENvbnRyb2wgVmFsaWRhdGVkMSEwHwYDVQQLExhQb3NpdGl2ZVNTTCBNdWx0 -aS1Eb21haW4xJDAiBgNVBAMTG3NzbDMxMDkzOC5jbG91ZGZsYXJlc3NsLmNvbTCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALYtvcavhTumH/f9oo0UxpP5 -RFjDogfxBhfgwcWHbV9y1EUQEo4XeRze0xwHo7j+N+hmdo6+LwAZkHCUO4LXjDAX -+s3pMGHUwcy47LcZKLd4DiTDx0ke2Qo6OUt7xNrexq2nPTRuul/9NdnHS1TroW/E -+lN2hWvqREnF6Az8eARoS5+UBbHNFHfVsJSNVhjE8k6EAs8gFaw7bOTCk7a7ZE+K -W2vDA/BmDx6zcEGn27bEBsvNZXLzX2PVtxYBcbShcSfEeeOO8jEN8ey5clYRnaz2 -PoxeD7C5mMO1gcga+UMJzghz3+ACgWKezM1Uk75cls/XfkVL00mCe6p9XcCzKq8C -AwEAAaOCAgEwggH9MB8GA1UdIwQYMBaAFGx3kOtsaJn2rmFG1WmlVeCFcjBLMB0G -A1UdDgQWBBSjeVjzGrh4VXrOC1QBSDs0gJgbLjAOBgNVHQ8BAf8EBAMCBaAwDAYD -VR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwTwYDVR0g -BEgwRjA6BgsrBgEEAbIxAQICBzArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3Vy -ZS5jb21vZG8uY29tL0NQUzAIBgZngQwBAgEwUwYDVR0fBEwwSjBIoEagRIZCaHR0 -cDovL2NybC5jb21vZG9jYTQuY29tL0NPTU9ET0RvbWFpblZhbGlkYXRpb25TZWN1 -cmVTZXJ2ZXJDQTIuY3JsMIGFBggrBgEFBQcBAQR5MHcwTgYIKwYBBQUHMAKGQmh0 -dHA6Ly9jcnQuY29tb2RvY2E0LmNvbS9DT01PRE9Eb21haW5WYWxpZGF0aW9uU2Vj -dXJlU2VydmVyQ0EyLmNydDAlBggrBgEFBQcwAYYZaHR0cDovL29jc3AuY29tb2Rv -Y2E0LmNvbTBQBgNVHREESTBHghtzc2wzMTA5MzguY2xvdWRmbGFyZXNzbC5jb22C -FCoub3N0Z290YXRyYWZpa2VuLnNlghJvc3Rnb3RhdHJhZmlrZW4uc2UwDQYJKoZI -hvcNAQEFBQADggEBAHTI4niIacYjr11856WPSJWY7haBqXDnDnZxuQu+tLm0zX3+ -zFdmgiJBbEOfrTxzMW0sA7HlrjL4FPbMZCGirR17taXogn4wiyCXZ63I7hKB65Nm -8pwHiqqOHx95d7AohDdrCfYL1rAyCP6BaBWh0JQFKu9m+ZQyOF6zog7IcMeOEHJi -XuySpNVeT/DLvUbb4gWd6jaEcCp4zSZBqLLXXbIkkunJ3eTqXq8z5x+mOJpj1UV/ -AwynkIWdAJxLaJma0Rdw0Ow89uknBWh1mBtQYaSBiltw/3bnTSGhScS00ZxZ85lL -E1dVzW3GXPiOEh4LMGnnJERSVztgihJziES2RVE= +VQQDEytDT01PRE8gRG9tYWluIFZhbGlkYXRpb24gTGVnYWN5IFNlcnZlciBDQSAy +MB4XDTE2MTIxOTAwMDAwMFoXDTE3MTIxNzIzNTk1OVowazEhMB8GA1UECxMYRG9t +YWluIENvbnRyb2wgVmFsaWRhdGVkMSAwHgYDVQQLExdMZWdhY3kgTXVsdGktRG9t +YWluIFNTTDEkMCIGA1UEAxMbc3NsNTEzMjMxLmNsb3VkZmxhcmVzc2wuY29tMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoo5oiTWjySYRFPue9v7OFv27 +95/KJcUZ50tTZx+Mu0XsM/vIeEifp2+/EMq13kr26xlY4mytJ8amhZlIC1gwh3/7 +29RngE9u88UGJzhWKFmNu4yCtVZCoMkALaN4xIbVmlJVm6MWXFPDDYD2NVOA85TM +H28ACxkU5uch9a02LktuV0Evq8Uqb+ldSqDKNz9mp1whTb8WOwqDFDxlRXu6U1pR +g8YK1Wlc58hY8wgLnm5JwHnxYbHfAE5yiJMJ/d9HMxsKcooSMtP8WY42XJmcJ94B +15WJgLtZq/J7s+nvaRJ+roncNhx9kpHvUAO1E3aSrI4rT6iz6H4Iep/3b51THwID +AQABo4IB9zCCAfMwHwYDVR0jBBgwFoAUmY4ClcUeVSJ7h3CLXhwBwnbErugwHQYD +VR0OBBYEFFd9xsH12GaSTSgdudw1HKZ905dqMA4GA1UdDwEB/wQEAwIFoDAMBgNV +HRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBFBgNVHSAE +PjA8MDoGCysGAQQBsjEBAgIHMCswKQYIKwYBBQUHAgEWHWh0dHBzOi8vc2VjdXJl +LmNvbW9kby5jb20vQ1BTMFMGA1UdHwRMMEowSKBGoESGQmh0dHA6Ly9jcmwuY29t +b2RvY2E0LmNvbS9DT01PRE9Eb21haW5WYWxpZGF0aW9uTGVnYWN5U2VydmVyQ0Ey +LmNybDCBhQYIKwYBBQUHAQEEeTB3ME4GCCsGAQUFBzAChkJodHRwOi8vY3J0LmNv +bW9kb2NhNC5jb20vQ09NT0RPRG9tYWluVmFsaWRhdGlvbkxlZ2FjeVNlcnZlckNB +Mi5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLmNvbW9kb2NhNC5jb20wUAYD +VR0RBEkwR4Ibc3NsNTEzMjMxLmNsb3VkZmxhcmVzc2wuY29tghQqLm9zdGdvdGF0 +cmFmaWtlbi5zZYISb3N0Z290YXRyYWZpa2VuLnNlMA0GCSqGSIb3DQEBBQUAA4IB +AQBCyqq8Cl2smTKklqTNvjmJVz/HOOuHi/YplukWPv5Ztc6KrQ3m279ipdyQR4IK +Q42iYhp7Z1/iw0WqCaDWHiibdJYKMaEWB6nr7BV2qvDnDdAgyTNcW4ZOLakfmy4y +kGygPzt0cjlZMmSI8iTBPCD4cTkMtQ7EKn3kcRlZ5azBFqT5VXH9N3P183qz1sv6 +iSg/pLR46dLy061+egeRoElS4Glggg0pAl1fv3duIGmYd3oREK3HRGHlCZ3dA0Hv +cmJp6Qs2K1phPW3DC6EAVkDKyRUN3BiglOz3xu0eCdAVvnlUFveYcNnH8P2047x1 +S8rFrPmWlg/+TVPw8kuNxYxG -----END CERTIFICATE----- www.ostgotatrafiken.se:443 diff --git bankdroid-legacy/src/main/res/raw/cert_osuuspankki.pem bankdroid-legacy/src/main/res/raw/cert_osuuspankki.pem index 49f5f1b..d3ad09e 100644 --- bankdroid-legacy/src/main/res/raw/cert_osuuspankki.pem +++ bankdroid-legacy/src/main/res/raw/cert_osuuspankki.pem @@ -1,37 +1,37 @@ -----BEGIN CERTIFICATE----- -MIIGQjCCBSqgAwIBAgIQP5hSiLJfDuQhp1cXs/mT1DANBgkqhkiG9w0BAQsFADB3 +MIIGSzCCBTOgAwIBAgIQQq4J10cEVtza7iMNxJPZGjANBgkqhkiG9w0BAQsFADB3 MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAd BgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxKDAmBgNVBAMTH1N5bWFudGVj -IENsYXNzIDMgRVYgU1NMIENBIC0gRzMwHhcNMTUxMjExMDAwMDAwWhcNMTYxMjEx +IENsYXNzIDMgRVYgU1NMIENBIC0gRzMwHhcNMTYxMTI4MDAwMDAwWhcNMTcxMjEx MjM1OTU5WjCB0jETMBEGCysGAQQBgjc8AgEDEwJGSTEdMBsGA1UEDxMUUHJpdmF0 ZSBPcmdhbml6YXRpb24xEjAQBgNVBAUTCTAyNDI1MjItMTELMAkGA1UEBhMCRkkx DjAMBgNVBBEMBTAwNTEwMRAwDgYDVQQIDAdVdXNpbWFhMREwDwYDVQQHDAhIZWxz aW5raTEaMBgGA1UECQwRVGVvbGxpc3V1c2thdHUgMUIxFjAUBgNVBAoMDU9QIE9z dXVza3VudGExEjAQBgNVBAMMCXd3dy5vcC5maTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAOSj+joJzJTPu3fFhVs8Rr1spy+3yDfeZRVZpgPEqEywTIS5 -4mK1AfmOFjRlv/PJjFXeK8DR/atABgZrcMZHDj3SsIDTvPzRSVZXcRo5FUXJJ1zE -B7CIXjsxuCRDgbYVqiAl2NIufjooKQcqOB3lbAooLxOruHbG/Bu11h2X4NKEAyWH -IohCfGTU/qEJBi/7MjRAf5G0j271j9yKxmaEQ4746Y+SW5rTi+HTREpTZgu2zDrN -CE/znRIW6iUrqQAJQSB/lfjAk1MI6aho9LpgjtUixN3R+D+xF2XqdrrbaEby8KKn -HmQWDbScoQacqSiU0N5G/wBOlmD+VmPHic8PIQMCAwEAAaOCAmwwggJoMBQGA1Ud +ggEPADCCAQoCggEBAOtUH95ashzyEZYAvTzQ9EEijM7+styfIK/KZBJWUKM0+mod +haod+cGvMT27IDm/EZt013X4RG8RbW74K54bdHdBkwFDOpVTRLL6ZGiGkz+C/Dd4 ++iPXJwKek0Bw+BicTwIayyvOIa3NlAGaJWUVG/R9MLZGu+Tx9/LtYStXQHOmBdsy +jocNN+62OjFW/J11KW2jQTPeg/gZ7iE4S92U1jythHZp0/pSNYW2jCh8HAHj5F2d +Se8VAkW5M7X7I30RVcb2tN4c2vdjSXsedwxlvPrZ+9SNpA+vHiTcH+PhVCeRfWnD +vw96bSeS3XWSai+5dRxTiBHjkMT6VdA8HJLc2lkCAwEAAaOCAnUwggJxMBQGA1Ud EQQNMAuCCXd3dy5vcC5maTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIFoDAdBgNV -HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwZgYDVR0gBF8wXTBbBgtghkgBhvhF -AQcXBjBMMCMGCCsGAQUFBwIBFhdodHRwczovL2Quc3ltY2IuY29tL2NwczAlBggr -BgEFBQcCAjAZGhdodHRwczovL2Quc3ltY2IuY29tL3JwYTAfBgNVHSMEGDAWgBQB -Wavn3ToLWaZkY9bPIAdX1ZHnajArBgNVHR8EJDAiMCCgHqAchhpodHRwOi8vc3Iu -c3ltY2IuY29tL3NyLmNybDBXBggrBgEFBQcBAQRLMEkwHwYIKwYBBQUHMAGGE2h0 -dHA6Ly9zci5zeW1jZC5jb20wJgYIKwYBBQUHMAKGGmh0dHA6Ly9zci5zeW1jYi5j -b20vc3IuY3J0MIIBBQYKKwYBBAHWeQIEAgSB9gSB8wDxAHYA3esdK3oNT6Ygi4Gt -gWhwfi6OnQHVXIiNPRHEzbbsvswAAAFRkUQSTAAABAMARzBFAiEAriTv2d2Ao6yZ -Trh+sI47Wpwbc2U/dCEusw19L9Y3wHUCIEibTxmqb5yHSP3d9xc8bCVKA62mPoWa -5jsNvNvp39N/AHcApLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BAAAAFR -kUQSbAAABAMASDBGAiEAyxrljfrbJ7gbOM1Muaf32Qwr4KgzFO0Szbpv/MHaISIC -IQCUh2c0pM8V1p/yWUZYmXhNRGhn1AkA3xVckr1is9H2vzANBgkqhkiG9w0BAQsF -AAOCAQEAc1+J5Ex91zoB+0RJHzSFoPQ9TegFuyo0wO3M4278FYxwO2IDtcUspDvq -J/ct2W04btWGur1EuG3Y6i87dhkFiIn2o9TbEmb7QvFBB/Ak3eSyi8Y7cqSPhpl4 -o9m/7mBu6rfgJRdZCgKOE5xD/ND/dCLNzuOeYwXwvYOFv3k3yttU1I7hUVb2d46h -9clPgOA8N27gKPp01rkTjfF65xpDlmX3xufMwFa9N4C9yJD1SeuZ0EDnjrYLTAp/ -N5FLXTRm4wLIRzuvb3UoYMM7QLMxcnbXkGqT1SFKxbkupTVcQS5ZDfGNNLi1RTc2 -/aCGL94jL5MWifVSVGvNLkogJSPiiA== +HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwbwYDVR0gBGgwZjAHBgVngQwBATBb +BgtghkgBhvhFAQcXBjBMMCMGCCsGAQUFBwIBFhdodHRwczovL2Quc3ltY2IuY29t +L2NwczAlBggrBgEFBQcCAjAZDBdodHRwczovL2Quc3ltY2IuY29tL3JwYTAfBgNV +HSMEGDAWgBQBWavn3ToLWaZkY9bPIAdX1ZHnajArBgNVHR8EJDAiMCCgHqAchhpo +dHRwOi8vc3Iuc3ltY2IuY29tL3NyLmNybDBXBggrBgEFBQcBAQRLMEkwHwYIKwYB +BQUHMAGGE2h0dHA6Ly9zci5zeW1jZC5jb20wJgYIKwYBBQUHMAKGGmh0dHA6Ly9z +ci5zeW1jYi5jb20vc3IuY3J0MIIBBQYKKwYBBAHWeQIEAgSB9gSB8wDxAHcA3esd +K3oNT6Ygi4GtgWhwfi6OnQHVXIiNPRHEzbbsvswAAAFYquWeNQAABAMASDBGAiEA +m/vqbAbbDWYSuyDaeX/tL0Tgu6Yd9fTYsNS4sAPR3V8CIQDHEuKjK6b26BaEX3NM +06N2iRP/Fc6jAG902C+DGoKf1AB2AO5Lvbd1zmC64UJpH6vhnmajD35fsHLYgwDE +e4l6qP3LAAABWKrlnoQAAAQDAEcwRQIhAPfcqyFXMb4jVRim8DIE/ahDR2T6KaZ5 +/Pk4oQIf0RkvAiBjbtXK8OxVk20vce9jgblMdYaANEuX1uZ/m91qCG0i3TANBgkq +hkiG9w0BAQsFAAOCAQEASAe4g82eWmOp9ALiojwejKwQVV4LbT7vQ8smZGhptu1p +r4zUppGqXRLpaJjudgLIgZsrHygbGTPJaqw8HR7PYjMt3fOozwH/W2dVHNhjPp09 +IiBtERECYB9LXkbIC/Iqgn60OWgnaoh7mPjH6jgVGsG/KpDRyLQUx64nLIMFXqdV +06xu/JcmEoe/5Tf30kcvVAB0I5T17FvzXoAQXUn2IgjYbPomC+Gg3YB80cz6/+NR +GTBonUx/4aK3XhC5NyFYoR+U+rzEZOPwwLb8kdfg7ykSqz9OngKQl/lgpHsSmIqM +cB1X7Qc6VtmqOTNuKPcPRlA7OV4N3DYNzhC2ePxoIw== -----END CERTIFICATE----- www.op.fi:443 commit b3ca27108f9677b0a172cdd155bf3bef1952d668 Author: Mathias AÌŠhsberg Date: Mon Nov 28 17:12:45 2016 +0100 Update certs for Västtrafik and Ica diff --git bankdroid-legacy/src/main/res/raw/cert_ica.pem bankdroid-legacy/src/main/res/raw/cert_ica.pem index 36cb10d..3df2bec 100644 --- bankdroid-legacy/src/main/res/raw/cert_ica.pem +++ bankdroid-legacy/src/main/res/raw/cert_ica.pem @@ -1,31 +1,31 @@ -----BEGIN CERTIFICATE----- -MIIFKzCCBBOgAwIBAgISESHslX6V0aQ+7RZ1+hfIRWm5MA0GCSqGSIb3DQEBCwUA -MGYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTwwOgYD -VQQDEzNHbG9iYWxTaWduIE9yZ2FuaXphdGlvbiBWYWxpZGF0aW9uIENBIC0gU0hB -MjU2IC0gRzIwHhcNMTUxMTE4MDk0MTAyWhcNMTYxMTE4MDk0MTAyWjBpMQswCQYD -VQQGEwJTRTEXMBUGA1UECBMOU3RvY2tob2xtcyBsYW4xDjAMBgNVBAcTBVNvbG5h -MQswCQYDVQQLEwJJVDEPMA0GA1UEChMGSUNBIEFCMRMwEQYDVQQDEwphcGkuaWNh -LnNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvtOXFdViq2xV7p32 -XbxA27+emUxPKJ3xmhVGK6sMHGdyOSdaiyOWhUpmtl+FsgZ/svRbSMJyK9voSO+6 -faXP62aaJP5cQIlm0repD9bbcyEZ0Mqe+I1Y5sOCMjwI50dGL+gHEEyB3bEcB1ID -QRq14UvMT8JybLnOzs2xFkFDwPREzlqnEIt1MwNPudY/5/KHFOdwh937QCcW420l -PkX3Caow4Sc8vEnsQmPzdrxQuZ2wT7gMMokYSby4eAyhT9QGPc4wZxd/jaOSSI4N -1qrgCfmW0G4e9b9mI+FADK+yjwtdolT2eBlT7Zchg2NuQdtjUEeuCph4C7eCfLso -05feywIDAQABo4IBzjCCAcowDgYDVR0PAQH/BAQDAgWgMEkGA1UdIARCMEAwPgYG -Z4EMAQICMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29t -L3JlcG9zaXRvcnkvMBUGA1UdEQQOMAyCCmFwaS5pY2Euc2UwCQYDVR0TBAIwADAd -BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwSQYDVR0fBEIwQDA+oDygOoY4 -aHR0cDovL2NybC5nbG9iYWxzaWduLmNvbS9ncy9nc29yZ2FuaXphdGlvbnZhbHNo -YTJnMi5jcmwwgaAGCCsGAQUFBwEBBIGTMIGQME0GCCsGAQUFBzAChkFodHRwOi8v -c2VjdXJlLmdsb2JhbHNpZ24uY29tL2NhY2VydC9nc29yZ2FuaXphdGlvbnZhbHNo -YTJnMnIxLmNydDA/BggrBgEFBQcwAYYzaHR0cDovL29jc3AyLmdsb2JhbHNpZ24u -Y29tL2dzb3JnYW5pemF0aW9udmFsc2hhMmcyMB0GA1UdDgQWBBQTwufvdytHvYMM -eHVL2qa4w7VzbjAfBgNVHSMEGDAWgBSW3mHxvRwWKVMcwMx9O4MAQOYafDANBgkq -hkiG9w0BAQsFAAOCAQEAGHY6XgnAmrDR810WBns/S2q4cNxq5D/FkCHMbyo231ce -9LXmbNQQsvmoDJuOivSN04e50DktGQAzk5xbZ0rnjrzFd9HnllWmSh3SPFmqNK9y -IywGyVzU1UX2lHQ9etZRfnAfdKkajSeHdeBouYiYhgOKFhDkJGJk7qtRFX/PtClA -dxioVa4kVsXNQ5H17SBQWDwGTRUeUXJUzuPcycvu+D41mdvOONBxKyJUoW0qy945 -gVfwL0u/EW6jDPofNdvdLaysmGa6YAntqcMf+MUeZ92sbagdIKKiJuEXqzl6iYH+ -Be2TKW8A9Zy/ke8Z3LCOsj6BVQIeAyMuZsIcvGGsEg== +MIIFMjCCBBqgAwIBAgIMYiysILzIsNkk0aiAMA0GCSqGSIb3DQEBCwUAMGYxCzAJ +BgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTwwOgYDVQQDEzNH +bG9iYWxTaWduIE9yZ2FuaXphdGlvbiBWYWxpZGF0aW9uIENBIC0gU0hBMjU2IC0g +RzIwHhcNMTYxMTE1MDg0MTA2WhcNMTcxMTE2MDg0MTA2WjBpMQswCQYDVQQGEwJT +RTEXMBUGA1UECBMOU3RvY2tob2xtcyBsYW4xDjAMBgNVBAcTBVNvbG5hMQswCQYD +VQQLEwJJVDEPMA0GA1UEChMGSUNBIEFCMRMwEQYDVQQDEwphcGkuaWNhLnNlMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyUT84RTCZGqEcEe+8CNDDyve +ayptEmLsnLo7SC0G/LWnvn5XhrOv9TQDhFiD1ayjwXvcbZjDzoLp5dN8M/r0c6NM +6GSMo1F/mfwDmLJpp1Jx6FCL5CZ7+MYfzP0KaxGTLa9otbWy/Mfcxxx8ifvKI88h +OjvJC+g3nz0wy1AJKDfYBgEraeFb5FtdhuuhUY78zHeXEXksZpiuh+x9DeEbHpPx +vmRUDdZLOJVUwhGEa5kJyqMCqDEjwXX+gsfs+zIqIC6XW1kXRL53zF3Oroeat+qy +vc/JtyI8tEuifrUIhqOipZVmGf2asw6isWpN/G1zoIIKTMRkJRMUssyLPZCqXwID +AQABo4IB2zCCAdcwDgYDVR0PAQH/BAQDAgWgMIGgBggrBgEFBQcBAQSBkzCBkDBN +BggrBgEFBQcwAoZBaHR0cDovL3NlY3VyZS5nbG9iYWxzaWduLmNvbS9jYWNlcnQv +Z3Nvcmdhbml6YXRpb252YWxzaGEyZzJyMS5jcnQwPwYIKwYBBQUHMAGGM2h0dHA6 +Ly9vY3NwMi5nbG9iYWxzaWduLmNvbS9nc29yZ2FuaXphdGlvbnZhbHNoYTJnMjBW +BgNVHSAETzBNMEEGCSsGAQQBoDIBFDA0MDIGCCsGAQUFBwIBFiZodHRwczovL3d3 +dy5nbG9iYWxzaWduLmNvbS9yZXBvc2l0b3J5LzAIBgZngQwBAgIwCQYDVR0TBAIw +ADBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmdsb2JhbHNpZ24uY29tL2dz +L2dzb3JnYW5pemF0aW9udmFsc2hhMmcyLmNybDAVBgNVHREEDjAMggphcGkuaWNh +LnNlMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQU8zEw +//vJou9pN8JUZ+yxttNcLdIwHwYDVR0jBBgwFoAUlt5h8b0cFilTHMDMfTuDAEDm +GnwwDQYJKoZIhvcNAQELBQADggEBAAU4C0JNq9AM2mYT3b8VXY6em77Y/AbXAze0 +0TYeZgn2wtp9lSBmVUtyAqWFZIE9aKzDruRCaeRzla4zZPN5TDS8jm2KEuBp7xb3 +4u3Fb7jSmOhyyMuqXzcjFVXp3Gde2GVYAgnDsaXBfJuk63aeUU1mg6kWOh+P7Vez +84VLXofWNpdhspWXGkBc898GgLK7Ko+lJQ3LS5vn3ITTxlmD2t66jNib8R2aihwa +XPUdPdTvFuyhT1i8CIuSZAXbZiQRtQh1ooh0lWGxYnL3zGQ29i0O5h44tq+gFOTB +XP5rIJEjeETnTBZYVLKKjMP5+kUzD2+4o5jMz5ucz7Kzb2LcWYg= -----END CERTIFICATE----- api.ica.se:443 diff --git bankdroid-legacy/src/main/res/raw/cert_vasttrafik.pem bankdroid-legacy/src/main/res/raw/cert_vasttrafik.pem index ecc7ed3..1eb5a57 100644 --- bankdroid-legacy/src/main/res/raw/cert_vasttrafik.pem +++ bankdroid-legacy/src/main/res/raw/cert_vasttrafik.pem @@ -1,30 +1,41 @@ -----BEGIN CERTIFICATE----- -MIIE8zCCA9ugAwIBAgIQUeTHmHB7Xr1CudcxMmVzpjANBgkqhkiG9w0BAQsFADBE +MIIG8TCCBdmgAwIBAgIQLw3oiO5pfyS//nmbxVLjUDANBgkqhkiG9w0BAQsFADBE MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMU -R2VvVHJ1c3QgU1NMIENBIC0gRzMwHhcNMTQxMTEyMDAwMDAwWhcNMTYxMjExMjM1 -OTU5WjBvMQswCQYDVQQGEwJTRTEPMA0GA1UECAwGU3dlZGVuMRAwDgYDVQQHDAdT -S8OWVkRFMRYwFAYDVQQKDA1WYXN0dHJhZmlrIEFCMQswCQYDVQQLDAJJVDEYMBYG -A1UEAwwPKi52YXN0dHJhZmlrLnNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEApcU6TNs1zd1p0Y8ZwRagpav7ir4erVZwdjdocHXS5cTrTzgiwOeXlJRm -d/708OFRfN6bQq+s1hnULqdwB9eWXUdOd3jZwJPuSupBnChJHrCQd9X81/hmvqUK -nqvy9YBB2KZPSip4vj5C0r6gyL7FzqywHWmZ0KCyD+a0y3rj6jZHreMq2v6FpjLA -8F55pegEI21PVVQ0HEWJV2K6ATnbEVw3/u1CqA5DuRj0zWJjYU+jQfTYsXVq/mA1 -CntuNsAXVosrrolgbr+T4KqTmsDhXLAGeS/CGjQBgCNhHI9ljU8GkDH9k7N4mQDy -J23Stxpsr9G2GmcZ3HEpApgpFz9EcwIDAQABo4IBtDCCAbAwKQYDVR0RBCIwIIIP -Ki52YXN0dHJhZmlrLnNlgg12YXN0dHJhZmlrLnNlMAkGA1UdEwQCMAAwDgYDVR0P -AQH/BAQDAgWgMCsGA1UdHwQkMCIwIKAeoByGGmh0dHA6Ly9nbi5zeW1jYi5jb20v -Z24uY3JsMIGhBgNVHSAEgZkwgZYwgZMGCmCGSAGG+EUBBzYwgYQwPwYIKwYBBQUH -AgEWM2h0dHBzOi8vd3d3Lmdlb3RydXN0LmNvbS9yZXNvdXJjZXMvcmVwb3NpdG9y -eS9sZWdhbDBBBggrBgEFBQcCAjA1DDNodHRwczovL3d3dy5nZW90cnVzdC5jb20v -cmVzb3VyY2VzL3JlcG9zaXRvcnkvbGVnYWwwHQYDVR0lBBYwFAYIKwYBBQUHAwEG -CCsGAQUFBwMCMB8GA1UdIwQYMBaAFNJv95b0hT9yPDB9I9qFeJujfFp8MFcGCCsG -AQUFBwEBBEswSTAfBggrBgEFBQcwAYYTaHR0cDovL2duLnN5bWNkLmNvbTAmBggr -BgEFBQcwAoYaaHR0cDovL2duLnN5bWNiLmNvbS9nbi5jcnQwDQYJKoZIhvcNAQEL -BQADggEBANw8IArd3uMLPTObxzXxt/l+JsevXQoq2HKJeWerdW/yer8W8s5oNGmL -gdFrBhtpxIDK/8x3bY9y1DjmlFjl1ZiQAW4yqSzFjDK9Fs3nM4SPjL0DnTTBIJ1h -ZPhAof1l6iW+ft0h1/wqel8XMl2PeShsKSN3NQCYu7nuB20n+/no2KqmeFfzZzkU -MFgcV0sNj019vVne3RizP93yrvAVILRZbU4EkiaBgf5y1GNyLWneRpRGennnbDMB -oEOd427Js5wKuXAuLXyUOsfwTLB4gMD2RpIeiYnEhhN4n8nStDnbvsoq6tIYC4qz -7lu66yWdSo3BXrk0mpe3BvNNjGY5R0U= +R2VvVHJ1c3QgU1NMIENBIC0gRzMwHhcNMTYxMTI0MDAwMDAwWhcNMjAwMTE2MjM1 +OTU5WjB4MQswCQYDVQQGEwJTRTEYMBYGA1UECAwPVmFzdHJhIEdvdGFsYW5kMRAw +DgYDVQQHDAdTS8OWVkRFMRYwFAYDVQQKDA1WYXN0dHJhZmlrIEFCMQswCQYDVQQL +DAJJVDEYMBYGA1UEAwwPKi52YXN0dHJhZmlrLnNlMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEApcU6TNs1zd1p0Y8ZwRagpav7ir4erVZwdjdocHXS5cTr +TzgiwOeXlJRmd/708OFRfN6bQq+s1hnULqdwB9eWXUdOd3jZwJPuSupBnChJHrCQ +d9X81/hmvqUKnqvy9YBB2KZPSip4vj5C0r6gyL7FzqywHWmZ0KCyD+a0y3rj6jZH +reMq2v6FpjLA8F55pegEI21PVVQ0HEWJV2K6ATnbEVw3/u1CqA5DuRj0zWJjYU+j +QfTYsXVq/mA1CntuNsAXVosrrolgbr+T4KqTmsDhXLAGeS/CGjQBgCNhHI9ljU8G +kDH9k7N4mQDyJ23Stxpsr9G2GmcZ3HEpApgpFz9EcwIDAQABo4IDqTCCA6UwKQYD +VR0RBCIwIIIPKi52YXN0dHJhZmlrLnNlgg12YXN0dHJhZmlrLnNlMAkGA1UdEwQC +MAAwDgYDVR0PAQH/BAQDAgWgMCsGA1UdHwQkMCIwIKAeoByGGmh0dHA6Ly9nbi5z +eW1jYi5jb20vZ24uY3JsMIGdBgNVHSAEgZUwgZIwgY8GBmeBDAECAjCBhDA/Bggr +BgEFBQcCARYzaHR0cHM6Ly93d3cuZ2VvdHJ1c3QuY29tL3Jlc291cmNlcy9yZXBv +c2l0b3J5L2xlZ2FsMEEGCCsGAQUFBwICMDUMM2h0dHBzOi8vd3d3Lmdlb3RydXN0 +LmNvbS9yZXNvdXJjZXMvcmVwb3NpdG9yeS9sZWdhbDAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwHwYDVR0jBBgwFoAU0m/3lvSFP3I8MH0j2oV4m6N8Wnww +VwYIKwYBBQUHAQEESzBJMB8GCCsGAQUFBzABhhNodHRwOi8vZ24uc3ltY2QuY29t +MCYGCCsGAQUFBzAChhpodHRwOi8vZ24uc3ltY2IuY29tL2duLmNydDCCAfUGCisG +AQQB1nkCBAIEggHlBIIB4QHfAHcA3esdK3oNT6Ygi4GtgWhwfi6OnQHVXIiNPRHE +zbbsvswAAAFYljT1wAAABAMASDBGAiEAzUGB2nVUZmC/eiEOWFYaeOm61lHVgSzB +YMfQ2chLCOsCIQC5yme+mKFeYJlsOcZGXviu4trgbFTwQ5/+dsGwX7cVAQB1AO5L +vbd1zmC64UJpH6vhnmajD35fsHLYgwDEe4l6qP3LAAABWJY09gAAAAQDAEYwRAIg +VPvLVocxnFGLTT1lhPN3Qgbr7ps5R48r6lYeaUWChMICIAcQD2cHWNx8RKHFPGE0 +/moYHbD8rQTDDUBrMRLdGhcxAHUAvHjh38X2PGhGSTNNoQ+hXwl5aSAJwIG08/aR +fz7ZuKUAAAFYljT2tgAABAMARjBEAiAgz+64cfRsaYoEKblmyo1rG+7g5fIvODu1 +9klLI0EO+AIgDNce5RMtVyDiaMUKTvWqKh7Rn9F2/kdjyk8PPTNQCsUAdgCkuQmQ +tBhYFIe7E6LMZ3AKPDWYBPkb37jjd80OyA3cEAAAAViWNPXfAAAEAwBHMEUCICHG +6qRiiiTNKbZPNU55DKOpLn7mLsoD4MCxtyzuYz6TAiEA1kguSD4gB1Ov/zMdTytm +yXZvRAo5PTmulMZX/v1XnDowDQYJKoZIhvcNAQELBQADggEBAFIyiOvLKy/sPqOp +wYFyb/U6coPLGoCaqZG5XP5GC5wl1i2+y2U0GJ6270LHRQHtHxIkggYGiZUF2FE2 +FjFwcSDvE1jiQF6Oub/VapNdRoBDG0OtBILF5rgS0k2lpMpk6Q7yKUJbARaUTbWX +rMA/rcWwsOMCoYO3rS+KrefsZsOjt9UKPljbn/BrA+++FyK3TCwpli529+BCIkP8 +fprgHJY/DbIXCU/PSBeIdyIq6YYyg5v1ZB0/uv5pFgQ6tpXhR1Fj8+ICuctmQ/8Y +94rgEraoBiKpVL8EK8T5t7cHJLq53jT76eWHolLzQNr96NCrU9rGQaTKKF/SZ78M +3WKfVac= -----END CERTIFICATE----- www.vasttrafik.se:443 commit ef07d475822680cb39898ad433fae1be5874d475 Author: Johan Walles Date: Wed Nov 9 20:17:38 2016 +0100 Blame Urllib problems on the callers For Crashlytics. The previous attempt at doing this failed, because it tried to modify the *first* exception in the chain, but it turns out that it's the *last* exception in the chain that Crashlytics looks at. So given an Exception... " java.lang.Exception: This is a test Exception at not.bankdroid.at.all.ExceptionFactory.getException(ExceptionFactory.java:20) at com.liato.bankdroid.utils.ExceptionUtilsTest.testBlameBankdroid(ExceptionUtilsTest.java:16) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at ... " ... we now report to Crashlytics: " java.lang.Exception: This is a test Exception at not.bankdroid.at.all.ExceptionFactory.getException(ExceptionFactory.java:20) at com.liato.bankdroid.utils.ExceptionUtilsTest.testBlameBankdroid(ExceptionUtilsTest.java:16) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at ... Caused by: java.lang.Exception: This is a test Exception at com.liato.bankdroid.utils.ExceptionUtilsTest.testBlameBankdroid(ExceptionUtilsTest.java:16) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at ... ... 37 more " diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/ExceptionUtils.java bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/ExceptionUtils.java index 468b56e..91fe4d5 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/ExceptionUtils.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/ExceptionUtils.java @@ -1,6 +1,8 @@ package com.liato.bankdroid.utils; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; @@ -11,41 +13,109 @@ public class ExceptionUtils { private static final String PREFIX = "com.liato.bankdroid."; /** - * Take an exception thrown and make it look like it came from Bankdroid. + * Modify an Exception to make it look like it was ultimately caused by Bankdroid. *

- * Specifically, if Urllib.java, called by Bankdroid code, throws an exception, - * rewrite the exception so that it appears as if it was thrown from the - * Bankdroid method calling Urllib, but caused by the original Exception. + * The purpose is to make Crashlytics report Urllib exceptions as coming from whatever + * bank Urllib is trying to access. + *

+ * For example, this exception: + *

+     * java.lang.Exception: This is a test Exception
+     *     at not.bankdroid.at.all.ExceptionFactory.getException(ExceptionFactory.java:20)
+     *     at com.liato.bankdroid.utils.ExceptionUtilsTest.testBlameBankdroid(ExceptionUtilsTest.java:16)
+     *     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+     *     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
+     *     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
+     *     at java.lang.reflect.Method.invoke(Method.java:497)
+     *     at ...
+     * 
+ * + * Would be turned into this exception: + *
+     * java.lang.Exception: This is a test Exception
+     *     at not.bankdroid.at.all.ExceptionFactory.getException(ExceptionFactory.java:20)
+     *     at com.liato.bankdroid.utils.ExceptionUtilsTest.testBlameBankdroid(ExceptionUtilsTest.java:16)
+     *     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+     *     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
+     *     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
+     *     at java.lang.reflect.Method.invoke(Method.java:497)
+     *     at ...
+     * Caused by: java.lang.Exception: This is a test Exception
+     *     at com.liato.bankdroid.utils.ExceptionUtilsTest.testBlameBankdroid(ExceptionUtilsTest.java:16)
+     *     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+     *     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
+     *     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
+     *     at java.lang.reflect.Method.invoke(Method.java:497)
+     *     at ...
+     *     ... 37 more
+     * 
*/ - public static T bankdroidifyException(T exception) { + public static void blameBankdroid(Throwable exception) { + Throwable ultimateCause = getUltimateCause(exception); + if (ultimateCause == null) { + // Unable to find ultimate cause, never mind + return; + } + StackTraceElement[] bankdroidifiedStacktrace = - bankdroidifyStacktrace(exception.getStackTrace()); - if (bankdroidifiedStacktrace.length == exception.getStackTrace().length) { - // Unable to bankdroidify stacktrace, never mind - return exception; + bankdroidifyStacktrace(ultimateCause.getStackTrace()); + if (bankdroidifiedStacktrace.length == 0) { + // No Bankdroid stack frames found, never mind + return; + } + if (bankdroidifiedStacktrace.length == ultimateCause.getStackTrace().length) { + // Bankdroid already to blame, never mind + return; } - T returnMe = createWrapperException(exception); - if (returnMe == null) { + Throwable fakeCause = cloneException(ultimateCause); + if (fakeCause == null) { Timber.w(new RuntimeException( - "Unable to bankdroidify exception of class: " + exception.getClass())); - return exception; + "Unable to bankdroidify exception of class: " + ultimateCause.getClass())); + return; } - returnMe.initCause(exception); + // Put the bankdroidified stack trace before the fakeCause's actual stack trace + fakeCause.setStackTrace(concatArrays(bankdroidifiedStacktrace, fakeCause.getStackTrace())); - returnMe.setStackTrace(bankdroidifiedStacktrace); + ultimateCause.initCause(fakeCause); + } + @VisibleForTesting + static StackTraceElement[] concatArrays(StackTraceElement[] a, StackTraceElement[] b) { + StackTraceElement[] returnMe = new StackTraceElement[a.length + b.length]; + System.arraycopy(a, 0, returnMe, 0, a.length); + System.arraycopy(b, 0, returnMe, a.length, b.length); return returnMe; } + @VisibleForTesting + @Nullable + static Throwable getUltimateCause(Throwable t) { + int laps = 0; + Throwable ultimateCause = t; + while (ultimateCause.getCause() != null) { + ultimateCause = ultimateCause.getCause(); + if (laps++ > 10) { + return null; + } + } + return ultimateCause; + } + + /** + * Clone message and stacktrace but not the cause. + */ @Nullable - private static T createWrapperException(T wrapMe) { + @VisibleForTesting + static T cloneException(T wrapMe) { Class newClass = wrapMe.getClass(); while (newClass != null) { try { - return (T) newClass.getConstructor(String.class) - .newInstance(wrapMe.getMessage()); + T returnMe = + (T) newClass.getConstructor(String.class).newInstance(wrapMe.getMessage()); + returnMe.setStackTrace(wrapMe.getStackTrace()); + return returnMe; } catch (InvocationTargetException e) { newClass = newClass.getSuperclass(); } catch (NoSuchMethodException e) { @@ -63,9 +133,12 @@ public class ExceptionUtils { /** * Remove all initial non-Bankdroid frames from a stack. * - * @return A copy of rawStack but with the initial non-Bankdroid frames removed + * @return A copy of rawStack but with the initial non-Bankdroid frames removed, or null + * if no sensible answer can be given. */ - private static StackTraceElement[] bankdroidifyStacktrace(final StackTraceElement[] rawStack) { + @VisibleForTesting + @NonNull + static StackTraceElement[] bankdroidifyStacktrace(final StackTraceElement[] rawStack) { for (int i = 0; i < rawStack.length; i++) { StackTraceElement stackTraceElement = rawStack[i]; if (stackTraceElement.getClassName().startsWith(PREFIX)) { @@ -74,6 +147,6 @@ public class ExceptionUtils { } // No Bankdroid stack frames found, never mind - return rawStack; + return new StackTraceElement[0]; } } diff --git bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java index 36354b6..c064d9c 100644 --- bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java +++ bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java @@ -149,7 +149,8 @@ public class Urllib { try { return this.open(url, new ArrayList()); } catch (IOException e) { - throw ExceptionUtils.bankdroidifyException(e); + ExceptionUtils.blameBankdroid(e); + throw e; } } @@ -162,7 +163,8 @@ public class Urllib { try { return open(url, postData, false); } catch (IOException e) { - throw ExceptionUtils.bankdroidifyException(e); + ExceptionUtils.blameBankdroid(e); + throw e; } } @@ -182,7 +184,8 @@ public class Urllib { try { return openAsHttpResponse(url, entity, forcePost); } catch (IOException e) { - throw ExceptionUtils.bankdroidifyException(e); + ExceptionUtils.blameBankdroid(e); + throw e; } } @@ -200,7 +203,8 @@ public class Urllib { return openAsHttpResponse(url, entity, HttpMethod.POST); } } catch (IOException e) { - throw ExceptionUtils.bankdroidifyException(e); + ExceptionUtils.blameBankdroid(e); + throw e; } } @@ -293,7 +297,8 @@ public class Urllib { return openStream(url, postData != null ? new StringEntity(postData, this.charset) : null, forcePost); } catch (IOException e) { - throw ExceptionUtils.bankdroidifyException(e); + ExceptionUtils.blameBankdroid(e); + throw e; } } diff --git bankdroid-legacy/src/test/java/com/liato/bankdroid/utils/ExceptionUtilsTest.java bankdroid-legacy/src/test/java/com/liato/bankdroid/utils/ExceptionUtilsTest.java index df673c5..2b87a96 100644 --- bankdroid-legacy/src/test/java/com/liato/bankdroid/utils/ExceptionUtilsTest.java +++ bankdroid-legacy/src/test/java/com/liato/bankdroid/utils/ExceptionUtilsTest.java @@ -3,85 +3,162 @@ package com.liato.bankdroid.utils; import org.junit.Assert; import org.junit.Test; +import java.io.PrintWriter; +import java.io.StringWriter; import java.net.ConnectException; import eu.nullbyte.android.urllib.Urllib; -import not.bankdroid.at.all.ExceptionThrower; +import not.bankdroid.at.all.ExceptionFactory; -@SuppressWarnings("CallToPrintStackTrace") public class ExceptionUtilsTest { @Test - @SuppressWarnings({"PMD.AvoidPrintStackTrace", "PMD.AvoidCatchingNPE", "PMD.SystemPrintln"}) - public void testBankdroidifyException() throws Exception { - Exception raw = null; - try { - //noinspection ConstantConditions - new Urllib(null); - Assert.fail("Exception expected"); - } catch (NullPointerException e) { - raw = e; + public void testBlameBankdroid() { + Exception e = ExceptionFactory.getException(); + String before = toStringWithStacktrace(e); + ExceptionUtils.blameBankdroid(e); + String after = toStringWithStacktrace(e); + String description = + String.format("\n---- Before ----\n%s---- After ----\n%s----", before, after); + + String[] afterLines = after.split("\n"); + int lastCausedByIndex = 0; + for (int i = 0; i < afterLines.length; i++) { + if (afterLines[i].startsWith("Caused by: ")) { + lastCausedByIndex = i; + } } - // Print stack traces, useful if the tests fail - System.err.println("Before:"); - raw.printStackTrace(); + Assert.assertNotEquals(description, 0, lastCausedByIndex); + Assert.assertTrue(description, + afterLines[lastCausedByIndex + 1].startsWith("\tat com.liato.bankdroid.")); + } - System.err.println(); - System.err.println("After:"); - Exception bankdroidified = ExceptionUtils.bankdroidifyException(raw); - bankdroidified.printStackTrace(); + /** + * Like {@link #testBlameBankdroid()} but with an Exception with a cause. + */ + @Test + public void testBlameBankdroidWithCause() { + Exception e = ExceptionFactory.getExceptionWithCause(); + String before = toStringWithStacktrace(e); + ExceptionUtils.blameBankdroid(e); + String after = toStringWithStacktrace(e); + String description = + String.format("\n---- Before ----\n%s---- After ----\n%s----", before, after); + + String[] afterLines = after.split("\n"); + int firstCausedByIndex = 0; + for (int i = 0; i < afterLines.length; i++) { + if (afterLines[i].startsWith("Caused by: ")) { + firstCausedByIndex = i; + break; + } + } + Assert.assertNotEquals(description, 0, firstCausedByIndex); + Assert.assertTrue(description, + afterLines[firstCausedByIndex + 1].startsWith("\tat not.bankdroid.at.all.")); + + int lastCausedByIndex = 0; + for (int i = 0; i < afterLines.length; i++) { + if (afterLines[i].startsWith("Caused by: ")) { + lastCausedByIndex = i; + } + } + Assert.assertNotEquals(description, 0, lastCausedByIndex); + Assert.assertTrue(description, + afterLines[lastCausedByIndex + 1].startsWith("\tat com.liato.bankdroid.")); + } - Assert.assertFalse("Test setup: Top frame of initial exception shouldn't be in Bankdroid", - raw.getStackTrace()[0].getClassName().startsWith("com.liato.bankdroid.")); + @Test + public void testBlameBankdroidAlreadyToBlame() { + // Creating it here we're already inside of Bankdroid code, blaming bankdroid should be a + // no-op + Exception e = new Exception(); - Assert.assertTrue("Top frame of bankdroidified exception should be in Bankdroid", - bankdroidified.getStackTrace()[0].getClassName().startsWith("com.liato.bankdroid.")); + String before = toStringWithStacktrace(e); - // Verify that e is the cause of bankdroidified - Assert.assertSame(raw, bankdroidified.getCause()); + ExceptionUtils.blameBankdroid(e); + String after = toStringWithStacktrace(e); - // Verify that re-bankdroidifying is a no-op - Assert.assertSame(bankdroidified, ExceptionUtils.bankdroidifyException(bankdroidified)); + Assert.assertEquals(before, after); + } + + private String toStringWithStacktrace(Exception e) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + e.printStackTrace(printWriter); + printWriter.close(); + return stringWriter.toString(); } - /** - * Test that we can wrap exceptions without (String) constructors. - */ @Test - @SuppressWarnings({"PMD.AvoidPrintStackTrace", "PMD.SystemPrintln"}) - public void testBankdroidifyWonkyException() { - ExceptionThrower.WonkyException raw = null; - try { - ExceptionThrower.throwWonkyException(); - Assert.fail("Exception expected"); - } catch (ExceptionThrower.WonkyException e) { - raw = e; - } + public void testBankdroidifyStacktrace() { + StackTraceElement[] bankdroidified = new StackTraceElement[] { + new StackTraceElement("not.bankdroid.SomeClass", "someMethod", "SomeClass.java", 42), + new StackTraceElement("com.liato.bankdroid.SomeOtherClass", "someOtherMethod", "SomeOtherClass.java", 43), + }; + bankdroidified = ExceptionUtils.bankdroidifyStacktrace(bankdroidified); - // Print stack traces, useful if the tests fail - System.err.println("Before:"); - raw.printStackTrace(); + StackTraceElement[] expected = new StackTraceElement[] { + new StackTraceElement("com.liato.bankdroid.SomeOtherClass", "someOtherMethod", "SomeOtherClass.java", 43), + }; - // Since bankdroidify() won't be able to create a WonkyException, it - // should fall back to creating something it extends - ConnectException bankdroidified = ExceptionUtils.bankdroidifyException(raw); + Assert.assertArrayEquals(expected, bankdroidified); - System.err.println(); - System.err.println("After:"); - bankdroidified.printStackTrace(); + // Test re-bankdroidification + Assert.assertArrayEquals(expected, ExceptionUtils.bankdroidifyStacktrace(bankdroidified)); + } - Assert.assertFalse("Test setup: Top frame of initial exception shouldn't be in Bankdroid", - raw.getStackTrace()[0].getClassName().startsWith("com.liato.bankdroid.")); + @Test + public void testCloneExceptionWonky() { + ExceptionFactory.WonkyException raw = ExceptionFactory.getWonkyException(); + + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + ConnectException cloned = ExceptionUtils.cloneException(raw); + + assert cloned != null; + Assert.assertEquals(raw.getMessage(), cloned.getMessage()); + Assert.assertArrayEquals(raw.getStackTrace(), cloned.getStackTrace()); + Assert.assertEquals( + "Cloning an uninstantiable Exception should return an instance of its super class", + raw.getClass().getSuperclass(), cloned.getClass()); + } + + @Test + @SuppressWarnings({"PMD.AvoidCatchingNPE"}) + public void testCloneExceptionNPE() { + NullPointerException raw = null; + try { + //noinspection ConstantConditions + new Urllib(null); + Assert.fail("Exception expected"); + } catch (NullPointerException e) { + raw = e; + } - Assert.assertTrue("Top frame of bankdroidified exception should be in Bankdroid", - bankdroidified.getStackTrace()[0].getClassName().startsWith("com.liato.bankdroid.")); + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + NullPointerException cloned = ExceptionUtils.cloneException(raw); - Assert.assertEquals(raw.getMessage(), bankdroidified.getMessage()); + assert cloned != null; + Assert.assertEquals(raw.getMessage(), cloned.getMessage()); + Assert.assertArrayEquals(raw.getStackTrace(), cloned.getStackTrace()); + Assert.assertEquals(raw.getClass(), cloned.getClass()); + } - // Verify that e is the cause of bankdroidified - Assert.assertSame(raw, bankdroidified.getCause()); + @Test(timeout = 1000) + public void testGetUltimateCauseRecursive() { + Exception recursive = new Exception(); + Exception intermediate = new Exception(recursive); + recursive.initCause(intermediate); + Assert.assertNull(ExceptionUtils.getUltimateCause(recursive)); + } - // Verify that re-bankdroidifying is a no-op - Assert.assertSame(bankdroidified, ExceptionUtils.bankdroidifyException(bankdroidified)); + @Test + public void testConcatArrays() { + StackTraceElement s1 = new StackTraceElement("a", "b", "c", 123); + StackTraceElement s2 = new StackTraceElement("d", "e", "f", 456); + StackTraceElement[] concatenated = + ExceptionUtils.concatArrays( + new StackTraceElement[]{s1}, new StackTraceElement[]{s2}); + Assert.assertArrayEquals(new StackTraceElement[]{ s1, s2 }, concatenated); } } diff --git bankdroid-legacy/src/test/java/not/bankdroid/at/all/ExceptionFactory.java bankdroid-legacy/src/test/java/not/bankdroid/at/all/ExceptionFactory.java new file mode 100644 index 0000000..bc7d6fe --- /dev/null +++ bankdroid-legacy/src/test/java/not/bankdroid/at/all/ExceptionFactory.java @@ -0,0 +1,26 @@ +package not.bankdroid.at.all; + +import java.net.ConnectException; + +/** + * For the test in {@link com.liato.bankdroid.utils.ExceptionUtilsTest} + */ +public class ExceptionFactory { + public static class WonkyException extends ConnectException { + public WonkyException(int wonky) { + super("Wonky: " + wonky); + } + } + + public static WonkyException getWonkyException() { + return new WonkyException(5); + } + + public static Exception getException() { + return new Exception("This is a test Exception"); + } + + public static Exception getExceptionWithCause() { + return new Exception("This Exception has a cause", getException()); + } +} diff --git config/quality/lint/lint.xml config/quality/lint/lint.xml index 8195ce2..c3565a5 100644 --- config/quality/lint/lint.xml +++ config/quality/lint/lint.xml @@ -1,8 +1,11 @@ - + + + + diff --git tools/update-suppressions.sh tools/update-suppressions.sh index 53ff534..1652568 100755 --- tools/update-suppressions.sh +++ tools/update-suppressions.sh @@ -21,9 +21,12 @@ function set_lint_suppressions() { cat > ${LINT_XML} << EOF - + + + + EOF commit d64ede1abee89d557a5f4e0ea720a265b4256e05 Author: Johan Walles Date: Thu Nov 3 20:55:57 2016 +0100 Fix wrapping exceptions without String constructors Before this change, bankdroidifyException() failed if the exception to wrap didn't come with a String constructor. Now it works, with tests and all! diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/ExceptionUtils.java bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/ExceptionUtils.java index e61097b..468b56e 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/ExceptionUtils.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/ExceptionUtils.java @@ -1,5 +1,7 @@ package com.liato.bankdroid.utils; +import android.support.annotation.Nullable; + import java.lang.reflect.InvocationTargetException; import java.util.Arrays; @@ -23,21 +25,10 @@ public class ExceptionUtils { return exception; } - T returnMe; - try { - returnMe = (T)exception.getClass().getConstructor(String.class) - .newInstance(exception.getMessage()); - } catch (InstantiationException e) { - Timber.e(e, "Unable to Bankdroidify exception of type %s", exception.getClass()); - return exception; - } catch (InvocationTargetException e) { - Timber.e(e, "Unable to Bankdroidify exception of type %s", exception.getClass()); - return exception; - } catch (IllegalAccessException e) { - Timber.e(e, "Unable to Bankdroidify exception of type %s", exception.getClass()); - return exception; - } catch (NoSuchMethodException e) { - Timber.e(e, "Unable to Bankdroidify exception of type %s", exception.getClass()); + T returnMe = createWrapperException(exception); + if (returnMe == null) { + Timber.w(new RuntimeException( + "Unable to bankdroidify exception of class: " + exception.getClass())); return exception; } @@ -48,6 +39,27 @@ public class ExceptionUtils { return returnMe; } + @Nullable + private static T createWrapperException(T wrapMe) { + Class newClass = wrapMe.getClass(); + while (newClass != null) { + try { + return (T) newClass.getConstructor(String.class) + .newInstance(wrapMe.getMessage()); + } catch (InvocationTargetException e) { + newClass = newClass.getSuperclass(); + } catch (NoSuchMethodException e) { + newClass = newClass.getSuperclass(); + } catch (InstantiationException e) { + newClass = newClass.getSuperclass(); + } catch (IllegalAccessException e) { + newClass = newClass.getSuperclass(); + } + } + + return null; + } + /** * Remove all initial non-Bankdroid frames from a stack. * diff --git bankdroid-legacy/src/test/java/com/liato/bankdroid/utils/ExceptionUtilsTest.java bankdroid-legacy/src/test/java/com/liato/bankdroid/utils/ExceptionUtilsTest.java index 803dc5f..df673c5 100644 --- bankdroid-legacy/src/test/java/com/liato/bankdroid/utils/ExceptionUtilsTest.java +++ bankdroid-legacy/src/test/java/com/liato/bankdroid/utils/ExceptionUtilsTest.java @@ -3,14 +3,19 @@ package com.liato.bankdroid.utils; import org.junit.Assert; import org.junit.Test; +import java.net.ConnectException; + import eu.nullbyte.android.urllib.Urllib; +import not.bankdroid.at.all.ExceptionThrower; +@SuppressWarnings("CallToPrintStackTrace") public class ExceptionUtilsTest { @Test - @SuppressWarnings("PMD") // This is for the stack trace printing, we really want to do it here - public void bankdroidifyException() throws Exception { + @SuppressWarnings({"PMD.AvoidPrintStackTrace", "PMD.AvoidCatchingNPE", "PMD.SystemPrintln"}) + public void testBankdroidifyException() throws Exception { Exception raw = null; try { + //noinspection ConstantConditions new Urllib(null); Assert.fail("Exception expected"); } catch (NullPointerException e) { @@ -38,4 +43,45 @@ public class ExceptionUtilsTest { // Verify that re-bankdroidifying is a no-op Assert.assertSame(bankdroidified, ExceptionUtils.bankdroidifyException(bankdroidified)); } + + /** + * Test that we can wrap exceptions without (String) constructors. + */ + @Test + @SuppressWarnings({"PMD.AvoidPrintStackTrace", "PMD.SystemPrintln"}) + public void testBankdroidifyWonkyException() { + ExceptionThrower.WonkyException raw = null; + try { + ExceptionThrower.throwWonkyException(); + Assert.fail("Exception expected"); + } catch (ExceptionThrower.WonkyException e) { + raw = e; + } + + // Print stack traces, useful if the tests fail + System.err.println("Before:"); + raw.printStackTrace(); + + // Since bankdroidify() won't be able to create a WonkyException, it + // should fall back to creating something it extends + ConnectException bankdroidified = ExceptionUtils.bankdroidifyException(raw); + + System.err.println(); + System.err.println("After:"); + bankdroidified.printStackTrace(); + + Assert.assertFalse("Test setup: Top frame of initial exception shouldn't be in Bankdroid", + raw.getStackTrace()[0].getClassName().startsWith("com.liato.bankdroid.")); + + Assert.assertTrue("Top frame of bankdroidified exception should be in Bankdroid", + bankdroidified.getStackTrace()[0].getClassName().startsWith("com.liato.bankdroid.")); + + Assert.assertEquals(raw.getMessage(), bankdroidified.getMessage()); + + // Verify that e is the cause of bankdroidified + Assert.assertSame(raw, bankdroidified.getCause()); + + // Verify that re-bankdroidifying is a no-op + Assert.assertSame(bankdroidified, ExceptionUtils.bankdroidifyException(bankdroidified)); + } } diff --git bankdroid-legacy/src/test/java/not/bankdroid/at/all/ExceptionThrower.java bankdroid-legacy/src/test/java/not/bankdroid/at/all/ExceptionThrower.java new file mode 100644 index 0000000..1d21a09 --- /dev/null +++ bankdroid-legacy/src/test/java/not/bankdroid/at/all/ExceptionThrower.java @@ -0,0 +1,18 @@ +package not.bankdroid.at.all; + +import java.net.ConnectException; + +/** + * For the test in {@link com.liato.bankdroid.utils.ExceptionUtilsTest} + */ +public class ExceptionThrower { + public static class WonkyException extends ConnectException { + public WonkyException(int wonky) { + super("Wonky: " + wonky); + } + } + + public static void throwWonkyException() throws WonkyException { + throw new WonkyException(5); + } +} commit 04230928f02c083eafa9c631bb82c09f54ce8143 Author: Johan Walles Date: Fri Nov 4 11:26:49 2016 +0100 PMD: Enforce variable naming Inspired by a review for another change; these things are better found by tooling. Non-final fields with NAMING_INDICATING_FINALITY have been turned into final fields when possible. In Bank.java, refactored the API a bit so that bank names can be constant. diff --git app/src/main/java/com/liato/bankdroid/BankEditActivity.java app/src/main/java/com/liato/bankdroid/BankEditActivity.java index 835c64e..88c142e 100644 --- app/src/main/java/com/liato/bankdroid/BankEditActivity.java +++ app/src/main/java/com/liato/bankdroid/BankEditActivity.java @@ -81,9 +81,9 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected @InjectView(R.id.txtErrorDesc) TextView mErrorDescription; - private Bank SELECTED_BANK; + private Bank selectedBank; - private long BANKID = -1; + private long bankId = -1; @Override public void onCreate(Bundle savedInstanceState) { @@ -101,16 +101,16 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected Bundle extras = getIntent().getExtras(); if (extras != null) { - BANKID = extras.getLong("id", -1); - if (BANKID != -1) { - Bank bank = BankFactory.bankFromDb(BANKID, this, false); + bankId = extras.getLong("id", -1); + if (bankId != -1) { + Bank bank = BankFactory.bankFromDb(bankId, this, false); if (bank != null) { mErrorDescription.setVisibility( bank.isDisabled() ? View.VISIBLE : View.INVISIBLE); mBankSpinner.setEnabled(false); mBankSpinner.setSelection(adapter.getPosition(bank)); - SELECTED_BANK = bank; - createForm(SELECTED_BANK.getConnectionConfiguration(), + selectedBank = bank; + createForm(selectedBank.getConnectionConfiguration(), DefaultConnectionConfiguration.fields() ); populateForm(bank); @@ -125,10 +125,10 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected if (!validate()) { return; } - SELECTED_BANK.setProperties(getFormParameters(SELECTED_BANK.getConnectionConfiguration())); - SELECTED_BANK.setCustomName(getFormParameter(DefaultConnectionConfiguration.NAME)); - SELECTED_BANK.setDbid(BANKID); - new DataRetrieverTask(this, SELECTED_BANK).execute(); + selectedBank.setProperties(getFormParameters(selectedBank.getConnectionConfiguration())); + selectedBank.setCustomName(getFormParameter(DefaultConnectionConfiguration.NAME)); + selectedBank.setDbid(bankId); + new DataRetrieverTask(this, selectedBank).execute(); } @OnClick(R.id.btnSettingsCancel) @@ -139,10 +139,10 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected @Override public void onItemSelected(AdapterView parentView, View selectedItemView, int pos, long id) { Bank selectedBank = (Bank) parentView.getItemAtPosition(pos); - if (SELECTED_BANK == null || !SELECTED_BANK.equals(selectedBank)) { - SELECTED_BANK = selectedBank; + if (this.selectedBank == null || !this.selectedBank.equals(selectedBank)) { + this.selectedBank = selectedBank; mFormContainer.removeAllViewsInLayout(); - createForm(SELECTED_BANK.getConnectionConfiguration(), + createForm(this.selectedBank.getConnectionConfiguration(), DefaultConnectionConfiguration.fields() ); } @@ -233,7 +233,7 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected private boolean validate() { boolean valid = true; - Iterator fields = Iterators.concat(SELECTED_BANK.getConnectionConfiguration().iterator(), + Iterator fields = Iterators.concat(selectedBank.getConnectionConfiguration().iterator(), DefaultConnectionConfiguration.fields().iterator()); while (fields.hasNext()) { Field field = fields.next(); @@ -357,8 +357,8 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected builder.setTitle(R.string.select_a_bank); builder.setItems(items, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { - SELECTED_BANK.setExtras(e.getBanks().get(item).getId()); - new DataRetrieverTask(context, SELECTED_BANK).execute(); + selectedBank.setExtras(e.getBanks().get(item).getId()); + new DataRetrieverTask(context, selectedBank).execute(); } }); } else { diff --git app/src/main/java/com/liato/bankdroid/LockableActivity.java app/src/main/java/com/liato/bankdroid/LockableActivity.java index 1b5720f..397b3e1 100644 --- app/src/main/java/com/liato/bankdroid/LockableActivity.java +++ app/src/main/java/com/liato/bankdroid/LockableActivity.java @@ -40,7 +40,7 @@ import android.view.WindowManager; public class LockableActivity extends ActionBarActivity { - private static int PATTERNLOCK_UNLOCK = 42; + private static final int PATTERNLOCK_UNLOCK = 42; protected boolean mSkipLockOnce = false; diff --git app/src/main/java/com/liato/bankdroid/LockablePreferenceActivity.java app/src/main/java/com/liato/bankdroid/LockablePreferenceActivity.java index ffa55f5..b690302 100644 --- app/src/main/java/com/liato/bankdroid/LockablePreferenceActivity.java +++ app/src/main/java/com/liato/bankdroid/LockablePreferenceActivity.java @@ -31,7 +31,7 @@ import android.view.WindowManager; public class LockablePreferenceActivity extends PreferenceActivity { - private static int PATTERNLOCK_UNLOCK = 42; + private static final int PATTERNLOCK_UNLOCK = 42; private SharedPreferences mPrefs; diff --git app/src/main/java/com/liato/bankdroid/MainActivity.java app/src/main/java/com/liato/bankdroid/MainActivity.java index 48acb74..98c8a2b 100644 --- app/src/main/java/com/liato/bankdroid/MainActivity.java +++ app/src/main/java/com/liato/bankdroid/MainActivity.java @@ -59,9 +59,9 @@ public class MainActivity extends LockableActivity { protected static boolean showHidden = false; - private static Bank selected_bank = null; + private static Bank selectedBank = null; - private static Account selected_account = null; + private static Account selectedAccount = null; private final BroadcastReceiver receiver = new BroadcastReceiver() { @Override @@ -91,14 +91,14 @@ public class MainActivity extends LockableActivity { public boolean onItemLongClick(final AdapterView parent, final View view, final int position, final long id) { if (adapter.getItem(position) instanceof Account) { - selected_account = (Account) adapter.getItem(position); + selectedAccount = (Account) adapter.getItem(position); final PopupMenuAccount pmenu = new PopupMenuAccount(parent, view, MainActivity.this); pmenu.showLikeQuickAction(0, 12); return true; } else if (adapter.getItem(position) instanceof Bank) { - selected_bank = (Bank) adapter.getItem(position); - selected_bank.toggleHideAccounts(); - DBAdapter.save(selected_bank, MainActivity.this); + selectedBank = (Bank) adapter.getItem(position); + selectedBank.toggleHideAccounts(); + DBAdapter.save(selectedBank, MainActivity.this); refreshView(); return true; } @@ -110,7 +110,7 @@ public class MainActivity extends LockableActivity { public void onItemClick(final AdapterView parent, final View view, final int position, final long id) { if (adapter.getItem(position) instanceof Bank) { - selected_bank = (Bank) adapter.getItem(position); + selectedBank = (Bank) adapter.getItem(position); final PopupMenuBank pmenu = new PopupMenuBank(parent, view, MainActivity.this); pmenu.showLikeQuickAction(0, 12); } else { @@ -241,7 +241,7 @@ public class MainActivity extends LockableActivity { final Button btnHide = (Button) root.findViewById(R.id.btnHide); final Button btnUnhide = (Button) root.findViewById(R.id.btnUnhide); final Button btnWWW = (Button) root.findViewById(R.id.btnWWW); - if (selected_bank.getHideAccounts()) { + if (selectedBank.getHideAccounts()) { btnHide.setVisibility(View.GONE); btnUnhide.setVisibility(View.VISIBLE); btnUnhide.setOnClickListener(this); @@ -250,7 +250,7 @@ public class MainActivity extends LockableActivity { btnUnhide.setVisibility(View.GONE); btnHide.setOnClickListener(this); } - if (selected_bank.isWebViewEnabled()) { + if (selectedBank.isWebViewEnabled()) { btnWWW.setOnClickListener(this); } else { btnWWW.setVisibility(View.GONE); @@ -270,29 +270,29 @@ public class MainActivity extends LockableActivity { case R.id.btnHide: case R.id.btnUnhide: this.dismiss(); - selected_bank.toggleHideAccounts(); - DBAdapter.save(selected_bank, context); + selectedBank.toggleHideAccounts(); + DBAdapter.save(selectedBank, context); parent.refreshView(); return; case R.id.btnWWW: - if (selected_bank != null && selected_bank.isWebViewEnabled()) { - //Uri uri = Uri.parse(selected_bank.getURL()); + if (selectedBank != null && selectedBank.isWebViewEnabled()) { + //Uri uri = Uri.parse(selectedBank.getURL()); //Intent intent = new Intent(Intent.ACTION_VIEW, uri); final Intent intent = new Intent(context, WebViewActivity.class); - intent.putExtra("bankid", selected_bank.getDbId()); + intent.putExtra("bankid", selectedBank.getDbId()); context.startActivity(intent); } this.dismiss(); return; case R.id.btnEdit: final Intent intent = new Intent(context, BankEditActivity.class); - intent.putExtra("id", selected_bank.getDbId()); + intent.putExtra("id", selectedBank.getDbId()); context.startActivity(intent); this.dismiss(); return; case R.id.btnRefresh: this.dismiss(); - new DataRetrieverTask(parent, selected_bank.getDbId()).execute(); + new DataRetrieverTask(parent, selectedBank.getDbId()).execute(); return; case R.id.btnRemove: this.dismiss(); @@ -307,7 +307,7 @@ public class MainActivity extends LockableActivity { public void onClick(final DialogInterface dialog, final int id) { final DBAdapter db = new DBAdapter(context); - db.deleteBank(selected_bank.getDbId()); + db.deleteBank(selectedBank.getDbId()); dialog.cancel(); parent.refreshView(); } @@ -358,7 +358,7 @@ public class MainActivity extends LockableActivity { .findViewById(R.id.btnDisableNotifications); final Button btnEnableNotifications = (Button) root .findViewById(R.id.btnEnableNotifications); - if (selected_account.isHidden()) { + if (selectedAccount.isHidden()) { btnHide.setVisibility(View.GONE); btnUnhide.setVisibility(View.VISIBLE); btnUnhide.setOnClickListener(this); @@ -367,7 +367,7 @@ public class MainActivity extends LockableActivity { btnUnhide.setVisibility(View.GONE); btnHide.setOnClickListener(this); } - if (selected_account.isNotify()) { + if (selectedAccount.isNotify()) { btnDisableNotifications.setVisibility(View.VISIBLE); btnDisableNotifications.setOnClickListener(this); btnEnableNotifications.setVisibility(View.GONE); @@ -385,26 +385,26 @@ public class MainActivity extends LockableActivity { switch (id) { case R.id.btnHide: this.dismiss(); - selected_account.setHidden(true); - DBAdapter.save(selected_account.getBank(), parent); + selectedAccount.setHidden(true); + DBAdapter.save(selectedAccount.getBank(), parent); parent.refreshView(); return; case R.id.btnUnhide: this.dismiss(); - selected_account.setHidden(false); - DBAdapter.save(selected_account.getBank(), parent); + selectedAccount.setHidden(false); + DBAdapter.save(selectedAccount.getBank(), parent); parent.refreshView(); return; case R.id.btnEnableNotifications: this.dismiss(); - selected_account.setNotify(true); - DBAdapter.save(selected_account.getBank(), parent); + selectedAccount.setNotify(true); + DBAdapter.save(selectedAccount.getBank(), parent); parent.refreshView(); return; case R.id.btnDisableNotifications: this.dismiss(); - selected_account.setNotify(false); - DBAdapter.save(selected_account.getBank(), parent); + selectedAccount.setNotify(false); + DBAdapter.save(selectedAccount.getBank(), parent); parent.refreshView(); return; diff --git app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java index 7a82c5b..6af968b 100644 --- app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java +++ app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java @@ -394,15 +394,15 @@ public class AutoRefreshService extends Service { } } catch (final BankException e) { // Refresh widgets if an update fails - Timber.e(e, "Could not update bank %s", bank.getShortName()); + Timber.e(e, "Could not update bank %s", bank.getName()); } catch (final LoginException e) { - Timber.d(e, "Invalid credentials for bank %s", bank.getShortName()); + Timber.d(e, "Invalid credentials for bank %s", bank.getName()); refreshWidgets = true; db.disableBank(bank.getDbId()); } catch (BankChoiceException e) { Timber.w(e, "BankChoiceException"); } catch (Exception e) { - Timber.e(e, "An unexpected error occurred while updating bank %s", bank.getShortName()); + Timber.e(e, "An unexpected error occurred while updating bank %s", bank.getName()); } } diff --git app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java index fd0011c..e6dfd7f 100644 --- app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java +++ app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java @@ -390,15 +390,15 @@ public abstract class BankdroidWidgetProvider extends AppWidgetProvider { } } catch (BankException e) { - Timber.e(e, "Could not update bank %s", bank.getShortName()); + Timber.e(e, "Could not update bank %s", bank.getName()); } catch (LoginException e) { - Timber.w(e, "Invalid credentials for bank %s", bank.getShortName()); + Timber.w(e, "Invalid credentials for bank %s", bank.getName()); DBAdapter.disable(bank, context); } catch (BankChoiceException e) { Timber.w(e, "BankChoiceException"); } catch (IOException e) { if (NetworkUtils.isInternetAvailable()) { - Timber.e(e, "Could not update bank %s", bank.getShortName()); + Timber.e(e, "Could not update bank %s", bank.getName()); } } BankdroidWidgetProvider.updateAppWidget(context, appWidgetManager, appWidgetId); diff --git app/src/main/java/com/liato/bankdroid/provider/BankTransactionsProvider.java app/src/main/java/com/liato/bankdroid/provider/BankTransactionsProvider.java index 3e16654..0610ad7 100644 --- app/src/main/java/com/liato/bankdroid/provider/BankTransactionsProvider.java +++ app/src/main/java/com/liato/bankdroid/provider/BankTransactionsProvider.java @@ -67,44 +67,44 @@ public class BankTransactionsProvider extends ContentProvider implements private static final String TRANSACTIONS_TABLE = "transactions"; - private final static UriMatcher uriMatcher; + private final static UriMatcher URI_MATCHER; - private final static Map bankAccountProjectionMap; + private final static Map BANK_ACCOUNT_PROJECTION_MAP; - private final static Map transProjectionMap; + private final static Map TRANS_PROJECTION_MAP; static { - uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - uriMatcher.addURI(AUTHORITY, TRANSACTIONS_CAT + "/" + WILD_CARD, + URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); + URI_MATCHER.addURI(AUTHORITY, TRANSACTIONS_CAT + "/" + WILD_CARD, TRANSACTIONS); - uriMatcher.addURI(AUTHORITY, BANK_ACCOUNTS_CAT + "/" + WILD_CARD, + URI_MATCHER.addURI(AUTHORITY, BANK_ACCOUNTS_CAT + "/" + WILD_CARD, BANK_ACCOUNTS); // Projections are "Poor mans views" of the data. - bankAccountProjectionMap = new HashMap(); + BANK_ACCOUNT_PROJECTION_MAP = new HashMap(); // Must match bankAccountProjection in // IBankTransactionsProvider#bankAccountProjection - bankAccountProjectionMap.put(BANK_ID, BANK_ID); - bankAccountProjectionMap.put(BANK_NAME, BANK_NAME); - bankAccountProjectionMap.put(BANK_TYPE, BANK_TYPE); - bankAccountProjectionMap.put(BANK_LAST_UPDATED, BANK_LAST_UPDATED); - bankAccountProjectionMap.put(ACC_ID, ACC_ID); - bankAccountProjectionMap.put(ACC_NAME, ACC_NAME); + BANK_ACCOUNT_PROJECTION_MAP.put(BANK_ID, BANK_ID); + BANK_ACCOUNT_PROJECTION_MAP.put(BANK_NAME, BANK_NAME); + BANK_ACCOUNT_PROJECTION_MAP.put(BANK_TYPE, BANK_TYPE); + BANK_ACCOUNT_PROJECTION_MAP.put(BANK_LAST_UPDATED, BANK_LAST_UPDATED); + BANK_ACCOUNT_PROJECTION_MAP.put(ACC_ID, ACC_ID); + BANK_ACCOUNT_PROJECTION_MAP.put(ACC_NAME, ACC_NAME); // Table name has to be explicitly included here since Banks also have a column named balance. - bankAccountProjectionMap.put(ACC_BALANCE, ACCOUNT_TABLE + "." + ACC_BALANCE); - bankAccountProjectionMap.put(ACC_TYPE, ACC_TYPE); + BANK_ACCOUNT_PROJECTION_MAP.put(ACC_BALANCE, ACCOUNT_TABLE + "." + ACC_BALANCE); + BANK_ACCOUNT_PROJECTION_MAP.put(ACC_TYPE, ACC_TYPE); - transProjectionMap = new HashMap(); + TRANS_PROJECTION_MAP = new HashMap(); // Must match transactionProjection in // IBankTransactionsProvider#transactionProjection - transProjectionMap.put(TRANS_ID, TRANS_ID); - transProjectionMap.put(TRANS_DATE, TRANS_DATE); - transProjectionMap.put(TRANS_DESC, TRANS_DESC); - transProjectionMap.put(TRANS_AMT, TRANS_AMT); - transProjectionMap.put(TRANS_CUR, TRANS_CUR); - transProjectionMap.put(TRANS_ACCNT, TRANS_ACCNT); + TRANS_PROJECTION_MAP.put(TRANS_ID, TRANS_ID); + TRANS_PROJECTION_MAP.put(TRANS_DATE, TRANS_DATE); + TRANS_PROJECTION_MAP.put(TRANS_DESC, TRANS_DESC); + TRANS_PROJECTION_MAP.put(TRANS_AMT, TRANS_AMT); + TRANS_PROJECTION_MAP.put(TRANS_CUR, TRANS_CUR); + TRANS_PROJECTION_MAP.put(TRANS_ACCNT, TRANS_ACCNT); } private DatabaseHelper dbHelper; @@ -143,7 +143,7 @@ public class BankTransactionsProvider extends ContentProvider implements public String getType(final Uri uri) { Timber.d("Got URI: %s", uri.toString()); - switch (uriMatcher.match(uri)) { + switch (URI_MATCHER.match(uri)) { case BANK_ACCOUNTS: return BANK_ACCOUNTS_MIME; case TRANSACTIONS: @@ -207,12 +207,12 @@ public class BankTransactionsProvider extends ContentProvider implements if (BANK_ACCOUNTS_MIME.equals(getType(uri))) { qb = new SQLiteQueryBuilder(); qb.setTables(BANK_ACCOUNT_TABLES); - qb.setProjectionMap(bankAccountProjectionMap); + qb.setProjectionMap(BANK_ACCOUNT_PROJECTION_MAP); qb.setDistinct(true); } else if (TRANSACTIONS_MIME.equals(getType(uri))) { qb = new SQLiteQueryBuilder(); qb.setTables(TRANSACTIONS_TABLE); - qb.setProjectionMap(transProjectionMap); + qb.setProjectionMap(TRANS_PROJECTION_MAP); } else { throw new IllegalArgumentException("Unsupported URI: " + uri); } diff --git app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java index 87a0c1d..8997f8f 100644 --- app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java +++ app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java @@ -41,7 +41,7 @@ public class ColorPickerPreference Preference.OnPreferenceClickListener, ColorPickerDialog.OnColorChangedListener { - private static final String androidns = "http://schemas.android.com/apk/res/android"; + private static final String ANDROID_NS = "http://schemas.android.com/apk/res/android"; private ViewGroup parent; @@ -137,7 +137,7 @@ public class ColorPickerPreference mDensity = getContext().getResources().getDisplayMetrics().density; setOnPreferenceClickListener(this); if (attrs != null) { - String defaultValue = attrs.getAttributeValue(androidns, "defaultValue"); + String defaultValue = attrs.getAttributeValue(ANDROID_NS, "defaultValue"); if (defaultValue.startsWith("#")) { try { mDefaultValue = convertToColorInt(defaultValue); @@ -146,7 +146,7 @@ public class ColorPickerPreference mDefaultValue = convertToColorInt("#FF000000"); } } else { - int resourceId = attrs.getAttributeResourceValue(androidns, "defaultValue", 0); + int resourceId = attrs.getAttributeResourceValue(ANDROID_NS, "defaultValue", 0); if (resourceId != 0) { mDefaultValue = context.getResources().getInteger(resourceId); } diff --git app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerView.java app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerView.java index 8ff79d3..13af48e 100644 --- app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerView.java +++ app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerView.java @@ -64,29 +64,29 @@ public class ColorPickerView extends View { /** * The width in dp of the hue panel. */ - private float HUE_PANEL_WIDTH = 30f; + private float huePanelWidth = 30f; /** * The height in dp of the alpha panel */ - private float ALPHA_PANEL_HEIGHT = 20f; + private float alphaPanelHeight = 20f; /** * The distance in dp between the different * color panels. */ - private float PANEL_SPACING = 10f; + private float panelSpacing = 10f; /** * The radius in dp of the color palette tracker circle. */ - private float PALETTE_CIRCLE_TRACKER_RADIUS = 5f; + private float paletteCircleTrackerRadius = 5f; /** * The dp which the tracker of the hue or alpha panel * will extend outside of its bounds. */ - private float RECTANGLE_TRACKER_OFFSET = 2f; + private float rectangleTrackerOffset = 2f; private float mDensity = 1f; @@ -172,11 +172,11 @@ public class ColorPickerView extends View { setLayerType(View.LAYER_TYPE_SOFTWARE, null); } mDensity = getContext().getResources().getDisplayMetrics().density; - PALETTE_CIRCLE_TRACKER_RADIUS *= mDensity; - RECTANGLE_TRACKER_OFFSET *= mDensity; - HUE_PANEL_WIDTH *= mDensity; - ALPHA_PANEL_HEIGHT *= mDensity; - PANEL_SPACING = PANEL_SPACING * mDensity; + paletteCircleTrackerRadius *= mDensity; + rectangleTrackerOffset *= mDensity; + huePanelWidth *= mDensity; + alphaPanelHeight *= mDensity; + panelSpacing = panelSpacing * mDensity; mDrawingOffset = calculateRequiredOffset(); @@ -216,7 +216,7 @@ public class ColorPickerView extends View { } private float calculateRequiredOffset() { - float offset = Math.max(PALETTE_CIRCLE_TRACKER_RADIUS, RECTANGLE_TRACKER_OFFSET); + float offset = Math.max(paletteCircleTrackerRadius, rectangleTrackerOffset); offset = Math.max(offset, BORDER_WIDTH_PX * mDensity); return offset * 1.5f; @@ -274,11 +274,11 @@ public class ColorPickerView extends View { Point p = satValToPoint(mSat, mVal); mSatValTrackerPaint.setColor(0xff000000); - canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS - 1f * mDensity, + canvas.drawCircle(p.x, p.y, paletteCircleTrackerRadius - 1f * mDensity, mSatValTrackerPaint); mSatValTrackerPaint.setColor(0xffdddddd); - canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS, mSatValTrackerPaint); + canvas.drawCircle(p.x, p.y, paletteCircleTrackerRadius, mSatValTrackerPaint); } @@ -308,8 +308,8 @@ public class ColorPickerView extends View { Point p = hueToPoint(mHue); RectF r = new RectF(); - r.left = rect.left - RECTANGLE_TRACKER_OFFSET; - r.right = rect.right + RECTANGLE_TRACKER_OFFSET; + r.left = rect.left - rectangleTrackerOffset; + r.right = rect.right + rectangleTrackerOffset; r.top = p.y - rectHeight; r.bottom = p.y + rectHeight; @@ -359,8 +359,8 @@ public class ColorPickerView extends View { RectF r = new RectF(); r.left = p.x - rectWidth; r.right = p.x + rectWidth; - r.top = rect.top - RECTANGLE_TRACKER_OFFSET; - r.bottom = rect.bottom + RECTANGLE_TRACKER_OFFSET; + r.top = rect.top - rectangleTrackerOffset; + r.bottom = rect.bottom + rectangleTrackerOffset; canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint); @@ -663,22 +663,22 @@ public class ColorPickerView extends View { if (!mShowAlphaPanel) { - height = (int) (widthAllowed - PANEL_SPACING - HUE_PANEL_WIDTH); + height = (int) (widthAllowed - panelSpacing - huePanelWidth); //If calculated height (based on the width) is more than the allowed height. if (height > heightAllowed || getTag().equals("landscape")) { height = heightAllowed; - width = (int) (height + PANEL_SPACING + HUE_PANEL_WIDTH); + width = (int) (height + panelSpacing + huePanelWidth); } else { width = widthAllowed; } } else { - width = (int) (heightAllowed - ALPHA_PANEL_HEIGHT + HUE_PANEL_WIDTH); + width = (int) (heightAllowed - alphaPanelHeight + huePanelWidth); if (width > widthAllowed) { width = widthAllowed; - height = (int) (widthAllowed - HUE_PANEL_WIDTH + ALPHA_PANEL_HEIGHT); + height = (int) (widthAllowed - huePanelWidth + alphaPanelHeight); } else { height = heightAllowed; } @@ -709,10 +709,10 @@ public class ColorPickerView extends View { int width = getPrefferedHeight(); if (mShowAlphaPanel) { - width -= (PANEL_SPACING + ALPHA_PANEL_HEIGHT); + width -= (panelSpacing + alphaPanelHeight); } - return (int) (width + HUE_PANEL_WIDTH + PANEL_SPACING); + return (int) (width + huePanelWidth + panelSpacing); } @@ -721,7 +721,7 @@ public class ColorPickerView extends View { int height = (int) (200 * mDensity); if (mShowAlphaPanel) { - height += PANEL_SPACING + ALPHA_PANEL_HEIGHT; + height += panelSpacing + alphaPanelHeight; } return height; @@ -748,7 +748,7 @@ public class ColorPickerView extends View { float panelSide = dRect.height() - BORDER_WIDTH_PX * 2; if (mShowAlphaPanel) { - panelSide -= PANEL_SPACING + ALPHA_PANEL_HEIGHT; + panelSide -= panelSpacing + alphaPanelHeight; } float left = dRect.left + BORDER_WIDTH_PX; @@ -762,10 +762,10 @@ public class ColorPickerView extends View { private void setUpHueRect() { final RectF dRect = mDrawingRect; - float left = dRect.right - HUE_PANEL_WIDTH + BORDER_WIDTH_PX; + float left = dRect.right - huePanelWidth + BORDER_WIDTH_PX; float top = dRect.top + BORDER_WIDTH_PX; - float bottom = dRect.bottom - BORDER_WIDTH_PX - (mShowAlphaPanel ? (PANEL_SPACING - + ALPHA_PANEL_HEIGHT) : 0); + float bottom = dRect.bottom - BORDER_WIDTH_PX - (mShowAlphaPanel ? (panelSpacing + + alphaPanelHeight) : 0); float right = dRect.right - BORDER_WIDTH_PX; mHueRect = new RectF(left, top, right, bottom); @@ -780,7 +780,7 @@ public class ColorPickerView extends View { final RectF dRect = mDrawingRect; float left = dRect.left + BORDER_WIDTH_PX; - float top = dRect.bottom - ALPHA_PANEL_HEIGHT + BORDER_WIDTH_PX; + float top = dRect.bottom - alphaPanelHeight + BORDER_WIDTH_PX; float bottom = dRect.bottom - BORDER_WIDTH_PX; float right = dRect.right - BORDER_WIDTH_PX; diff --git app/src/test/java/com/liato/bankdroid/appwidget/DataRetrieverTaskTest.java app/src/test/java/com/liato/bankdroid/appwidget/DataRetrieverTaskTest.java index b821497..5e3de22 100644 --- app/src/test/java/com/liato/bankdroid/appwidget/DataRetrieverTaskTest.java +++ app/src/test/java/com/liato/bankdroid/appwidget/DataRetrieverTaskTest.java @@ -3,6 +3,7 @@ package com.liato.bankdroid.appwidget; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.db.DBAdapter; +import com.liato.bankdroid.provider.IBankTypes; import org.junit.Assert; import org.junit.Test; @@ -56,6 +57,16 @@ public class DataRetrieverTaskTest { public BigDecimal getBalance() { return getAccounts().get(0).getBalance(); } + + @Override + public int getBanktypeId() { + return IBankTypes.TESTBANK; + } + + @Override + public String getName() { + return "Testbanken"; + } } private static class TestableDataRetrieverTask extends AutoRefreshService.DataRetrieverTask { diff --git bankdroid-core/src/main/java/com/liato/bankdroid/configuration/DefaultConnectionConfiguration.java bankdroid-core/src/main/java/com/liato/bankdroid/configuration/DefaultConnectionConfiguration.java index 3a47c5b..2fefbb9 100644 --- bankdroid-core/src/main/java/com/liato/bankdroid/configuration/DefaultConnectionConfiguration.java +++ bankdroid-core/src/main/java/com/liato/bankdroid/configuration/DefaultConnectionConfiguration.java @@ -12,7 +12,7 @@ public class DefaultConnectionConfiguration { public static final String NAME = "provider.configuration.name"; - private final static List configuration = createConfiguration(); + private final static List CONFIGURATION = createConfiguration(); private static List createConfiguration() { List configuration = new ArrayList<>(); @@ -24,6 +24,6 @@ public class DefaultConnectionConfiguration { } public static List fields() { - return configuration; + return CONFIGURATION; } } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/Helpers.java bankdroid-legacy/src/main/java/com/liato/bankdroid/Helpers.java index 2ea759a..7e53cf4 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/Helpers.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/Helpers.java @@ -38,7 +38,7 @@ import timber.log.Timber; public class Helpers { private static final StrikethroughSpan STRIKE_THROUGH_SPAN = new StrikethroughSpan(); - private final static String[] currencies = {"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", + private final static String[] CURRENCIES = {"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTN", "BWP", "BYR", @@ -62,7 +62,7 @@ public class Helpers { "XAU", "XCD", "XDR", "XOF", "XPD", "XPF", "XPT", "YER", "ZAR", "ZMK", "ZWD"}; - private final static String[][] symMappings = {{"$U", "UYU"}, {"$b", "BOB"}, {"BZ$", "BZD"}, + private final static String[][] SYM_MAPPINGS = {{"$U", "UYU"}, {"$b", "BOB"}, {"BZ$", "BZD"}, {"C$", "NIO"}, {"J$", "JMD"}, {"NT$", "TWD"}, {"R$", "BRL"}, {"RD$", "DOP"}, {"TT$", "TTD"}, {"Z$", "ZWD"}, {"$", "USD"}, {"B/.", "PAB"}, @@ -148,12 +148,12 @@ public class Helpers { public static String parseCurrency(String text, String def) { text = text != null ? text.toLowerCase() : ""; - for (String currency : currencies) { + for (String currency : CURRENCIES) { if (text.contains(currency)) { return currency; } } - for (String[] symCur : symMappings) { + for (String[] symCur : SYM_MAPPINGS) { if (text.contains(symCur[0])) { return symCur[1]; } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/Bank.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/Bank.java index 18a6a0c..b3f6e97 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/Bank.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/Bank.java @@ -54,14 +54,6 @@ public abstract class Bank implements Comparable, IBankTypes { @DrawableRes private final int logoResource; - protected String TAG = "Bank"; - - protected String NAME = "Bank"; - - protected String NAME_SHORT = "bank"; - - protected int BANKTYPE_ID = 0; - /** * URL for human-accessible web bank. *

@@ -71,43 +63,43 @@ public abstract class Bank implements Comparable, IBankTypes { * @see #isWebViewEnabled() */ @Nullable - protected String URL; + protected String url; - protected int INPUT_TYPE_USERNAME = InputType.TYPE_CLASS_TEXT; + protected int inputTypeUsername = InputType.TYPE_CLASS_TEXT; - protected int INPUT_TYPE_PASSWORD = InputType.TYPE_CLASS_TEXT + protected int inputTypePassword = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD; - protected int INPUT_TYPE_EXTRAS = InputType.TYPE_CLASS_TEXT; + private static final int INPUT_TYPE_EXTRAS = InputType.TYPE_CLASS_TEXT; - protected String INPUT_HINT_USERNAME = null; + protected String inputHintUsername = null; - protected boolean INPUT_HIDDEN_USERNAME = false; + private static final boolean INPUT_HIDDEN_USERNAME = false; - protected boolean INPUT_HIDDEN_PASSWORD = false; + protected boolean inputHiddenPassword = false; - protected boolean INPUT_HIDDEN_EXTRAS = true; + private static final boolean INPUT_HIDDEN_EXTRAS = true; - protected int INPUT_TITLETEXT_USERNAME = R.string.username; + protected int inputTitletextUsername = R.string.username; - protected int INPUT_TITLETEXT_PASSWORD = R.string.password; + private final int INPUT_TITLETEXT_PASSWORD = R.string.password; - protected int INPUT_TITLETEXT_EXTRAS = R.string.extras_field; + private final int INPUT_TITLETEXT_EXTRAS = R.string.extras_field; - protected boolean STATIC_BALANCE = false; + protected boolean staticBalance = false; - protected boolean BROKEN = false; + private static final boolean BROKEN = false; - protected boolean DISPLAY_DECIMALS = true; + protected boolean displayDecimals = true; /** * Whether or not we support opening the web version of a bank. *

* Lots of banks don't have this any more, but have apps instead. * @see #isWebViewEnabled() - * @see #URL + * @see #url */ - protected boolean WEB_VIEW_ENABLED = true; + protected boolean webViewEnabled = true; protected Context context; @@ -229,7 +221,7 @@ public abstract class Bank implements Comparable, IBankTypes { } public BigDecimal getBalance() { - if (STATIC_BALANCE) { + if (staticBalance) { return balance; } else { BigDecimal bal = new BigDecimal(0); @@ -246,13 +238,9 @@ public abstract class Bank implements Comparable, IBankTypes { } } - public int getBanktypeId() { - return BANKTYPE_ID; - } + public abstract int getBanktypeId(); - public String getName() { - return NAME; - } + public abstract String getName(); public String getDisplayName() { if (customName != null && customName.length() > 0) { @@ -278,10 +266,6 @@ public abstract class Bank implements Comparable, IBankTypes { getProperties().put(LegacyProviderConfiguration.EXTRAS, extras); } - public String getShortName() { - return NAME_SHORT; - } - public void setData(BigDecimal balance, boolean disabled, long dbid, String currency, String customName, int hideAccounts) { @@ -314,15 +298,15 @@ public abstract class Bank implements Comparable, IBankTypes { } public String getURL() { - return URL; + return url; } public int getInputTypeUsername() { - return INPUT_TYPE_USERNAME; + return inputTypeUsername; } public int getInputTypePassword() { - return INPUT_TYPE_PASSWORD; + return inputTypePassword; } public int getInputTypeExtras() { @@ -330,7 +314,7 @@ public abstract class Bank implements Comparable, IBankTypes { } public String getInputHintUsername() { - return INPUT_HINT_USERNAME; + return inputHintUsername; } public boolean isInputUsernameHidden() { @@ -338,7 +322,7 @@ public abstract class Bank implements Comparable, IBankTypes { } public boolean isInputPasswordHidden() { - return INPUT_HIDDEN_PASSWORD; + return inputHiddenPassword; } public boolean isInputExtrasHidden() { @@ -346,7 +330,7 @@ public abstract class Bank implements Comparable, IBankTypes { } public int getInputTitleUsername() { - return INPUT_TITLETEXT_USERNAME; + return inputTitletextUsername; } public int getInputTitlePassword() { @@ -361,11 +345,11 @@ public abstract class Bank implements Comparable, IBankTypes { * Whether or not we support opening the web version of a bank. *

* Lots of banks don't have this any more, but have apps instead. - * @see #WEB_VIEW_ENABLED - * @see #URL + * @see #webViewEnabled + * @see #url */ public boolean isWebViewEnabled() { - return URL != null && WEB_VIEW_ENABLED; + return url != null && webViewEnabled; } public Map getProperties() { @@ -455,7 +439,7 @@ public abstract class Bank implements Comparable, IBankTypes { Timber.e(e, "Error getting session package"); } String html = String.format(preloader, - String.format("function go(){window.location=\"%s\" }", this.URL), + String.format("function go(){window.location=\"%s\" }", this.url), // Javascript function "" // HTML ); @@ -467,7 +451,7 @@ public abstract class Bank implements Comparable, IBankTypes { } public boolean getDisplayDecimals() { - return DISPLAY_DECIMALS; + return displayDecimals; } protected Context getContext() { diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AbsIkanoPartner.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AbsIkanoPartner.java index 1abfc23..61ce9dd 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AbsIkanoPartner.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AbsIkanoPartner.java @@ -58,10 +58,10 @@ public abstract class AbsIkanoPartner extends Bank { public AbsIkanoPartner(Context context, @DrawableRes int logoResource) { super(context, logoResource); - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; - super.STATIC_BALANCE = true; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputHintUsername = INPUT_HINT_USERNAME; + super.staticBalance = true; } public AbsIkanoPartner(String username, String password, Context context, @DrawableRes int logoResource) diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AkeliusInvest.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AkeliusInvest.java index f84865b..848b842 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AkeliusInvest.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AkeliusInvest.java @@ -46,8 +46,6 @@ public class AkeliusInvest extends Bank { private static final String NAME = "Akelius Invest"; - private static final String NAME_SHORT = "akeliusinvest"; - private static final String URL = "https://online.akeliusinvest.com/"; private static final int BANKTYPE_ID = IBankTypes.AKELIUSINVEST; @@ -80,14 +78,11 @@ public class AkeliusInvest extends Bank { public AkeliusInvest(Context context) { super(context, R.drawable.logo_akeliusinvest); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; - super.STATIC_BALANCE = STATIC_BALANCE; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputHintUsername = INPUT_HINT_USERNAME; + super.staticBalance = STATIC_BALANCE; } public AkeliusInvest(String username, String password, Context context) throws BankException, @@ -137,6 +132,16 @@ public class AkeliusInvest extends Bank { return urlopen; } + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; + } + @Override public void update() throws BankException, LoginException, BankChoiceException, IOException { super.update(); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AkeliusSpar.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AkeliusSpar.java index 038f0fe..715515e 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AkeliusSpar.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AkeliusSpar.java @@ -47,8 +47,6 @@ public class AkeliusSpar extends Bank { private static final String NAME = "Akelius Spar"; - private static final String NAME_SHORT = "akeliusspar"; - private static final String URL = "https://www.online.akeliusspar.se/"; private static final int BANKTYPE_ID = IBankTypes.AKELIUSSPAR; @@ -81,14 +79,21 @@ public class AkeliusSpar extends Bank { public AkeliusSpar(Context context) { super(context, R.drawable.logo_akeliusspar); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; - super.STATIC_BALANCE = STATIC_BALANCE; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputHintUsername = INPUT_HINT_USERNAME; + super.staticBalance = STATIC_BALANCE; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public AkeliusSpar(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AppeakPoker.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AppeakPoker.java index 4940fed..fd2e258 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AppeakPoker.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AppeakPoker.java @@ -42,8 +42,6 @@ public class AppeakPoker extends Bank { private static final String NAME = "Appeak Poker"; - private static final String NAME_SHORT = "appeakpoker"; - private static final String URL = "http://poker.appeak.se/"; private static final int BANKTYPE_ID = Bank.APPEAKPOKER; @@ -56,16 +54,23 @@ public class AppeakPoker extends Bank { public AppeakPoker(Context context) { super(context, R.drawable.logo_appeakpoker); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_HIDDEN_PASSWORD = INPUT_HIDDEN_PASSWORD; - super.DISPLAY_DECIMALS = false; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputHiddenPassword = INPUT_HIDDEN_PASSWORD; + super.displayDecimals = false; currency = "chips"; } + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; + } + public AppeakPoker(String username, String password, Context context) throws BankException, LoginException, BankChoiceException, IOException { this(context); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AvanzaMini.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AvanzaMini.java index cc859ac..5f7889b 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AvanzaMini.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/AvanzaMini.java @@ -26,9 +26,16 @@ public class AvanzaMini extends Avanza { public AvanzaMini(Context context) { super(context, R.drawable.logo_avanzamini); - NAME = "Avanza Mini"; - NAME_SHORT = "avanzamini"; - URL = "https://www.avanza.se/mini/hem/"; - BANKTYPE_ID = IBankTypes.AVANZAMINI; + url = "https://www.avanza.se/mini/hem/"; + } + + @Override + public int getBanktypeId() { + return IBankTypes.AVANZAMINI; + } + + @Override + public String getName() { + return "Avanza Mini"; } } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BetterGlobe.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BetterGlobe.java index eb46cac..536aafb 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BetterGlobe.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BetterGlobe.java @@ -44,8 +44,6 @@ public class BetterGlobe extends Bank { private static final String NAME = "Better Globe"; - private static final String NAME_SHORT = "betterglobe"; - private static final String URL = "http://betterglobe.com"; private static final int BANKTYPE_ID = IBankTypes.BETTERGLOBE; @@ -70,17 +68,24 @@ public class BetterGlobe extends Bank { public BetterGlobe(Context context) { super(context, R.drawable.logo_betterglobe); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; - super.STATIC_BALANCE = STATIC_BALANCE; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputHintUsername = INPUT_HINT_USERNAME; + super.staticBalance = STATIC_BALANCE; super.currency = "EUR"; } + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; + } + public BetterGlobe(String username, String password, Context context) throws BankException, LoginException, BankChoiceException, IOException { this(context); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bioklubben.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bioklubben.java index df9e368..55336d7 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bioklubben.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bioklubben.java @@ -46,8 +46,6 @@ public class Bioklubben extends Bank { private static final String NAME = "Bioklubben"; - private static final String NAME_SHORT = "bioklubben"; - private static final String URL = "https://bioklubben.sf.se/Start.aspx"; private static final int BANKTYPE_ID = Bank.BIOKLUBBEN; @@ -58,17 +56,24 @@ public class Bioklubben extends Bank { public Bioklubben(Context context) { super(context, R.drawable.logo_bioklubben); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.DISPLAY_DECIMALS = DISPLAY_DECIMALS; - super.INPUT_TYPE_USERNAME = InputType.TYPE_CLASS_TEXT + super.url = URL; + super.displayDecimals = DISPLAY_DECIMALS; + super.inputTypeUsername = InputType.TYPE_CLASS_TEXT | +InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; - super.INPUT_HINT_USERNAME = context.getString(R.string.email); + super.inputHintUsername = context.getString(R.string.email); currency = context.getString(R.string.points); } + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; + } + public Bioklubben(String username, String password, Context context) throws BankException, LoginException, BankChoiceException, IOException { this(context); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BlekingeTrafiken.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BlekingeTrafiken.java index 62d977f..d2af51a 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BlekingeTrafiken.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BlekingeTrafiken.java @@ -42,8 +42,6 @@ public class BlekingeTrafiken extends Bank { private static final String NAME = "Blekingetrafiken"; - private static final String NAME_SHORT = "blekingetrafiken"; - private static final String URL = "https://www.blekingetrafiken.se"; private static final int BANKTYPE_ID = IBankTypes.BLEKINGETRAFIKEN; @@ -52,15 +50,21 @@ public class BlekingeTrafiken extends Bank { public BlekingeTrafiken(Context context) { super(context, R.drawable.logo_blekingetrafiken); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = InputType.TYPE_CLASS_PHONE; - super.INPUT_HINT_USERNAME = "XXXXXXXXXX"; - super.INPUT_TITLETEXT_USERNAME = R.string.card_number; - super.INPUT_HIDDEN_PASSWORD = true; + super.url = URL; + super.inputTypeUsername = InputType.TYPE_CLASS_PHONE; + super.inputHintUsername = "XXXXXXXXXX"; + super.inputTitletextUsername = R.string.card_number; + super.inputHiddenPassword = true; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + @Override + public String getName() { + return NAME; } public BlekingeTrafiken(String username, String password, Context context) diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bredband2VoIP.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bredband2VoIP.java index 60145b8..309ee6a 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bredband2VoIP.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bredband2VoIP.java @@ -63,11 +63,18 @@ public class Bredband2VoIP extends Bank { public Bredband2VoIP(Context context) { super(context, R.drawable.logo_bredband2voip); - NAME = "Bredband2 VoIP"; - NAME_SHORT = "bredband2voip"; - BANKTYPE_ID = IBankTypes.BREDBAND2VOIP; - super.INPUT_TYPE_USERNAME = InputType.TYPE_CLASS_PHONE; - super.INPUT_HINT_USERNAME = "19XXXXXX-XXXX"; + super.inputTypeUsername = InputType.TYPE_CLASS_PHONE; + super.inputHintUsername = "19XXXXXX-XXXX"; + } + + @Override + public int getBanktypeId() { + return IBankTypes.BREDBAND2VOIP; + } + + @Override + public String getName() { + return "Bredband2 VoIP"; } public Bredband2VoIP(String username, String password, Context context) diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BrummerKF.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BrummerKF.java index 7c35408..c821869 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BrummerKF.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/BrummerKF.java @@ -46,8 +46,6 @@ public class BrummerKF extends Bank { private static final String NAME = "Brummer KF & Pension"; - private static final String NAME_SHORT = "brummer_kf"; - private static final String URL = "https://www.brummer.se/"; private static final int BANKTYPE_ID = IBankTypes.BRUMMER_KF; @@ -78,14 +76,21 @@ public class BrummerKF extends Bank { public BrummerKF(Context context) { super(context, R.drawable.logo_brummer_kf); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; - super.STATIC_BALANCE = STATIC_BALANCE; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputHintUsername = INPUT_HINT_USERNAME; + super.staticBalance = STATIC_BALANCE; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public BrummerKF(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/CSN.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/CSN.java index 3640850..1ba7084 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/CSN.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/CSN.java @@ -49,8 +49,6 @@ public class CSN extends Bank { private static final String NAME = "CSN"; - private static final String NAME_SHORT = "csn"; - private static final String URL = "https://www.csn.se/bas/inloggning/pinkod.do"; private static final int BANKTYPE_ID = IBankTypes.CSN; @@ -82,14 +80,21 @@ public class CSN extends Bank { public CSN(Context context) { super(context, R.drawable.logo_csn); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; - super.STATIC_BALANCE = STATIC_BALANCE; + super.url = URL; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputHintUsername = INPUT_HINT_USERNAME; + super.staticBalance = STATIC_BALANCE; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public CSN(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Chalmrest.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Chalmrest.java index 5276b57..d59b8f2 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Chalmrest.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Chalmrest.java @@ -28,8 +28,6 @@ public class Chalmrest extends Bank { private static final String NAME = "Chalmrest"; - private static final String NAME_SHORT = "chalmrest"; - private static final int BANKTYPE_ID = IBankTypes.CHALMREST; private Pattern reViewState = Pattern.compile("__VIEWSTATE\"\\s+value=\"([^\"]+)\""); @@ -47,13 +45,20 @@ public class Chalmrest extends Bank { public Chalmrest(Context context) { super(context, R.drawable.logo_chalmrest); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.INPUT_TITLETEXT_USERNAME = R.string.card_number; - super.INPUT_HINT_USERNAME = "XXXXXXXXXXXXXXXX"; - super.INPUT_TYPE_USERNAME = InputType.TYPE_CLASS_NUMBER; - super.INPUT_HIDDEN_PASSWORD = true; + super.inputTitletextUsername = R.string.card_number; + super.inputHintUsername = "XXXXXXXXXXXXXXXX"; + super.inputTypeUsername = InputType.TYPE_CLASS_NUMBER; + super.inputHiddenPassword = true; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public Chalmrest(String username, String password, Context context) throws BankException, @@ -110,30 +115,30 @@ public class Chalmrest extends Bank { } urlopen = login(); response = urlopen.open("http://kortladdning3.chalmerskonferens.se/CardLoad_Order.aspx"); - Matcher matcher; - Matcher matcher_b; + Matcher accountMatcher; + Matcher balanceMatcher; - matcher = reAccount.matcher(response); - if (matcher.find()) { + accountMatcher = reAccount.matcher(response); + if (accountMatcher.find()) { /* * Capture groups: * GROUP EXAMPLE DATA * 1: Name Kalle Karlsson */ - matcher_b = reBalance.matcher(response); - if (matcher_b.find()) { + balanceMatcher = reBalance.matcher(response); + if (balanceMatcher.find()) { /* * Capture groups: * GROUP EXAMPLE DATA * 1: Balance 118 kr */ - String balanceString = matcher_b.group(1).replaceAll("\\]*>", "") + String balanceString = balanceMatcher.group(1).replaceAll("\\]*>", "") .replaceAll("\\<[^>]*>", "").trim(); - accounts.add(new Account(Html.fromHtml(matcher.group(1)).toString().trim(), - Helpers.parseBalance(balanceString), matcher.group(1))); + accounts.add(new Account(Html.fromHtml(accountMatcher.group(1)).toString().trim(), + Helpers.parseBalance(balanceString), accountMatcher.group(1))); balance = balance.add(Helpers.parseBalance(balanceString)); } } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/DanskeBank.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/DanskeBank.java index af8c51f..4f5e687 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/DanskeBank.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/DanskeBank.java @@ -49,8 +49,6 @@ public class DanskeBank extends Bank { private static final String NAME = "DanskeBank"; - private static final String NAME_SHORT = "danskebank"; - private static final String URL = "https://mobil.danskebank.se/XI?WP=XAI&WO=Logon&WA=MBSELogon&gsSprog=SE&gsBrand=OEB"; @@ -82,13 +80,20 @@ public class DanskeBank extends Bank { public DanskeBank(Context context) { super(context, R.drawable.logo_danskebank); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputHintUsername = INPUT_HINT_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public DanskeBank(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Everydaycard.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Everydaycard.java index 729c222..0e2046d 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Everydaycard.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Everydaycard.java @@ -45,8 +45,6 @@ public class Everydaycard extends Bank { private static final String NAME = "Everydaycard"; - private static final String NAME_SHORT = "everydaycard"; - private static final String URL = "http://www.everydaycard.se/mobil/"; private static final int BANKTYPE_ID = IBankTypes.EVERYDAYCARD; @@ -67,12 +65,19 @@ public class Everydaycard extends Bank { public Everydaycard(Context context) { super(context, R.drawable.logo_everydaycard); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputHintUsername = INPUT_HINT_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public Everydaycard(String username, String password, Context context) diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/FirstCard.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/FirstCard.java index 90a513c..cb8b9a4 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/FirstCard.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/FirstCard.java @@ -46,8 +46,6 @@ public class FirstCard extends Bank { private static final String NAME = "First Card"; - private static final String NAME_SHORT = "firstcard"; - private static final String URL = "https://www.firstcard.se/login.jsp"; private static final int BANKTYPE_ID = IBankTypes.FIRSTCARD; @@ -68,12 +66,19 @@ public class FirstCard extends Bank { public FirstCard(Context context) { super(context, R.drawable.logo_firstcard); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputHintUsername = INPUT_HINT_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public FirstCard(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Hemkop.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Hemkop.java index 8d97593..688d147 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Hemkop.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Hemkop.java @@ -50,8 +50,6 @@ public class Hemkop extends Bank { private static final String NAME = "Hemköp Kundkort"; - private static final String NAME_SHORT = "hemkop"; - private static final String URL = "https://www.hemkop.se/Mina-sidor/Logga-in/"; private static final int BANKTYPE_ID = IBankTypes.HEMKOP; @@ -65,12 +63,19 @@ public class Hemkop extends Bank { public Hemkop(Context context) { super(context, R.drawable.logo_hemkop); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputHintUsername = INPUT_HINT_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public Hemkop(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Hors.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Hors.java index 0930038..3fed434 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Hors.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Hors.java @@ -45,8 +45,6 @@ public class Hors extends Bank { private static final String NAME = "Hörs"; - private static final String NAME_SHORT = "hors"; - private static final String URL = "http://www.dittkort.se/hors/"; private static final int BANKTYPE_ID = IBankTypes.HORS; @@ -58,15 +56,22 @@ public class Hors extends Bank { public Hors(Context context) { super(context, R.drawable.logo_hors); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.DISPLAY_DECIMALS = DISPLAY_DECIMALS; - super.INPUT_TYPE_USERNAME = InputType.TYPE_CLASS_TEXT + super.url = URL; + super.displayDecimals = DISPLAY_DECIMALS; + super.inputTypeUsername = InputType.TYPE_CLASS_TEXT | +InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; - super.INPUT_HINT_USERNAME = context.getString(R.string.card_id); - super.INPUT_HIDDEN_PASSWORD = true; + super.inputHintUsername = context.getString(R.string.card_id); + super.inputHiddenPassword = true; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public Hors(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/IKEA.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/IKEA.java index 3677f55..85255d6 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/IKEA.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/IKEA.java @@ -30,8 +30,6 @@ public class IKEA extends AbsIkanoPartner { private static final String NAME = "IKEA HANDLA kort"; - private static final String NAME_SHORT = "ikea"; - private static final String URL = "https://partner.ikanobank.se/web/engines/page.aspx?structid=1420"; @@ -40,13 +38,20 @@ public class IKEA extends AbsIkanoPartner { public IKEA(Context context) { super(context, R.drawable.logo_ikea); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; + super.url = URL; this.structId = "1420"; } + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; + } + public IKEA(String username, String password, Context context) throws BankException, LoginException, BankChoiceException, IOException { this(context); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/IkanoBank.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/IkanoBank.java index 9ea837a..b8de01c 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/IkanoBank.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/IkanoBank.java @@ -47,8 +47,6 @@ public class IkanoBank extends Bank { private static final String NAME = "Ikano Bank"; - private static final String NAME_SHORT = "ikanobank"; - private static final String URL = "https://secure.ikanobank.se/engines/page.aspx?structid=1895"; private static final int BANKTYPE_ID = IBankTypes.IKANOBANK; @@ -81,13 +79,20 @@ public class IkanoBank extends Bank { public IkanoBank(Context context) { super(context, R.drawable.logo_ikanobank); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputHintUsername = INPUT_HINT_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public IkanoBank(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Jojo.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Jojo.java index fddf1bb..0b1d3ed 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Jojo.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Jojo.java @@ -48,8 +48,6 @@ public class Jojo extends Bank { private static final String NAME = "Jojo Reskassa"; - private static final String NAME_SHORT = "jojo"; - private static final String URL = "https://www.skanetrafiken.se"; private static final int BANKTYPE_ID = IBankTypes.JOJO; @@ -64,12 +62,19 @@ public class Jojo extends Bank { public Jojo(Context context) { super(context, R.drawable.logo_jojo); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TITLETEXT_USERNAME = R.string.email; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; + super.url = URL; + super.inputTitletextUsername = R.string.email; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public Jojo(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/McDonalds.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/McDonalds.java index 1aeefd3..2fb50b7 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/McDonalds.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/McDonalds.java @@ -45,8 +45,6 @@ public class McDonalds extends Bank { private static final String NAME = "McDonald's Presentkort"; - private static final String NAME_SHORT = "mcdonalds"; - private static final String URL = "http://apps.mcdonalds.se/sweden/giftquer.nsf/egift?OpenForm"; private static final int BANKTYPE_ID = Bank.MCDONALDS; @@ -67,13 +65,20 @@ public class McDonalds extends Bank { public McDonalds(Context context) { super(context, R.drawable.logo_mcdonalds); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_HIDDEN_PASSWORD = INPUT_HIDDEN_PASSWORD; - super.INPUT_TITLETEXT_USERNAME = INPUT_TITLETEXT_USERNAME; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputHiddenPassword = INPUT_HIDDEN_PASSWORD; + super.inputTitletextUsername = INPUT_TITLETEXT_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public McDonalds(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Meniga.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Meniga.java index 9b20da6..c383409 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Meniga.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Meniga.java @@ -32,8 +32,6 @@ public class Meniga extends Bank { private static final String NAME = "Meniga"; - private static final String NAME_SHORT = "meniga"; - private static final String URL = "https://www.meniga.is/"; private static final int BANKTYPE_ID = IBankTypes.MENIGA; @@ -53,15 +51,22 @@ public class Meniga extends Bank { public Meniga(Context context) { super(context, R.drawable.logo_meniga); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputHintUsername = INPUT_HINT_USERNAME; super.setCurrency("ISK"); } + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; + } + public Meniga(String username, String password, Context context) throws BankException, LoginException, BankChoiceException, IOException { this(context); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/MinPension.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/MinPension.java index b6f3cbb..ab68bfa 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/MinPension.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/MinPension.java @@ -48,13 +48,19 @@ public class MinPension extends Bank { public MinPension(Context context) { super(context, R.drawable.logo_minpension); - TAG = "MinPension"; - NAME = "Min Pension.se"; - NAME_SHORT = "minpension"; - BANKTYPE_ID = IBankTypes.MINPENSION; - INPUT_TYPE_USERNAME = InputType.TYPE_CLASS_PHONE; - INPUT_TYPE_PASSWORD = InputType.TYPE_CLASS_PHONE | InputType.TYPE_TEXT_VARIATION_PASSWORD; - INPUT_HINT_USERNAME = res.getText(R.string.pno).toString(); + inputTypeUsername = InputType.TYPE_CLASS_PHONE; + inputTypePassword = InputType.TYPE_CLASS_PHONE | InputType.TYPE_TEXT_VARIATION_PASSWORD; + inputHintUsername = res.getText(R.string.pno).toString(); + } + + @Override + public int getBanktypeId() { + return IBankTypes.MINPENSION; + } + + @Override + public String getName() { + return "Min Pension.se"; } public MinPension(String username, String password, Context context) @@ -128,8 +134,8 @@ public class MinPension extends Bank { super.updateComplete(); } - private Account updateAccount(String URL, String selector, String name) throws IOException { - String response = urlopen.open(URL); + private Account updateAccount(String url, String selector, String name) throws IOException { + String response = urlopen.open(url); Document dResponse = Jsoup.parse(response); List transactions = new ArrayList<>(); String institute = ""; diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Nordnet.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Nordnet.java index 7552aa9..8925dc6 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Nordnet.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Nordnet.java @@ -48,8 +48,6 @@ public class Nordnet extends Bank { private static final String NAME = "Nordnet"; - private static final String NAME_SHORT = "nordnet"; - private static final String URL = "https://www.nordnet.se/mux/login/startSE.html"; private static final int BANKTYPE_ID = IBankTypes.NORDNET; @@ -66,10 +64,17 @@ public class Nordnet extends Bank { public Nordnet(Context context) { super(context, R.drawable.logo_nordnet); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; + super.url = URL; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public Nordnet(String username, String password, Context context) throws BankException, @@ -128,32 +133,32 @@ public class Nordnet extends Bank { throw new LoginException(res.getText(R.string.invalid_username_password).toString()); } urlopen = login(); - Matcher matcher = reAccounts.matcher(response); - Matcher matcher_b = reBalance.matcher(response); - while (matcher.find()) { + Matcher accountMatcher = reAccounts.matcher(response); + Matcher balanceMatcher = reBalance.matcher(response); + while (accountMatcher.find()) { /* * Capture groups: * GROUP EXAMPLE DATA * 1: Account name and number Investeringssparkonto 1234567 | Sparkonto 1234 567890 1 * */ - if (matcher_b.find()) { + if (balanceMatcher.find()) { /* * Capture groups: * GROUP EXAMPLE DATA * 1: Account balance 62 356 | 0 * */ - Account account = new Account(Html.fromHtml(matcher.group(1)).toString().trim(), - Helpers.parseBalance(matcher_b.group(1)), - Html.fromHtml(matcher.group(1)).toString().trim().replaceAll(" ", "")); + Account account = new Account(Html.fromHtml(accountMatcher.group(1)).toString().trim(), + Helpers.parseBalance(balanceMatcher.group(1)), + Html.fromHtml(accountMatcher.group(1)).toString().trim().replaceAll(" ", "")); // Saving accounts contain white space characters in the account number - if (!matcher.group(1).trim().contains(" ")) { + if (!accountMatcher.group(1).trim().contains(" ")) { account.setType(Account.FUNDS); } accounts.add(account); - balance = balance.add(Helpers.parseBalance(matcher_b.group(1))); + balance = balance.add(Helpers.parseBalance(balanceMatcher.group(1))); } } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/OKQ8.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/OKQ8.java index c7fa006..540abd3 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/OKQ8.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/OKQ8.java @@ -47,8 +47,6 @@ public class OKQ8 extends Bank { private static final String NAME = "OKQ8 VISA"; - private static final String NAME_SHORT = "okq8"; - private static final String URL = "https://nettbank.edb.com/Logon/index.jsp?domain=0066&from_page=http://www.okq8.se&to_page=https://nettbank.edb.com/cardpayment/transigo/logon/done/okq8"; @@ -76,13 +74,20 @@ public class OKQ8 extends Bank { public OKQ8(Context context) { super(context, R.drawable.logo_okq8); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; - super.STATIC_BALANCE = STATIC_BALANCE; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputHintUsername = INPUT_HINT_USERNAME; + super.staticBalance = STATIC_BALANCE; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public OKQ8(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Ostgotatrafiken.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Ostgotatrafiken.java index 8524e38..d017230 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Ostgotatrafiken.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Ostgotatrafiken.java @@ -43,8 +43,6 @@ public class Ostgotatrafiken extends Bank { private static final String NAME = "Östgötatrafiken"; - private static final String NAME_SHORT = "ogt"; - private static final int BANKTYPE_ID = IBankTypes.OSTGOTATRAFIKEN; private Pattern reViewState = Pattern.compile( @@ -68,10 +66,16 @@ public class Ostgotatrafiken extends Bank { public Ostgotatrafiken(Context context) { super(context, R.drawable.logo_ogt); + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; + @Override + public String getName() { + return NAME; } public Ostgotatrafiken(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Osuuspankki.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Osuuspankki.java index c597d44..f62b92a 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Osuuspankki.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Osuuspankki.java @@ -45,8 +45,6 @@ public class Osuuspankki extends Bank { private static final String NAME = "Osuuspankki"; - private static final String NAME_SHORT = "osuuspankki"; - private static final String URL = "https://www.op.fi/op?kielikoodi=sv"; private static final int BANKTYPE_ID = IBankTypes.OSUUSPANKKI; @@ -62,11 +60,17 @@ public class Osuuspankki extends Bank { public Osuuspankki(Context context) { super(context, R.drawable.logo_osuuspankki); + super.url = URL; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; + @Override + public String getName() { + return NAME; } public Osuuspankki(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Payson.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Payson.java index f90ae79..d98a263 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Payson.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Payson.java @@ -46,8 +46,6 @@ public class Payson extends Bank { private static final String NAME = "Payson"; - private static final String NAME_SHORT = "payson"; - private static final String URL = "https://www.payson.se/signin/"; private static final int BANKTYPE_ID = IBankTypes.PAYSON; @@ -66,11 +64,18 @@ public class Payson extends Bank { public Payson(Context context) { super(context, R.drawable.logo_payson); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public Payson(String username, String password, Context context) throws BankChoiceException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/PlusGirot.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/PlusGirot.java index a4da2e4..8e1ee5b 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/PlusGirot.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/PlusGirot.java @@ -45,8 +45,6 @@ public class PlusGirot extends Bank { private static final String NAME = "PlusGirot"; - private static final String NAME_SHORT = "plusgirot"; - private static final String URL = "https://kontoutdrag.plusgirot.se/"; private static final int BANKTYPE_ID = IBankTypes.PLUSGIROT; @@ -63,11 +61,17 @@ public class PlusGirot extends Bank { public PlusGirot(Context context) { super(context, R.drawable.logo_plusgirot); + super.url = URL; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; + @Override + public String getName() { + return NAME; } public PlusGirot(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SevenDay.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SevenDay.java index 490046a..9990b50 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SevenDay.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SevenDay.java @@ -45,8 +45,6 @@ public class SevenDay extends Bank { private static final String NAME = "SevenDay"; - private static final String NAME_SHORT = "sevenday"; - private static final String URL = "https://www.sevenday.se/mina-sidor/mina-sidor.htm"; private static final int BANKTYPE_ID = IBankTypes.SEVENDAY; @@ -67,12 +65,19 @@ public class SevenDay extends Bank { public SevenDay(Context context) { super(context, R.drawable.logo_sevenday); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputHintUsername = INPUT_HINT_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public SevenDay(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SveaDirekt.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SveaDirekt.java index 9aef3df..2a3566d 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SveaDirekt.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SveaDirekt.java @@ -32,8 +32,6 @@ public class SveaDirekt extends Bank { private static final String NAME = "Svea Direkt"; - private static final String NAME_SHORT = "sveadirekt"; - private static final String URL = "https://http://www.sveadirekt.com/sv/swe//"; private static final int BANKTYPE_ID = IBankTypes.SVEADIREKT; @@ -61,13 +59,20 @@ public class SveaDirekt extends Bank { public SveaDirekt(Context context) { super(context, R.drawable.logo_sveadirekt); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.URL = URL; - super.BANKTYPE_ID = BANKTYPE_ID; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputHintUsername = INPUT_HINT_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public SveaDirekt(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SvenskaSpel.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SvenskaSpel.java index 85d795c..b81cdc2 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SvenskaSpel.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SvenskaSpel.java @@ -45,8 +45,6 @@ public class SvenskaSpel extends Bank { private static final String NAME = "Svenska Spel"; - private static final String NAME_SHORT = "svenskaspel"; - private static final String URL = "https://api.www.svenskaspel.se/player/sessions"; private static final int BANKTYPE_ID = Bank.SVENSKASPEL; @@ -62,12 +60,19 @@ public class SvenskaSpel extends Bank { public SvenskaSpel(Context context) { super(context, R.drawable.logo_svenskaspel); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TITLETEXT_USERNAME = INPUT_TITLETEXT_USERNAME; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTitletextUsername = INPUT_TITLETEXT_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public SvenskaSpel(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/TestBank.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/TestBank.java index 2ceec08..a3c3a5e 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/TestBank.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/TestBank.java @@ -43,8 +43,6 @@ public class TestBank extends Bank { private static final String NAME = "Testbank"; - private static final String NAME_SHORT = "testbank"; - private static final String URL = "http://www.nullbyte.eu/"; private static final int BANKTYPE_ID = IBankTypes.TESTBANK; @@ -70,13 +68,20 @@ public class TestBank extends Bank { public TestBank(Context context) { super(context, R.drawable.logo_bankdroid); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputHintUsername = INPUT_HINT_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public TestBank(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/TicketRikskortet.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/TicketRikskortet.java index ed68966..2c370fa 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/TicketRikskortet.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/TicketRikskortet.java @@ -44,8 +44,6 @@ public class TicketRikskortet extends Bank { private static final String NAME = "Ticket Rikskortet"; - private static final String NAME_SHORT = "rikskortet"; - private static final String URL = "https://www.edenred.se/rikskuponger/mina-sidor/logga-in/"; private static final String URL_OVERVIEW = "https://www.edenred.se/rikskuponger/mina-sidor/employee/start/"; @@ -71,10 +69,17 @@ public class TicketRikskortet extends Bank { public TicketRikskortet(Context context) { super(context, R.drawable.logo_rikskortet); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; + super.url = URL; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public TicketRikskortet(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Vasttrafik.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Vasttrafik.java index 3c51255..0668464 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Vasttrafik.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Vasttrafik.java @@ -44,8 +44,6 @@ public class Vasttrafik extends Bank { private static final String NAME = "Västtrafik"; - private static final String NAME_SHORT = "vasttrafik"; - private static final String URL = "https://www.vasttrafik.se/mina-sidor/"; private static final int BANKTYPE_ID = IBankTypes.VASTTRAFIK; @@ -64,11 +62,17 @@ public class Vasttrafik extends Bank { public Vasttrafik(Context context) { super(context, R.drawable.logo_vasttrafik); + super.url = URL; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; + @Override + public String getName() { + return NAME; } public Vasttrafik(String username, String password, Context context) throws BankException, @@ -127,11 +131,11 @@ public class Vasttrafik extends Bank { } urlopen = login(); response = urlopen.open("https://www.vasttrafik.se/mina-sidor-inloggad/mina-kort/"); - Matcher matcher; - Matcher matcher_b; + Matcher accountMatcher; + Matcher balanceMatcher; - matcher = reAccounts.matcher(response); - while (matcher.find()) { + accountMatcher = reAccounts.matcher(response); + while (accountMatcher.find()) { /* * Capture groups: * GROUP EXAMPLE DATA @@ -139,12 +143,12 @@ public class Vasttrafik extends Bank { * 2: Balance information */ - if ("".equals(matcher.group(1))) { + if ("".equals(accountMatcher.group(1))) { continue; } - matcher_b = reBalance.matcher(matcher.group(2)); - if (matcher_b.find()) { + balanceMatcher = reBalance.matcher(accountMatcher.group(2)); + if (balanceMatcher.find()) { /* * Capture groups: * GROUP EXAMPLE DATA @@ -152,11 +156,11 @@ public class Vasttrafik extends Bank { * 2: Amount 592,80 kr */ - String balanceString = matcher_b.group(2).replaceAll("\\]*>", "") + String balanceString = balanceMatcher.group(2).replaceAll("\\]*>", "") .replaceAll("\\<[^>]*>", "").trim(); - accounts.add(new Account(Html.fromHtml(matcher.group(1)).toString().trim(), - Helpers.parseBalance(balanceString), matcher.group(1))); + accounts.add(new Account(Html.fromHtml(accountMatcher.group(1)).toString().trim(), + Helpers.parseBalance(balanceString), accountMatcher.group(1))); balance = balance.add(Helpers.parseBalance(balanceString)); } } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Zidisha.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Zidisha.java index a7b13e1..ab4cef3 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Zidisha.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Zidisha.java @@ -44,8 +44,6 @@ public class Zidisha extends Bank { private static final String NAME = "Zidisha"; - private static final String NAME_SHORT = "zidisha"; - private static final String URL = "https://www.zidisha.org/"; private static final int BANKTYPE_ID = IBankTypes.ZIDISHA; @@ -70,17 +68,24 @@ public class Zidisha extends Bank { public Zidisha(Context context) { super(context, R.drawable.logo_zidisha); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; - super.STATIC_BALANCE = STATIC_BALANCE; + super.url = URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputHintUsername = INPUT_HINT_USERNAME; + super.staticBalance = STATIC_BALANCE; super.currency = "USD"; } + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; + } + public Zidisha(String username, String password, Context context) throws BankException, LoginException, BankChoiceException, IOException { this(context); @@ -99,14 +104,14 @@ public class Zidisha extends Bank { throw new BankException( res.getText(R.string.unable_to_find).toString() + " user_guess."); } - String user_guess = mUserGuess.group(1); + String userGuess = mUserGuess.group(1); List postData = new ArrayList(); postData.add(new BasicNameValuePair("username", getUsername())); postData.add(new BasicNameValuePair("password", getPassword())); postData.add(new BasicNameValuePair("textpassword", getUsername())); postData.add(new BasicNameValuePair("userlogin", "")); - postData.add(new BasicNameValuePair("user_guess", user_guess)); + postData.add(new BasicNameValuePair("user_guess", userGuess)); return new LoginPackage(urlopen, postData, response, "https://www.zidisha.org/process.php"); } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/AmericanExpress.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/AmericanExpress.java index 933ec1f..c853116 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/AmericanExpress.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/AmericanExpress.java @@ -52,12 +52,8 @@ import eu.nullbyte.android.urllib.Urllib; public class AmericanExpress extends Bank { - private static final String TAG = "AmericanExpress"; - private static final String NAME = "American Express"; - private static final String NAME_SHORT = "americanexpress"; - private static final int BANKTYPE_ID = IBankTypes.AMERICANEXPRESS; private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -67,11 +63,17 @@ public class AmericanExpress extends Bank { public AmericanExpress(Context context) { super(context, R.drawable.logo_americanexpress); - super.TAG = TAG; - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.WEB_VIEW_ENABLED = false; + super.webViewEnabled = false; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public AmericanExpress(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/Avanza.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/Avanza.java index 1e6bf30..3d3aa64 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/Avanza.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/Avanza.java @@ -57,11 +57,17 @@ public class Avanza extends Bank { protected Avanza(Context context, @DrawableRes int logoResource) { super(context, logoResource); - TAG = "Avanza"; - NAME = "Avanza"; - NAME_SHORT = "avanza"; - URL = "https://iphone.avanza.se"; - BANKTYPE_ID = IBankTypes.AVANZA; + url = "https://iphone.avanza.se"; + } + + @Override + public int getBanktypeId() { + return IBankTypes.AVANZA; + } + + @Override + public String getName() { + return "Avanza"; } public Avanza(Context context) { @@ -72,12 +78,12 @@ public class Avanza extends Bank { protected LoginPackage preLogin() throws BankException, IOException { urlopen = new Urllib(context, CertificateReader.getCertificates(context, R.raw.cert_avanza)); - urlopen.addHeader("Referer", URL + "/start"); + urlopen.addHeader("Referer", url + "/start"); List postData = new ArrayList(); postData.add(new BasicNameValuePair("j_username", getUsername())); postData.add(new BasicNameValuePair("j_password", getPassword())); - postData.add(new BasicNameValuePair("url", URL + "/start")); - String response = urlopen.open(URL + "/ab/handlelogin", postData); + postData.add(new BasicNameValuePair("url", url + "/start")); + String response = urlopen.open(url + "/ab/handlelogin", postData); String homeUrl = ""; try { JSONObject jsonResponse = new JSONObject(response); @@ -86,7 +92,7 @@ public class Avanza extends Bank { throw new BankException( res.getText(R.string.unable_to_find).toString() + " login link.", e); } - LoginPackage lp = new LoginPackage(urlopen, postData, "", URL + homeUrl); + LoginPackage lp = new LoginPackage(urlopen, postData, "", url + homeUrl); lp.setIsLoggedIn(true); return lp; } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/bitcoin/Bitcoin.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/bitcoin/Bitcoin.java index c726896..9efdf1d 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/bitcoin/Bitcoin.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/bitcoin/Bitcoin.java @@ -40,8 +40,6 @@ public class Bitcoin extends Bank { private static final String NAME = "Bitcoin"; - private static final String NAME_SHORT = "bitcoin"; - private static final String URL = "http://blockchain.info"; private static final int BANKTYPE_ID = IBankTypes.BITCOIN; @@ -59,14 +57,21 @@ public class Bitcoin extends Bank { public Bitcoin(Context context) { super(context, R.drawable.logo_bitcoin); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.STATIC_BALANCE = STATIC_BALANCE; + super.url = URL; + super.staticBalance = STATIC_BALANCE; super.currency = "BTC"; - super.INPUT_HIDDEN_PASSWORD = INPUT_HIDDEN_PASSWORD; - super.INPUT_TITLETEXT_USERNAME = INPUT_TITLETEXT_USERNAME; + super.inputHiddenPassword = INPUT_HIDDEN_PASSWORD; + super.inputTitletextUsername = INPUT_TITLETEXT_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public Bitcoin(String username, String password, Context context) diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/coop/Coop.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/coop/Coop.java index 1ac76d6..7c27950 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/coop/Coop.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/coop/Coop.java @@ -57,8 +57,6 @@ public class Coop extends Bank { private static final String NAME = "Coop"; - private static final String NAME_SHORT = "coop"; - private static final String URL = "https://www.coop.se/mina-sidor/oversikt/"; private static final int BANKTYPE_ID = IBankTypes.COOP; @@ -83,11 +81,18 @@ public class Coop extends Bank { public Coop(Context context) { super(context, R.drawable.logo_coop); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.STATIC_BALANCE = true; + super.url = URL; + super.staticBalance = true; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public Coop(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/ica/ICA.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/ica/ICA.java index ffc18a5..ebdb705 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/ica/ICA.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/ica/ICA.java @@ -66,19 +66,25 @@ public class ICA extends Bank { public ICA(Context context) { super(context, R.drawable.logo_ica); - TAG = "ICA"; - NAME = "ICA"; - NAME_SHORT = "ica"; - URL = "http://mobil.ica.se/"; - BANKTYPE_ID = IBankTypes.ICA; - INPUT_TYPE_USERNAME = InputType.TYPE_CLASS_PHONE; - INPUT_TYPE_PASSWORD = InputType.TYPE_CLASS_PHONE; - INPUT_HINT_USERNAME = "ÅÅMMDDXXXX"; + url = "http://mobil.ica.se/"; + inputTypeUsername = InputType.TYPE_CLASS_PHONE; + inputTypePassword = InputType.TYPE_CLASS_PHONE; + inputHintUsername = "ÅÅMMDDXXXX"; mHeaders.put(AUTHENTICATION_TICKET_HEADER, null); mHeaders.put(SESSION_TICKET_HEADER, null); mHeaders.put(LOGOUT_KEY_HEADER, null); } + @Override + public int getBanktypeId() { + return IBankTypes.ICA; + } + + @Override + public String getName() { + return "ICA"; + } + public ICA(String username, String password, Context context) throws BankException, LoginException, BankChoiceException, IOException { this(context); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java index b825abf..97e4b46 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java @@ -64,8 +64,6 @@ public class Lansforsakringar extends Bank { private static final String NAME = "Länsförsäkringar"; - private static final String NAME_SHORT = "lansforsakringar"; - private static final int BANKTYPE_ID = IBankTypes.LANSFORSAKRINGAR; private static final int INPUT_TYPE_USERNAME = InputType.TYPE_CLASS_PHONE; @@ -83,17 +81,24 @@ public class Lansforsakringar extends Bank { public Lansforsakringar(Context context) { super(context, R.drawable.logo_lansforsakringar); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputHintUsername = INPUT_HINT_USERNAME; mObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mObjectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false); mObjectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); } + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; + } + public Urllib login() throws LoginException, BankException, IOException { urlopen = new Urllib(context, CertificateReader.getCertificates(context, R.raw.cert_lansforsakringar)); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/nordea/Nordea.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/nordea/Nordea.java index 24dbb7e..cf8dd19 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/nordea/Nordea.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/nordea/Nordea.java @@ -46,8 +46,6 @@ public class Nordea extends Bank { private static final String NAME = "Nordea"; - private static final String NAME_SHORT = "nordea"; - private static final String BASE_URL = "https://internetbanken.privat.nordea.se/nsp/"; private static final String LOGIN_URL = BASE_URL + "login"; @@ -210,13 +208,20 @@ public class Nordea extends Bank { public Nordea(Context context) { super(context, R.drawable.logo_nordea); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = BASE_URL; - super.INPUT_TYPE_USERNAME = INPUT_TYPE_USERNAME; - super.INPUT_TYPE_PASSWORD = INPUT_TYPE_PASSWORD; - super.INPUT_HINT_USERNAME = INPUT_HINT_USERNAME; + super.url = BASE_URL; + super.inputTypeUsername = INPUT_TYPE_USERNAME; + super.inputTypePassword = INPUT_TYPE_PASSWORD; + super.inputHintUsername = INPUT_HINT_USERNAME; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public Nordea(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/rikslunchen/Rikslunchen.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/rikslunchen/Rikslunchen.java index 1d58f40..9b08924 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/rikslunchen/Rikslunchen.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/rikslunchen/Rikslunchen.java @@ -40,8 +40,6 @@ public class Rikslunchen extends Bank { private static final String NAME = "Rikslunchen"; - private static final String NAME_SHORT = "rikslunchen"; - private static final String URL = "http://www.rikslunchen.se/index.html"; private static final int BANKTYPE_ID = Bank.RIKSLUNCHEN; @@ -53,13 +51,20 @@ public class Rikslunchen extends Bank { public Rikslunchen(Context context) { super(context, R.drawable.logo_rikslunchen); - super.NAME = NAME; - super.NAME_SHORT = NAME_SHORT; - super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; - super.INPUT_TYPE_USERNAME = InputType.TYPE_CLASS_PHONE; - super.INPUT_TITLETEXT_USERNAME = R.string.card_id; - super.INPUT_HIDDEN_PASSWORD = true; + super.url = URL; + super.inputTypeUsername = InputType.TYPE_CLASS_PHONE; + super.inputTitletextUsername = R.string.card_id; + super.inputHiddenPassword = true; + } + + @Override + public int getBanktypeId() { + return BANKTYPE_ID; + } + + @Override + public String getName() { + return NAME; } public Rikslunchen(String username, String password, Context context) throws BankException, diff --git bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java index 2636816..36354b6 100644 --- bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java +++ bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java @@ -88,9 +88,9 @@ import timber.log.Timber; public class Urllib { - private static int MAX_RETRIES = 5; + private final static int MAX_RETRIES = 5; - public static String DEFAULT_USER_AGENT + public final static String DEFAULT_USER_AGENT = "Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17"; private String userAgent = null; diff --git config/quality/lint/lint.xml config/quality/lint/lint.xml index f81c4ca..8195ce2 100644 --- config/quality/lint/lint.xml +++ config/quality/lint/lint.xml @@ -30,7 +30,6 @@ - diff --git config/quality/pmd/pmd-ruleset.xml config/quality/pmd/pmd-ruleset.xml index 05b8f94..68eee4b 100644 --- config/quality/pmd/pmd-ruleset.xml +++ config/quality/pmd/pmd-ruleset.xml @@ -113,7 +113,6 @@ - @@ -139,6 +138,5 @@ - commit e1cbce765867ba88c0bf9c1931a4e47f34dacf44 Author: Johan Walles Date: Thu Nov 3 21:01:58 2016 +0100 If asked for an unknown bank, log the id So instead of getting in Crashlytics... BankType id not found. ... we now get... BankType id not found: 1234 This way it will be easier to understand why we get these exceptions and how they should be handled (or not). diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankFactory.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankFactory.java index d2a8b11..f95ef32 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankFactory.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankFactory.java @@ -144,7 +144,7 @@ public class LegacyBankFactory { case IBankTypes.HORS: return new Hors(context); default: - throw new BankException("BankType id not found."); + throw new BankException("BankType id not found: " + id); } } commit de72b761b9a9a84ea49fb4036e942b24ca49cd5e Author: Johan Walles Date: Thu Nov 3 21:21:31 2016 +0100 Don't lose the stacks when re-throwing exceptions diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Payson.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Payson.java index 4f2d1ae..f90ae79 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Payson.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Payson.java @@ -108,7 +108,7 @@ public class Payson extends Bank { userInfo = new JSONObject( urlopen.open("https://www.payson.se/myaccount/user/getuserinfo")); } catch (JSONException e) { - throw new LoginException(res.getText(R.string.invalid_username_password).toString()); + throw new LoginException(res.getText(R.string.invalid_username_password).toString(), e); } return urlopen; } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/Avanza.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/Avanza.java index e0ee2d8..1e6bf30 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/Avanza.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/avanza/Avanza.java @@ -84,7 +84,7 @@ public class Avanza extends Bank { homeUrl = jsonResponse.getString("redirectUrl"); } catch (JSONException e) { throw new BankException( - res.getText(R.string.unable_to_find).toString() + " login link."); + res.getText(R.string.unable_to_find).toString() + " login link.", e); } LoginPackage lp = new LoginPackage(urlopen, postData, "", URL + homeUrl); lp.setIsLoggedIn(true); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/bitcoin/Bitcoin.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/bitcoin/Bitcoin.java index 0f789ff..c726896 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/bitcoin/Bitcoin.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/bitcoin/Bitcoin.java @@ -96,7 +96,7 @@ public class Bitcoin extends Bank { setCurrency("BTC"); } catch (JsonParseException e) { throw new BankException(res.getText( - R.string.invalid_bitcoin_address).toString()); + R.string.invalid_bitcoin_address).toString(), e); } return urlopen; diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java index 7e8fe82..b825abf 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java @@ -115,7 +115,7 @@ public class Lansforsakringar extends Bank { objectAsJson(new LoginRequest(getUsername(), getPassword())), LoginResponse.class); urlopen.addHeader("Utoken", lr.getTicket()); } catch (Exception e) { - throw new LoginException(res.getText(R.string.invalid_username_password).toString()); + throw new LoginException(res.getText(R.string.invalid_username_password).toString(), e); } return urlopen; } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/exceptions/LoginException.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/exceptions/LoginException.java index 65446a9..67895b3 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/exceptions/LoginException.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/exceptions/LoginException.java @@ -23,4 +23,8 @@ public class LoginException extends Exception { public LoginException(String message) { super(message); } + + public LoginException(String message, Exception cause) { + super(message, cause); + } } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/StringUtils.java bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/StringUtils.java index f85b8f2..6656ba3 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/StringUtils.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/StringUtils.java @@ -13,7 +13,7 @@ public class StringUtils { try { return string.getBytes(CHARSET); } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Internal error"); + throw new RuntimeException("Internal error", e); } } @@ -21,7 +21,7 @@ public class StringUtils { try { return new String(bytes, CHARSET); } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Internal error"); + throw new RuntimeException("Internal error", e); } } } diff --git bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertPinningSSLSocketFactory.java bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertPinningSSLSocketFactory.java index 616084c..bbae0ee 100644 --- bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertPinningSSLSocketFactory.java +++ bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertPinningSSLSocketFactory.java @@ -86,7 +86,7 @@ public class CertPinningSSLSocketFactory extends SSLSocketFactory { context.init(keyManagers, new TrustManager[]{mTrustManager}, null); return context; } catch (Exception e) { - throw new IOException(e.getMessage()); + throw new IOException(e.getMessage(), e); } } diff --git config/quality/pmd/pmd-ruleset.xml config/quality/pmd/pmd-ruleset.xml index 288976e..05b8f94 100644 --- config/quality/pmd/pmd-ruleset.xml +++ config/quality/pmd/pmd-ruleset.xml @@ -104,7 +104,6 @@ - commit 35a0c501384bd9bd99f998d48fd5e2cd1b32fa5e (tag: v1.9.13) Author: Mathias Åhsberg Date: Thu Nov 3 20:32:14 2016 +0100 Creates release v1.9.13 diff --git CHANGELOG CHANGELOG index f2f5b8d..91dd35a 100644 --- CHANGELOG +++ CHANGELOG @@ -1,5 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. +v1.9.13 (2016-11-03) +* Fixes Crashlytics logging issue + v1.9.12 (2016-11-02) * Bioklubben: Use https. It's secure and http page no longer exists. * Östgötatrafiken: Adapt to new login page (Facebook login not supported) commit 709cf1fa638b955646f76775592a433b0e4ad707 Author: Johan Walles Date: Thu Nov 3 07:30:07 2016 +0100 Log correct bank names to Crashlytics diff --git app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java index bea4936..e5c019d 100644 --- app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java +++ app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java @@ -56,7 +56,7 @@ public class LoggingUtils { } logCustom(new CustomEvent("Disabled Bank"). - putCustomAttribute("Name", bank.getDisplayName())); + putCustomAttribute("Name", bank.getName())); } public static void logBankUpdate(Bank bank, boolean withTransactions) { @@ -65,7 +65,7 @@ public class LoggingUtils { } logCustom(new CustomEvent("Bank Updated"). - putCustomAttribute("Name", bank.getDisplayName()). + putCustomAttribute("Name", bank.getName()). putCustomAttribute("With Transactions", Boolean.toString(withTransactions))); boolean hasTransactions = false; @@ -76,7 +76,7 @@ public class LoggingUtils { } if (withTransactions && !hasTransactions) { logCustom(new CustomEvent("Bank Without Transactions"). - putCustomAttribute("Name", bank.getDisplayName())); + putCustomAttribute("Name", bank.getName())); } } commit c60033a4f1f18446e0c298147764af052f90e0a6 (tag: v1.9.12) Author: Mathias Åhsberg Date: Wed Nov 2 22:25:10 2016 +0100 Create release v1.9.12 diff --git CHANGELOG CHANGELOG index 5ebd411..f2f5b8d 100644 --- CHANGELOG +++ CHANGELOG @@ -1,8 +1,9 @@ Please view this file on the master branch, on stable branches it's out of date. -Not yet released +v1.9.12 (2016-11-02) * Bioklubben: Use https. It's secure and http page no longer exists. * Östgötatrafiken: Adapt to new login page (Facebook login not supported) +* Removes broken encryption v1.9.11 (2016-10-26) * Warn about disabled banks in the transactions list commit 1e50923e7b4092101945b4f73b27e82d1cff69f0 Author: Mathias Åhsberg Date: Wed Nov 2 22:21:59 2016 +0100 Update certificates diff --git bankdroid-legacy/src/main/res/raw/cert_danskebank.pem bankdroid-legacy/src/main/res/raw/cert_danskebank.pem index 328a9ec..8a9fe0a 100644 --- bankdroid-legacy/src/main/res/raw/cert_danskebank.pem +++ bankdroid-legacy/src/main/res/raw/cert_danskebank.pem @@ -1,31 +1,32 @@ -----BEGIN CERTIFICATE----- -MIIFOTCCBCGgAwIBAgISESGZp5e2NeqwsdyPhd1B3uLdMA0GCSqGSIb3DQEBCwUA -MGYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTwwOgYD -VQQDEzNHbG9iYWxTaWduIE9yZ2FuaXphdGlvbiBWYWxpZGF0aW9uIENBIC0gU0hB -MjU2IC0gRzIwHhcNMTUwNDE0MTMxNzAzWhcNMTYxMTA4MTI1NDQzWjBjMQswCQYD -VQQGEwJESzELMAkGA1UECBMCREsxEzARBgNVBAcTCkNvcGVuaGFnZW4xGDAWBgNV -BAoTD0RhbnNrZSBCYW5rIEEvUzEYMBYGA1UEAxQPKi5kYW5za2ViYW5rLnNlMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtoh4WuhtiJKh/585noJRB+nx -9DgSK4+oE4zpYX5kihpbqVf/UgUvfl73MDMcbFM4300q2CKh+O1dDRMcPNNdNTHN -PRtnNAoBlHrRU1OtfLJGYfQ5I8zWbip1CbY8DfSnNoav4JhgV3Jwryq+WpVk6Nww -YsfuJbyWrWJe2bIwydGIOrbyF+C1MNkUNRnB80NTNzPYL7VtecmfJtXm5MJspWNa -ZPSIQekrxoyxGTkJiBvRSFTioH5RgwVhC5guX08c2sZL2aMZiBazytZz4H4K89qF -ORwebPbq48QprvqHB8N1HFn2ygoab8HruWhoI6zz9sgbhOsKiqRQD6LcIcxMIwID -AQABo4IB4jCCAd4wDgYDVR0PAQH/BAQDAgWgMEkGA1UdIARCMEAwPgYGZ4EMAQIC -MDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9z -aXRvcnkvMCkGA1UdEQQiMCCCDyouZGFuc2tlYmFuay5zZYINZGFuc2tlYmFuay5z -ZTAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBJBgNV -HR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmdsb2JhbHNpZ24uY29tL2dzL2dzb3Jn -YW5pemF0aW9udmFsc2hhMmcyLmNybDCBoAYIKwYBBQUHAQEEgZMwgZAwTQYIKwYB -BQUHMAKGQWh0dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L2dzb3Jn -YW5pemF0aW9udmFsc2hhMmcycjEuY3J0MD8GCCsGAQUFBzABhjNodHRwOi8vb2Nz -cDIuZ2xvYmFsc2lnbi5jb20vZ3Nvcmdhbml6YXRpb252YWxzaGEyZzIwHQYDVR0O -BBYEFJcClG8LQNfWNbnpuFz9mOAqlhhRMB8GA1UdIwQYMBaAFJbeYfG9HBYpUxzA -zH07gwBA5hp8MA0GCSqGSIb3DQEBCwUAA4IBAQC9Zg40R7JHvAyF7k+D/08+vqQT -fpW0sxH8OBYGZgZ9sCLyuJuuqePWMh5dFpe1xjeDZUhF2WZx8rtBpe/bNEAcDOJM -taxlJizeTzVGx9DUmGEyfn6U1ot+peaJbjlpXaMH9VAwf0uhjanXD3jDz7S/7Iwu -ZHrJcgUmXVzxBzWHq3gd5zfbCrlBxvC+kZhO2j6Odl700NKSVSIYJI/E0VQI01x1 -lgAQ+1dWq9vHu83nsAxYW56elie/kplkRvPzow3ihI3cx8HVjoR2YNUS4ZuaW1vE -Ww8D9KttG3Qg6htbBX3lSUplls2Amto2aK99bjn6aN6jCdddgyj3xwc975tZ +MIIFSjCCBDKgAwIBAgIMI2d4vBm6pr6l/ezfMA0GCSqGSIb3DQEBCwUAMGYxCzAJ +BgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTwwOgYDVQQDEzNH +bG9iYWxTaWduIE9yZ2FuaXphdGlvbiBWYWxpZGF0aW9uIENBIC0gU0hBMjU2IC0g +RzIwHhcNMTYwOTEzMTEzNjAxWhcNMTgwOTE0MTEzNjAxWjBtMQswCQYDVQQGEwJE +SzETMBEGA1UECBMKQ29wZW5oYWdlbjEVMBMGA1UEBxMMQ29wZW5oYWdlbiBLMRgw +FgYDVQQKEw9EYW5za2UgQmFuayBBL1MxGDAWBgNVBAMMDyouZGFuc2tlYmFuay5z +ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANpBHTaIScLocNcQoOuj +PSX3xwlbomedbXDaeM1LBRLWIiGNibC/XUYGNjn+4tvYyU+B9GDYXLL1C6m3YHsu +Fts0CamxLYS2VSLCjc/v9sFbDZcodj+6sE9/AXhwmjYj8YC6p7FHcK7LGYYOoBAd +YCuQl7Xd+yaCbajOwyyoItiEe6qf0/hmV2SH6rJsQtPSCbUqMX3kAgAlQOhCqMdD +ke6w4TiQAHv+An4ceF7KuMIPOzHB2/PO1Z/YQLG1vLZMA892fuJeFH5KuvtN6d9a +h1z21vyu7eHsAg/Ua1HvEGYaA4xjQM5czoP4yQSJaR0kbpp1hrCv0q3K9R6NIUhF +lC0CAwEAAaOCAe8wggHrMA4GA1UdDwEB/wQEAwIFoDCBoAYIKwYBBQUHAQEEgZMw +gZAwTQYIKwYBBQUHMAKGQWh0dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2Fj +ZXJ0L2dzb3JnYW5pemF0aW9udmFsc2hhMmcycjEuY3J0MD8GCCsGAQUFBzABhjNo +dHRwOi8vb2NzcDIuZ2xvYmFsc2lnbi5jb20vZ3Nvcmdhbml6YXRpb252YWxzaGEy +ZzIwVgYDVR0gBE8wTTBBBgkrBgEEAaAyARQwNDAyBggrBgEFBQcCARYmaHR0cHM6 +Ly93d3cuZ2xvYmFsc2lnbi5jb20vcmVwb3NpdG9yeS8wCAYGZ4EMAQICMAkGA1Ud +EwQCMAAwSQYDVR0fBEIwQDA+oDygOoY4aHR0cDovL2NybC5nbG9iYWxzaWduLmNv +bS9ncy9nc29yZ2FuaXphdGlvbnZhbHNoYTJnMi5jcmwwKQYDVR0RBCIwIIIPKi5k +YW5za2ViYW5rLnNlgg1kYW5za2ViYW5rLnNlMB0GA1UdJQQWMBQGCCsGAQUFBwMB +BggrBgEFBQcDAjAdBgNVHQ4EFgQU5QZdNIS6nLfk6lV4vHVRHg6DTlMwHwYDVR0j +BBgwFoAUlt5h8b0cFilTHMDMfTuDAEDmGnwwDQYJKoZIhvcNAQELBQADggEBAB5P +l1RAQdS2tXyefgJaymhVpcJds63grMyUztkU6lzYbTbdZsMa5x96WLjfvdDDTVmI +pP5OMlv1q7Gh/4uhkIPOE9VH2thTyHsg9jUetBPSOAJqEi5DLGtlpYqlDRPqDrZp +bSP5PpKWOC5AXy4cho/Ix4dkRamB9zT7lbYtusCP3Cgnbfm00b2gmOe9ax8jOpXb +/1zLd7YTjmrGrElIuA0I1FmsEsVzPp0BONaSUVjWtJP4LJ+rQqp/0+iCKdBVLLbA +sE/FCXopdjvWaQgHoUDKes6FJmY4h0rDpViU3ClTEvfN+DdRlxlODRw0STh+CFtl +SnEPwHpG0wzZnwfPFvU= -----END CERTIFICATE----- mobil.danskebank.se:443 diff --git bankdroid-legacy/src/main/res/raw/cert_meniga.pem bankdroid-legacy/src/main/res/raw/cert_meniga.pem index 828d693..a899ce9 100644 --- bankdroid-legacy/src/main/res/raw/cert_meniga.pem +++ bankdroid-legacy/src/main/res/raw/cert_meniga.pem @@ -1,31 +1,32 @@ -----BEGIN CERTIFICATE----- -MIIFLDCCBBSgAwIBAgIHS2MFZfZKXTANBgkqhkiG9w0BAQsFADCBtDELMAkGA1UE -BhMCVVMxEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAY -BgNVBAoTEUdvRGFkZHkuY29tLCBJbmMuMS0wKwYDVQQLEyRodHRwOi8vY2VydHMu -Z29kYWRkeS5jb20vcmVwb3NpdG9yeS8xMzAxBgNVBAMTKkdvIERhZGR5IFNlY3Vy -ZSBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjAeFw0xNDEwMzAxNTA0MTJaFw0x -NjEwMzAxNTA0MTJaMEwxCzAJBgNVBAYTAklTMRIwEAYDVQQHEwlSZXlramF2aWsx -EzARBgNVBAoTCk1lbmlnYSBlaGYxFDASBgNVBAMMCyoubWVuaWdhLmlzMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwSMfGQ1DRMH6NuQm6AjlpnvHZMls -iqtlgneL32kRd6FHNTk3HoEy5s+AnfJhx2AO5aNuOb9mht7TwNZnfdRsK7RUOcN1 -w8hQDOlHeD4VlPzNm4rrlFjfMrsLG/8IlvKD7JWCh0rirPnSYfKXjTiEbCvHKXJ4 -mEhxOKrVD4It1UhZqZ3enVkMkUhDJYlyW0sW0fWSZGZkDopK6VX5O/mQ+AQrBfXb -UhJTubLAi0LRvQHexQPTs3hLLKWol0EUVoiqNALT19+y1+gy4Ojg8D2B/TOVP9vq -TQl8WX0gXKY7tzQPDyQ9KfFL+yluqahyEf1PR2QkALnz0BU76Dg41AHN2wIDAQAB -o4IBqDCCAaQwDAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB -BQUHAwIwDgYDVR0PAQH/BAQDAgWgMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9j -cmwuZ29kYWRkeS5jb20vZ2RpZzJzMi0wLmNybDBTBgNVHSAETDBKMEgGC2CGSAGG -/W0BBxcCMDkwNwYIKwYBBQUHAgEWK2h0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRk -eS5jb20vcmVwb3NpdG9yeS8wdgYIKwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhho -dHRwOi8vb2NzcC5nb2RhZGR5LmNvbS8wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jZXJ0 -aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVwb3NpdG9yeS9nZGlnMi5jcnQwHwYDVR0j -BBgwFoAUQMK9J47MNIMwojPX+2yz8LQsgM4wIQYDVR0RBBowGIILKi5tZW5pZ2Eu -aXOCCW1lbmlnYS5pczAdBgNVHQ4EFgQUdvrJexWWM/V5ciXjgKRnPjE8byAwDQYJ -KoZIhvcNAQELBQADggEBAIfo9SNMcfaYqgn8Fhydfr3rJM4qY8YMP/KIQfWhn/yZ -Gver/aRIfsZp6eCOg/qk4F+zJvORkZhw8jzYRRlzJpcSvy9o8n6sPXkU2gw3FiZj -B7EFJ2sdd2ZsmeZWbGANSRgEupxVsITu+HcDfjGcJ5q/3MLXQvthAB/hCU8h/q03 -LzGIS3OGmbxZLZTQlMCT2nPwte6yd3xSRSBGXOFpT3KTHKe6Gggq5e865IodXFbX -UGJNNqg9tMjg0n8iQp9Pn7I3J8vh673TmhFB05wfovKJ3GtB50U0tIwpGq38PLQ/ -83CWGJvQw2hT0OP6EwRKXIESPvxFC269xJdUwAuHrg0= +MIIFQDCCBCigAwIBAgIIHHBEGyxYdQQwDQYJKoZIhvcNAQELBQAwgbQxCzAJBgNV +BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRow +GAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UECxMkaHR0cDovL2NlcnRz +LmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMTMwMQYDVQQDEypHbyBEYWRkeSBTZWN1 +cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwHhcNMTYwODI1MTMzNDM4WhcN +MTkxMDMwMTUwNDEyWjBVMQswCQYDVQQGEwJJUzETMBEGA1UEBwwKS8OzcGF2b2d1 +cjEbMBkGA1UEChMSTWVuaWdhIEljZWxhbmQgZWhmMRQwEgYDVQQDDAsqLm1lbmln +YS5pczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMEjHxkNQ0TB+jbk +JugI5aZ7x2TJbIqrZYJ3i99pEXehRzU5Nx6BMubPgJ3yYcdgDuWjbjm/Zobe08DW +Z33UbCu0VDnDdcPIUAzpR3g+FZT8zZuK65RY3zK7Cxv/CJbyg+yVgodK4qz50mHy +l404hGwrxylyeJhIcTiq1Q+CLdVIWamd3p1ZDJFIQyWJcltLFtH1kmRmZA6KSulV ++Tv5kPgEKwX121ISU7mywItC0b0B3sUD07N4SyylqJdBFFaIqjQC09ffstfoMuDo +4PA9gf0zlT/b6k0JfFl9IFymO7c0Dw8kPSnxS/spbqmochH9T0dkJAC589AVO+g4 +ONQBzdsCAwEAAaOCAbIwggGuMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMA4GA1UdDwEB/wQEAwIFoDA1BgNVHR8ELjAsMCqgKKAm +hiRodHRwOi8vY3JsLmdvZGFkZHkuY29tL2dkaWcyczItMy5jcmwwXQYDVR0gBFYw +VDBIBgtghkgBhv1tAQcXAjA5MDcGCCsGAQUFBwIBFitodHRwOi8vY2VydGlmaWNh +dGVzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMAgGBmeBDAECAjB2BggrBgEFBQcB +AQRqMGgwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmdvZGFkZHkuY29tLzBABggr +BgEFBQcwAoY0aHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBvc2l0 +b3J5L2dkaWcyLmNydDAfBgNVHSMEGDAWgBRAwr0njsw0gzCiM9f7bLPwtCyAzjAh +BgNVHREEGjAYggsqLm1lbmlnYS5pc4IJbWVuaWdhLmlzMB0GA1UdDgQWBBR2+sl7 +FZYz9XlyJeOApGc+MTxvIDANBgkqhkiG9w0BAQsFAAOCAQEAR+uxgkI6AfSB+Txd +IYbWQEYYjTXDWCNGoNp5Y+Dl1M8qdBKjYXfyYAKOWCNi4qRwDk1+QOtv6AB3CkOy +H4O6HmhKcmeGOw2zpKzMLOD4y58VJr0LpIBvE4WNVHssjhKvZizaD5xGLtDbogXn +O5o1STzUp0FzQKwVtLw4KdE7UEJwfA7Q/MNOjIuzylNmKa2dDjHv+DfcJab4Qhw1 +1qUGdDIZxLOgyMkZiO1UbqBtctOeYgYNphBAoyzWurLmu0n7fzhhdCZTq67XHJAE +xV/zSLYmh50rSN1GEAg69zo5fAQFn9SKTTGdDo5jqB3hAHBiuf4/F1j0DWQ96kNK +2F8WeQ== -----END CERTIFICATE----- www.meniga.is:443 commit d32deec5dbe785ef35b1cb89484d6c29653e370c Author: Johan Walles Date: Mon Oct 31 21:42:39 2016 +0100 Enable PMD's field-can-be-local-var warning diff --git app/src/main/java/com/liato/bankdroid/db/DBAdapter.java app/src/main/java/com/liato/bankdroid/db/DBAdapter.java index e9850ef..af07d75 100644 --- app/src/main/java/com/liato/bankdroid/db/DBAdapter.java +++ app/src/main/java/com/liato/bankdroid/db/DBAdapter.java @@ -35,8 +35,6 @@ import java.util.Map; public class DBAdapter { - private DatabaseHelper mDbHelper; - private SQLiteDatabase mDb; /** @@ -46,8 +44,8 @@ public class DBAdapter { * @param ctx the Context within which to work */ public DBAdapter(Context ctx) { - mDbHelper = DatabaseHelper.getHelper(ctx); - mDb = mDbHelper.getWritableDatabase(); + DatabaseHelper dbHelper = DatabaseHelper.getHelper(ctx); + mDb = dbHelper.getWritableDatabase(); } /** diff --git app/src/main/java/com/liato/bankdroid/lockpattern/ChooseLockPatternExample.java app/src/main/java/com/liato/bankdroid/lockpattern/ChooseLockPatternExample.java index 0e7502e..f17d78b 100644 --- app/src/main/java/com/liato/bankdroid/lockpattern/ChooseLockPatternExample.java +++ app/src/main/java/com/liato/bankdroid/lockpattern/ChooseLockPatternExample.java @@ -36,8 +36,6 @@ public class ChooseLockPatternExample extends Activity implements View.OnClickLi private View mSkipButton; - private View mImageView; - private AnimationDrawable mAnimation; private Runnable mRunnable = new Runnable() { @@ -94,10 +92,10 @@ public class ChooseLockPatternExample extends Activity implements View.OnClickLi mSkipButton = findViewById(R.id.skip_button); mSkipButton.setOnClickListener(this); - mImageView = (ImageView) findViewById(R.id.lock_anim); - mImageView.setBackgroundResource(R.drawable.lock_anim); - mImageView.setOnClickListener(this); - mAnimation = (AnimationDrawable) mImageView.getBackground(); + View imageView = (ImageView) findViewById(R.id.lock_anim); + imageView.setBackgroundResource(R.drawable.lock_anim); + imageView.setOnClickListener(this); + mAnimation = (AnimationDrawable) imageView.getBackground(); } protected void startAnimation(final AnimationDrawable animation) { diff --git app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerDialog.java app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerDialog.java index 86f7136..4817e11 100644 --- app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerDialog.java +++ app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerDialog.java @@ -36,8 +36,6 @@ public class ColorPickerDialog private ColorPickerView mColorPicker; - private ColorPickerPanelView mOldColor; - private ColorPickerPanelView mNewColor; private OnColorChangedListener mListener; @@ -72,20 +70,20 @@ public class ColorPickerDialog setTitle(R.string.dialog_color_picker); mColorPicker = (ColorPickerView) layout.findViewById(R.id.color_picker_view); - mOldColor = (ColorPickerPanelView) layout.findViewById(R.id.old_color_panel); + ColorPickerPanelView oldColor = (ColorPickerPanelView) layout.findViewById(R.id.old_color_panel); mNewColor = (ColorPickerPanelView) layout.findViewById(R.id.new_color_panel); - ((LinearLayout) mOldColor.getParent()).setPadding( + ((LinearLayout) oldColor.getParent()).setPadding( Math.round(mColorPicker.getDrawingOffset()), 0, Math.round(mColorPicker.getDrawingOffset()), 0 ); - mOldColor.setOnClickListener(this); + oldColor.setOnClickListener(this); mNewColor.setOnClickListener(this); mColorPicker.setOnColorChangedListener(this); - mOldColor.setColor(color); + oldColor.setColor(color); mColorPicker.setColor(color, true); } diff --git bankdroid-core/src/main/java/com/liato/bankdroid/configuration/DefaultConnectionConfiguration.java bankdroid-core/src/main/java/com/liato/bankdroid/configuration/DefaultConnectionConfiguration.java index 62a1b46..3a47c5b 100644 --- bankdroid-core/src/main/java/com/liato/bankdroid/configuration/DefaultConnectionConfiguration.java +++ bankdroid-core/src/main/java/com/liato/bankdroid/configuration/DefaultConnectionConfiguration.java @@ -8,19 +8,13 @@ import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; -public enum DefaultConnectionConfiguration { - - INSTANCE; +public class DefaultConnectionConfiguration { public static final String NAME = "provider.configuration.name"; - private List configuration; - - DefaultConnectionConfiguration() { - configuration = createConfiguration(); - } + private final static List configuration = createConfiguration(); - private List createConfiguration() { + private static List createConfiguration() { List configuration = new ArrayList<>(); configuration.add(new FieldBuilder(NAME, ResourceBundle.getBundle("i18n.application")) .placeholder("") @@ -30,6 +24,6 @@ public enum DefaultConnectionConfiguration { } public static List fields() { - return INSTANCE.configuration; + return configuration; } } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/coop/Coop.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/coop/Coop.java index 84bcb8d..1ac76d6 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/coop/Coop.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/coop/Coop.java @@ -80,8 +80,6 @@ public class Coop extends Bank { private ObjectMapper mObjectMapper; - private String response; - public Coop(Context context) { super(context, R.drawable.logo_coop); @@ -141,7 +139,7 @@ public class Coop extends Bank { login(); - response = urlopen.open("https://www.coop.se/Mina-sidor/Oversikt/Mina-poang/"); + String response = urlopen.open("https://www.coop.se/Mina-sidor/Oversikt/Mina-poang/"); Document dResponse = Jsoup.parse(response); Account poang = new Account("\u2014 Poäng", Helpers.parseBalance(dResponse.select(".Grid-cell--1 p").text()), diff --git config/quality/pmd/pmd-ruleset.xml config/quality/pmd/pmd-ruleset.xml index 19538ce..288976e 100644 --- config/quality/pmd/pmd-ruleset.xml +++ config/quality/pmd/pmd-ruleset.xml @@ -113,7 +113,6 @@ - commit 37f16fda5cc03d3b2d6b9180e6bcb2ed5d600c57 Author: Johan Walles Date: Tue Nov 1 22:06:24 2016 +0100 Fix whitespace issues diff --git app/build.gradle app/build.gradle index aafa141..11832d8 100644 --- app/build.gradle +++ app/build.gradle @@ -19,7 +19,7 @@ apply plugin: 'com.android.application' apply from: '../config/quality/quality.gradle' apply plugin: "org.ajoberstar.grgit" -if(new File('app/crashlytics.properties').exists()) { +if (new File('app/crashlytics.properties').exists()) { apply plugin: 'io.fabric' } diff --git app/src/main/java/com/liato/bankdroid/BankEditActivity.java app/src/main/java/com/liato/bankdroid/BankEditActivity.java index fce0bbe..835c64e 100644 --- app/src/main/java/com/liato/bankdroid/BankEditActivity.java +++ app/src/main/java/com/liato/bankdroid/BankEditActivity.java @@ -122,7 +122,7 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected @OnClick(R.id.btnSettingsOk) public void onSubmit(View v) { - if(!validate()) { + if (!validate()) { return; } SELECTED_BANK.setProperties(getFormParameters(SELECTED_BANK.getConnectionConfiguration())); @@ -139,7 +139,7 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected @Override public void onItemSelected(AdapterView parentView, View selectedItemView, int pos, long id) { Bank selectedBank = (Bank) parentView.getItemAtPosition(pos); - if(SELECTED_BANK == null || !SELECTED_BANK.equals(selectedBank)) { + if (SELECTED_BANK == null || !SELECTED_BANK.equals(selectedBank)) { SELECTED_BANK = selectedBank; mFormContainer.removeAllViewsInLayout(); createForm(SELECTED_BANK.getConnectionConfiguration(), @@ -154,10 +154,10 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected private void createForm(List... configurations) { - for(List fields : configurations) { + for (List fields : configurations) { for (Field field : fields) { createLabel(field); - if(field.getValues().isEmpty()) { + if (field.getValues().isEmpty()) { createField(field); } else { createSpinner(field); @@ -203,7 +203,7 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected DefaultConnectionConfiguration.NAME); customName.setText(bank.getCustomName()); - for(Map.Entry property : bank.getProperties().entrySet()) { + for (Map.Entry property : bank.getProperties().entrySet()) { EditText propertyInput = (EditText) mFormContainer.findViewWithTag(property.getKey()); propertyInput.setText(property.getValue()); } @@ -211,7 +211,7 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected private Map getFormParameters(List fields) { Map properties = new HashMap<>(); - for(Field field : fields) { + for (Field field : fields) { properties.put(field.getReference(), getFormParameter(field.getReference())); } return properties; @@ -219,10 +219,10 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected private String getFormParameter(String property) { View propertyView = mFormContainer.findViewWithTag(property); - if(propertyView instanceof EditText) { + if (propertyView instanceof EditText) { EditText propertyInput = (EditText) propertyView; return propertyInput.getText().toString().trim(); - } else if(propertyView instanceof Spinner) { + } else if (propertyView instanceof Spinner) { Spinner spinnerProperty = (Spinner) propertyView; Entry entry = (Entry) spinnerProperty.getSelectedItem(); return entry.getKey(); @@ -235,7 +235,7 @@ public class BankEditActivity extends LockableActivity implements OnItemSelected boolean valid = true; Iterator fields = Iterators.concat(SELECTED_BANK.getConnectionConfiguration().iterator(), DefaultConnectionConfiguration.fields().iterator()); - while(fields.hasNext()) { + while (fields.hasNext()) { Field field = fields.next(); try { field.validate(getFormParameter(field.getReference())); diff --git app/src/main/java/com/liato/bankdroid/db/DBAdapter.java app/src/main/java/com/liato/bankdroid/db/DBAdapter.java index 215c59f..e9850ef 100644 --- app/src/main/java/com/liato/bankdroid/db/DBAdapter.java +++ app/src/main/java/com/liato/bankdroid/db/DBAdapter.java @@ -136,7 +136,7 @@ public class DBAdapter { public Cursor fetchProperties(String bankId) { return mDb.query(Database.PROPERTY_TABLE_NAME, null, - Database.PROPERTY_CONNECTION_ID+"='"+bankId+"'", null, null, null, null); + Database.PROPERTY_CONNECTION_ID + "='" + bankId + "'", null, null, null, null); } public long updateBank(Bank bank) { @@ -162,9 +162,9 @@ public class DBAdapter { } if (bankId != -1) { Map properties = bank.getProperties(); - for(Map.Entry property : properties.entrySet()) { + for (Map.Entry property : properties.entrySet()) { String value = property.getValue(); - if(value != null && !value.isEmpty()) { + if (value != null && !value.isEmpty()) { ContentValues propertyValues = new ContentValues(); propertyValues.put(Database.PROPERTY_KEY, property.getKey()); propertyValues.put(Database.PROPERTY_VALUE, value); diff --git app/src/main/java/com/liato/bankdroid/db/DatabaseHelper.java app/src/main/java/com/liato/bankdroid/db/DatabaseHelper.java index e4b166a..a295de8 100644 --- app/src/main/java/com/liato/bankdroid/db/DatabaseHelper.java +++ app/src/main/java/com/liato/bankdroid/db/DatabaseHelper.java @@ -74,7 +74,7 @@ final public class DatabaseHelper extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + LegacyDatabase.BANK_TABLE_NAME + " ADD " + LegacyDatabase.BANK_HIDE_ACCOUNTS + " integer;"); } - if(oldVersion <= 11) { + if (oldVersion <= 11) { try { db.beginTransaction(); db.execSQL(Database.TABLE_CONNECTION_PROPERTIES); @@ -104,7 +104,7 @@ final public class DatabaseHelper extends SQLiteOpenHelper { + LegacyDatabase.BANK_HIDE_ACCOUNTS + " FROM " + tempTable); // Add username, password and extras fields to properties table. - Cursor c = db.query(tempTable, null, null, null,null,null,null); + Cursor c = db.query(tempTable, null, null, null, null, null, null); try { if (!(c == null || c.isClosed() || (c.isBeforeFirst() && c.isAfterLast()))) { while (!c.isLast() && !c.isAfterLast()) { diff --git bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/Entry.java bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/Entry.java index 8201e43..e0d04d0 100644 --- bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/Entry.java +++ bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/Entry.java @@ -6,7 +6,7 @@ public class Entry { private final String mValue; public Entry(String key, String value) { - if(key == null || key.trim().isEmpty()) { + if (key == null || key.trim().isEmpty()) { throw new IllegalArgumentException("key cannot be null or empty."); } mKey = key; diff --git bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/FieldBuilder.java bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/FieldBuilder.java index ad41c8f..4d1ce55 100644 --- bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/FieldBuilder.java +++ bankdroid-interface/src/main/java/com/liato/bankdroid/api/configuration/FieldBuilder.java @@ -13,7 +13,7 @@ public class FieldBuilder { private BasicField field; /** - * Create a new {@link Field} builder without i18n support. + * Create a new {@link Field} builder without i18n support. */ public FieldBuilder(String reference) { this(reference, null); @@ -24,7 +24,7 @@ public class FieldBuilder { * The following keys needs to be in the ResourceBundle: * {@code field.{reference}.label} - Locale label value * {@code field.{reference}.placeholder} - Locale placeholder value. - * + * * Setting {@link #placeholder(String)} or {@link #label(String) specifically will override the i18n values. * Otherwise they will be set to the value specified in the Locale bundle or in the default bundle if not present. * If a key is not present at all in the ResourceBundle the key will will be returned. @@ -32,7 +32,7 @@ public class FieldBuilder { * @param bundle The ResourceBundle to be used for i18n support. */ public FieldBuilder(String reference, ResourceBundle bundle) { - if(reference == null || reference.trim().isEmpty()) { + if (reference == null || reference.trim().isEmpty()) { throw new IllegalArgumentException("reference must be provided."); } field = new BasicField(reference, bundle); @@ -146,7 +146,7 @@ public class FieldBuilder { @Override public List getValues() { - if(values == null) { + if (values == null) { values = Collections.emptyList(); } return values; @@ -154,18 +154,18 @@ public class FieldBuilder { @Override public void validate(String value) throws IllegalArgumentException { - if(isRequired()) { - if(value == null || value.trim().isEmpty()) { + if (isRequired()) { + if (value == null || value.trim().isEmpty()) { throw new IllegalArgumentException(String.format("%s is required", getLabel())); } - if(validator != null) { + if (validator != null) { validator.validate(value); } } } private String getLocaleString(String key) { - if(!isLocale()) { + if (!isLocale()) { return null; } String propertyKey = String.format("field.%s.%s", getReference(), key); diff --git bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/AbstractAccountBuilder.java bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/AbstractAccountBuilder.java index 44c8e9b..06b2566 100644 --- bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/AbstractAccountBuilder.java +++ bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/AbstractAccountBuilder.java @@ -34,7 +34,7 @@ abstract class AbstractAccountBuilder> { } public T addCustomAttribute(String key, String value) { - if(mCustomAttributes == null) { + if (mCustomAttributes == null) { mCustomAttributes = new HashMap<>(); } mCustomAttributes.put(key, value); diff --git bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/AccountBuilder.java bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/AccountBuilder.java index 8dff297..ef3c9d2 100644 --- bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/AccountBuilder.java +++ bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/AccountBuilder.java @@ -58,7 +58,7 @@ public class AccountBuilder extends AbstractAccountBuilder { @Override public Map getCustomAttributes() { - return mCustomAttributes == null ? Collections.emptyMap() : mCustomAttributes; + return mCustomAttributes == null ? Collections.emptyMap() : mCustomAttributes; } } } diff --git bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/CreditCardAccountBuilder.java bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/CreditCardAccountBuilder.java index f837bab..d5343cb 100644 --- bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/CreditCardAccountBuilder.java +++ bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/CreditCardAccountBuilder.java @@ -10,7 +10,7 @@ import java.util.Map; import static com.liato.bankdroid.api.domain.account.impl.AccountBuilder.BasicAccount; -public class CreditCardAccountBuilder extends AbstractAccountBuilder{ +public class CreditCardAccountBuilder extends AbstractAccountBuilder { private BigDecimal mCreditLimit; diff --git bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/EquityAccountBuilder.java bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/EquityAccountBuilder.java index 67abd33..f34df22 100644 --- bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/EquityAccountBuilder.java +++ bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/EquityAccountBuilder.java @@ -11,7 +11,7 @@ import java.util.Map; import static com.liato.bankdroid.api.domain.account.impl.AccountBuilder.BasicAccount; -public class EquityAccountBuilder extends AbstractAccountBuilder{ +public class EquityAccountBuilder extends AbstractAccountBuilder { private BigDecimal mCost; @@ -38,7 +38,7 @@ public class EquityAccountBuilder extends AbstractAccountBuilder(); } mEquities.add(equity); diff --git bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/EquityBuilder.java bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/EquityBuilder.java index bd1a3e8..5743c7d 100644 --- bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/EquityBuilder.java +++ bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/EquityBuilder.java @@ -19,9 +19,9 @@ public class EquityBuilder { * a 25 % loss, while {@code 1.5} is 50 % profit. * @param currency The currency of the equity. */ - public EquityBuilder(BigDecimal balance, double revenue, String currency){ + public EquityBuilder(BigDecimal balance, double revenue, String currency) { mEquity = new BasicEquity(costFromBalanceAndRevenue(balance, revenue), - revenueFromBalanceAndRevenueAsPerecntage(balance, revenue),currency); + revenueFromBalanceAndRevenueAsPerecntage(balance, revenue), currency); } public EquityBuilder name(String name) { diff --git bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/LiabilityAccountBuilder.java bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/LiabilityAccountBuilder.java index d99e715..1f6a20e 100644 --- bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/LiabilityAccountBuilder.java +++ bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/LiabilityAccountBuilder.java @@ -30,7 +30,7 @@ public class LiabilityAccountBuilder extends AbstractAccountBuilder(); } mPayments.add(payment); @@ -69,4 +69,4 @@ public class LiabilityAccountBuilder extends AbstractAccountBuilderemptyList() : mPayments; } } -} \ No newline at end of file +} diff --git bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/PrePaidCardAccountBuilder.java bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/PrePaidCardAccountBuilder.java index 641fd7e..814ed20 100644 --- bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/PrePaidCardAccountBuilder.java +++ bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/PrePaidCardAccountBuilder.java @@ -9,7 +9,7 @@ import java.util.Map; import static com.liato.bankdroid.api.domain.account.impl.AccountBuilder.BasicAccount; -public class PrePaidCardAccountBuilder extends AbstractAccountBuilder{ +public class PrePaidCardAccountBuilder extends AbstractAccountBuilder { private DateTime mValidFrom; private DateTime mExpirationDate; diff --git bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/TransactionAccountBuilder.java bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/TransactionAccountBuilder.java index 00e1e2f..6bd988f 100644 --- bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/TransactionAccountBuilder.java +++ bankdroid-interface/src/main/java/com/liato/bankdroid/api/domain/account/impl/TransactionAccountBuilder.java @@ -24,7 +24,7 @@ public class TransactionAccountBuilder extends AbstractAccountBuilder(); } mTransactions.add(transaction); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/Bank.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/Bank.java index ceca10f..18a6a0c 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/Bank.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/Bank.java @@ -369,7 +369,7 @@ public abstract class Bank implements Comparable, IBankTypes { } public Map getProperties() { - if(this.properties == null) { + if (this.properties == null) { this.properties = new HashMap<>(); } return this.properties; diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankHelper.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankHelper.java index 496f9e4..d2aa87c 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankHelper.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankHelper.java @@ -14,7 +14,7 @@ public class LegacyBankHelper { private static Map providerReferences; public static String getReferenceFromLegacyId(int legacyId) { - if(providerReferences == null) { + if (providerReferences == null) { generateLegacyProviderReferences(); } return providerReferences.get(legacyId); @@ -22,7 +22,7 @@ public class LegacyBankHelper { // TODO Used during refactoring. Remove before 2.0 public static int getLegacyIdFromReference(String reference) { - if(legacyProviderReferences == null) { + if (legacyProviderReferences == null) { generateLegacyProviderReferences(); } return legacyProviderReferences.get(reference); @@ -32,15 +32,14 @@ public class LegacyBankHelper { Map references = new HashMap<>(); Map legacyIds = new HashMap<>(); Field[] fields = IBankTypes.class.getFields(); - for(int i = 0 ; i < fields.length; i++) { - Field field = fields[i]; + for (Field field : fields) { try { String reference = field.getName().toLowerCase().replaceAll("_", "-"); Integer legacyId = field.getInt(new IBankTypes() { }); references.put(legacyId, reference); legacyIds.put(reference, legacyId); - } catch(IllegalAccessException e) { + } catch (IllegalAccessException e) { Timber.e(e, "Provider could not be mapped"); } } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/OKQ8.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/OKQ8.java index b740b6e..c7fa006 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/OKQ8.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/OKQ8.java @@ -106,7 +106,7 @@ public class OKQ8 extends Bank { } String postAction = matcher.group(1); postData.add(new BasicNameValuePair("javax.faces.ViewState", - postAction.substring(postAction.length()-5,postAction.length()-1))); + postAction.substring(postAction.length() - 5, postAction.length() - 1))); postData.add(new BasicNameValuePair("loginForm", "loginForm")); postData.add(new BasicNameValuePair("button", "Logga in")); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SveaDirekt.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SveaDirekt.java index 620c740..9aef3df 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SveaDirekt.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/SveaDirekt.java @@ -124,7 +124,7 @@ public class SveaDirekt extends Bank { response = urlopen.open(ACCOUNTS_URL); Document doc = Jsoup.parse(response); ArrayList accounts = parseAccounts(doc); - for(Account account : accounts) { + for (Account account : accounts) { response = urlopen.open(TRANSACTIONS_URL + "?account=" + account.getId()); account.setTransactions(parseTransactions(response)); } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/AmericanExpress.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/AmericanExpress.java index 396af06..933ec1f 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/AmericanExpress.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/AmericanExpress.java @@ -116,9 +116,9 @@ public class AmericanExpress extends Bank { urlopen = login(); - for(Card card : loginResponse.getCards()) { + for (Card card : loginResponse.getCards()) { Account account = asAccount(card); - if(card.isTransactionsEnabled()) { + if (card.isTransactionsEnabled()) { account.setTransactions(fetchTransactionsFor(card)); } accounts.add(account); @@ -137,7 +137,7 @@ public class AmericanExpress extends Bank { "\"sortedIndex\":" + card.getSortedIndex() + "}", HTTP.UTF_8), true); - if(response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { response.getEntity().consumeContent(); throw new BankException( res.getText(R.string.update_transactions_error).toString()); @@ -147,10 +147,10 @@ public class AmericanExpress extends Bank { .withType(TransactionsResponse.class) .readValue(response.getEntity().getContent()); - if(details.getTransactionDetails() == null) { + if (details.getTransactionDetails() == null) { throw new BankException(res.getText(R.string.server_error_try_again).toString()); } - if(details.getTransactionDetails().getStatus() != 0) { + if (details.getTransactionDetails().getStatus() != 0) { throw new BankException(details.getTransactionDetails().getMessage()); } @@ -159,8 +159,8 @@ public class AmericanExpress extends Bank { private List transactionsOf(@Nullable TransactionDetails details) { List transactions = new ArrayList<>(); - if(details != null) { - for(com.liato.bankdroid.banking.banks.americanexpress.model.Transaction transaction : details.getTransactions()) { + if (details != null) { + for (com.liato.bankdroid.banking.banks.americanexpress.model.Transaction transaction : details.getTransactions()) { transactions.add(asTransaction(transaction)); } } @@ -196,17 +196,17 @@ public class AmericanExpress extends Bank { } private LoginResponse parseLoginResponse(HttpResponse response) throws IOException, BankException { - if(response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { response.getEntity().consumeContent(); throw new BankException(res.getText(R.string.server_error_try_again).toString()); } LoginResponse loginResponse = MAPPER.reader() .withType(LoginResponse.class) .readValue(response.getEntity().getContent()); - if(loginResponse == null || loginResponse.getLogonData() == null) { + if (loginResponse == null || loginResponse.getLogonData() == null) { throw new BankException(res.getText(R.string.server_error_try_again).toString()); } - if(loginResponse.getLogonData().getStatus() != 0) { + if (loginResponse.getLogonData().getStatus() != 0) { throw new BankException(loginResponse.getLogonData().getMessage()); } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/Card.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/Card.java index c9abbe6..dbfe2d7 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/Card.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/Card.java @@ -70,7 +70,7 @@ public class Card { } public BigDecimal getBalance() { - if(summary != null && summary.getTotalBalance() != null) { + if (summary != null && summary.getTotalBalance() != null) { return Helpers.parseBalance(summary.getTotalBalance().getValue()).negate(); } return BigDecimal.ZERO; diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/LoginResponse.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/LoginResponse.java index b63fdd0..4d4ad79 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/LoginResponse.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/LoginResponse.java @@ -43,7 +43,7 @@ public class LoginResponse { } public List getCards() { - if(summaryData != null && summaryData.getCardList() != null) { + if (summaryData != null && summaryData.getCardList() != null) { return summaryData.getCardList(); } return Collections.emptyList(); diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/TransactionDetails.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/TransactionDetails.java index 2efa7ea..8673cb6 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/TransactionDetails.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/americanexpress/model/TransactionDetails.java @@ -40,7 +40,7 @@ public class TransactionDetails { public List getTransactions() { if (activityList != null) { List transactions = new ArrayList<>(); - for(AccountActivity activity : activityList) { + for (AccountActivity activity : activityList) { transactions.addAll(activity.getTransactionList()); } return transactions; diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java index 6969afa..7e8fe82 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java @@ -129,7 +129,7 @@ public class Lansforsakringar extends Bank { } finally { try { is.close(); - } catch(IOException e) { + } catch (IOException e) { Timber.w(e, "Closing JSON stream failed"); } } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/rikslunchen/Rikslunchen.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/rikslunchen/Rikslunchen.java index c8b5272..1d58f40 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/rikslunchen/Rikslunchen.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/rikslunchen/Rikslunchen.java @@ -86,7 +86,7 @@ public class Rikslunchen extends Bank { HttpResponse response = urlopen.openAsHttpResponse( BASE_URL + "?cardid=" + getUsername(), false); - if(response.getStatusLine().getStatusCode() != 200) { + if (response.getStatusLine().getStatusCode() != 200) { response.getEntity().consumeContent(); throw new LoginException(context.getString(R.string.invalid_card_number)); } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/FieldTypeMapper.java bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/FieldTypeMapper.java index 6436bdb..b57554a 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/FieldTypeMapper.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/FieldTypeMapper.java @@ -8,7 +8,7 @@ import android.text.InputType; public class FieldTypeMapper { public static FieldType toFieldType(int androidFieldType) { - switch(androidFieldType) { + switch (androidFieldType) { case InputType.TYPE_CLASS_NUMBER: return FieldType.NUMBER; case InputType.TYPE_CLASS_PHONE: @@ -20,7 +20,7 @@ public class FieldTypeMapper { } } public static int fromFieldType(FieldType fieldType) { - switch(fieldType) { + switch (fieldType) { case NUMBER: return InputType.TYPE_CLASS_NUMBER; case PHONE: diff --git config/quality/checkstyle/checkstyle.xml config/quality/checkstyle/checkstyle.xml index 204efc7..0493359 100644 --- config/quality/checkstyle/checkstyle.xml +++ config/quality/checkstyle/checkstyle.xml @@ -33,16 +33,17 @@ - + - --> + diff --git config/quality/quality.gradle config/quality/quality.gradle index bf2f1c6..9f4833d 100644 --- config/quality/quality.gradle +++ config/quality/quality.gradle @@ -63,7 +63,7 @@ task pmd(type: Pmd) { } } -if(plugins.hasPlugin('android') || plugins.hasPlugin('com.android.library')) { +if (plugins.hasPlugin('android') || plugins.hasPlugin('com.android.library')) { check.dependsOn 'lint' tasks.getByName("findbugs").dependsOn 'compileDebugSources' tasks.getByName('findbugs').classes = files("$project.buildDir/intermediates/classes") @@ -85,7 +85,7 @@ if(plugins.hasPlugin('android') || plugins.hasPlugin('com.android.library')) { } } } -if(plugins.hasPlugin('java')) { +if (plugins.hasPlugin('java')) { tasks.getByName('findbugs').classes = files("$project.buildDir/classes") tasks.getByName('findbugs').dependsOn 'classes' } commit e8ebeb2488435fbf9f990cd6bfc2d9fa12a561ac Author: Robert Högberg Date: Fri Oct 28 00:11:49 2016 +0200 Östgötatrafiken: Adapt to new login page Östgötatrafiken now supports three ways to log in/authenticate: * An e-mail address * A username (for legacy users (such as me)) * Facebook login Bankdroid only supports e-mail/username login. (I haven't tested e-mail login, but I expect it to work the same way as for username logins) diff --git CHANGELOG CHANGELOG index 970ea23..5ebd411 100644 --- CHANGELOG +++ CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. Not yet released * Bioklubben: Use https. It's secure and http page no longer exists. +* Östgötatrafiken: Adapt to new login page (Facebook login not supported) v1.9.11 (2016-10-26) * Warn about disabled banks in the transactions list diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Ostgotatrafiken.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Ostgotatrafiken.java index 37f3770..8524e38 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Ostgotatrafiken.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Ostgotatrafiken.java @@ -45,9 +45,6 @@ public class Ostgotatrafiken extends Bank { private static final String NAME_SHORT = "ogt"; - private static final String URL - = "https://www.ostgotatrafiken.se/Priser--biljetter/Mina-sidor/Login/"; - private static final int BANKTYPE_ID = IBankTypes.OSTGOTATRAFIKEN; private Pattern reViewState = Pattern.compile( @@ -75,7 +72,6 @@ public class Ostgotatrafiken extends Bank { super.NAME = NAME; super.NAME_SHORT = NAME_SHORT; super.BANKTYPE_ID = BANKTYPE_ID; - super.URL = URL; } public Ostgotatrafiken(String username, String password, Context context) throws BankException, @@ -90,18 +86,20 @@ public class Ostgotatrafiken extends Bank { R.raw.cert_ostgotatrafiken_login, R.raw.cert_ostgotatrafiken_overview)); List postData = new ArrayList(); - postData.add(new BasicNameValuePair("Username", getUsername())); - postData.add(new BasicNameValuePair("Password", getPassword())); - postData.add(new BasicNameValuePair("Login", "Logga in")); + postData.add(new BasicNameValuePair("", "{\"authSource\":10," + + "\"keepMeLimitedLoggedIn\":true," + + "\"userName\":\"" + getUsername() + "\"," + + "\"password\":\"" + getPassword() + "\"," + + "\"impersonateUserName\":\"\"}")); - return new LoginPackage(urlopen, postData, response, URL); + return new LoginPackage(urlopen, postData, response, "https://www.ostgotatrafiken.se/ajax/Login/Attempt"); } @Override public Urllib login() throws LoginException, BankException, IOException { LoginPackage lp = preLogin(); response = urlopen.open(lp.getLoginTarget(), lp.getPostData()); - if (!response.contains("Logga ut")) { + if (!response.contains("presentationUserName")) { throw new LoginException(res.getText(R.string.invalid_username_password).toString()); } return urlopen; commit ff91ffbc69b9d82ce6e75063b8914c1a8bb28865 Author: Johan Walles Date: Tue Nov 1 02:24:33 2016 +0100 Log bank usage statistics to Crashlytics With this change in place we'll be able to see in Crashlytics: * which banks are most frequently disabled * which banks are most frequently used * for which banks transactions updating doesn't work diff --git app/src/main/java/com/liato/bankdroid/DataRetrieverTask.java app/src/main/java/com/liato/bankdroid/DataRetrieverTask.java index b8145f3..331cfc3 100644 --- app/src/main/java/com/liato/bankdroid/DataRetrieverTask.java +++ app/src/main/java/com/liato/bankdroid/DataRetrieverTask.java @@ -24,6 +24,7 @@ import com.liato.bankdroid.banking.exceptions.BankChoiceException; import com.liato.bankdroid.banking.exceptions.BankException; import com.liato.bankdroid.banking.exceptions.LoginException; import com.liato.bankdroid.db.DBAdapter; +import com.liato.bankdroid.utils.LoggingUtils; import com.liato.bankdroid.utils.NetworkUtils; import android.app.AlertDialog; @@ -80,6 +81,7 @@ public class DataRetrieverTask extends AsyncTask { return this.dialog; } + @Nullable protected Bank getBankFromDb(long bankId, Context parent) { return BankFactory.bankFromDb(bankId, parent, true); } @@ -122,6 +124,7 @@ public class DataRetrieverTask extends AsyncTask { publishProgress(i, bank); if (isListingAllBanks() && bank.isDisabled()) { + LoggingUtils.logDisabledBank(bank); continue; } @@ -131,6 +134,8 @@ public class DataRetrieverTask extends AsyncTask { bank.closeConnection(); saveBank(bank, parent); i++; + + LoggingUtils.logBankUpdate(bank, true); } catch (final BankException e) { this.errors.add(bank.getName() + " (" + bank.getUsername() + ")"); @@ -194,6 +199,7 @@ public class DataRetrieverTask extends AsyncTask { .setIcon(android.R.drawable.ic_dialog_alert) .setNeutralButton("Ok", new DialogInterface.OnClickListener() { + @Override public void onClick( final DialogInterface dialog, final int id) { diff --git app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java index f289d76..7a82c5b 100644 --- app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java +++ app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java @@ -27,6 +27,7 @@ import com.liato.bankdroid.banking.exceptions.BankException; import com.liato.bankdroid.banking.exceptions.LoginException; import com.liato.bankdroid.db.DBAdapter; import com.liato.bankdroid.liveview.LiveViewService; +import com.liato.bankdroid.utils.LoggingUtils; import android.app.NotificationManager; import android.app.PendingIntent; @@ -289,7 +290,7 @@ public class AutoRefreshService extends Service { @Override protected Void doInBackground(final String... args) { - errors = new ArrayList(); + errors = new ArrayList<>(); Boolean refreshWidgets = false; final List banks = getBanks(); if (banks.isEmpty()) { @@ -300,7 +301,7 @@ public class AutoRefreshService extends Service { BigDecimal diff; BigDecimal minDelta = new BigDecimal(prefs.getString("notify_min_delta", "0")); - final HashMap accounts = new HashMap(); + final HashMap accounts = new HashMap<>(); for (final Bank bank : banks) { if (prefs.getBoolean("debug_mode", false) @@ -311,6 +312,7 @@ public class AutoRefreshService extends Service { continue; } if (bank.isDisabled()) { + LoggingUtils.logDisabledBank(bank); continue; } try { @@ -320,6 +322,7 @@ public class AutoRefreshService extends Service { accounts.put(account.getId(), account); } bank.update(); + diff = currentBalance.subtract(bank.getBalance()); if (diff.compareTo(BigDecimal.ZERO) != 0) { @@ -373,6 +376,9 @@ public class AutoRefreshService extends Service { if (prefs.getBoolean( "autoupdates_transactions_enabled", true)) { bank.updateAllTransactions(); + LoggingUtils.logBankUpdate(bank, true); + } else { + LoggingUtils.logBankUpdate(bank, false); } } bank.closeConnection(); diff --git app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java index f0cf5dc..bea4936 100644 --- app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java +++ app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java @@ -4,6 +4,8 @@ import com.crashlytics.android.Crashlytics; import com.crashlytics.android.answers.Answers; import com.crashlytics.android.answers.CustomEvent; import com.liato.bankdroid.BuildConfig; +import com.liato.bankdroid.banking.Account; +import com.liato.bankdroid.banking.Bank; import android.content.Context; import android.text.TextUtils; @@ -40,9 +42,41 @@ public class LoggingUtils { } public static void logCustom(CustomEvent event) { - if (isCrashlyticsEnabled()) { - event.putCustomAttribute("App Version", BuildConfig.VERSION_NAME); - Answers.getInstance().logCustom(event); + if (!isCrashlyticsEnabled()) { + return; + } + + event.putCustomAttribute("App Version", BuildConfig.VERSION_NAME); + Answers.getInstance().logCustom(event); + } + + public static void logDisabledBank(Bank bank) { + if (!isCrashlyticsEnabled()) { + return; + } + + logCustom(new CustomEvent("Disabled Bank"). + putCustomAttribute("Name", bank.getDisplayName())); + } + + public static void logBankUpdate(Bank bank, boolean withTransactions) { + if (!isCrashlyticsEnabled()) { + return; + } + + logCustom(new CustomEvent("Bank Updated"). + putCustomAttribute("Name", bank.getDisplayName()). + putCustomAttribute("With Transactions", Boolean.toString(withTransactions))); + + boolean hasTransactions = false; + for (Account account : bank.getAccounts()) { + if (account.getTransactions() != null && !account.getTransactions().isEmpty()) { + hasTransactions = true; + } + } + if (withTransactions && !hasTransactions) { + logCustom(new CustomEvent("Bank Without Transactions"). + putCustomAttribute("Name", bank.getDisplayName())); } } commit cb2027c3121ca4c37ef8ef1c45656d27e52e37c3 Author: Johan Walles Date: Sun Oct 30 17:12:07 2016 +0100 Require closing result cursors diff --git app/src/main/java/com/liato/bankdroid/banking/BankFactory.java app/src/main/java/com/liato/bankdroid/banking/BankFactory.java index d61e0ce..db905c8 100644 --- app/src/main/java/com/liato/bankdroid/banking/BankFactory.java +++ app/src/main/java/com/liato/bankdroid/banking/BankFactory.java @@ -84,10 +84,10 @@ public class BankFactory { ArrayList banks = new ArrayList<>(); DBAdapter db = new DBAdapter(context); Cursor c = db.fetchBanks(); - if (c == null) { - return banks; - } try { + if (c == null || c.getCount() == 0) { + return banks; + } while (!c.isLast() && !c.isAfterLast()) { c.moveToNext(); try { @@ -109,7 +109,9 @@ public class BankFactory { } } } finally { - c.close(); + if (c != null) { + c.close(); + } } return banks; } @@ -118,38 +120,50 @@ public class BankFactory { public static Account accountFromDb(Context context, String accountId, boolean loadTransactions) { DBAdapter db = new DBAdapter(context); - Cursor c = db.getAccount(accountId); + Cursor ac = db.getAccount(accountId); + + Account account; + try { + if (ac == null || ac.isClosed() || (ac.isBeforeFirst() && ac.isAfterLast())) { + return null; + } - if (c == null || c.isClosed() || (c.isBeforeFirst() && c.isAfterLast())) { - return null; + account = new Account(ac.getString(ac.getColumnIndex("name")), + new BigDecimal(ac.getString(ac.getColumnIndex("balance"))), + ac.getString(ac.getColumnIndex("id")).split("_", 2)[1], + ac.getLong(ac.getColumnIndex("bankid")), + ac.getInt(ac.getColumnIndex("acctype"))); + account.setHidden(ac.getInt(ac.getColumnIndex("hidden")) == 1); + account.setNotify(ac.getInt(ac.getColumnIndex("notify")) == 1); + account.setCurrency(ac.getString(ac.getColumnIndex("currency"))); + account.setAliasfor(ac.getString(ac.getColumnIndex("aliasfor"))); + } finally { + if (ac != null) { + ac.close(); + } } - Account account = new Account(c.getString(c.getColumnIndex("name")), - new BigDecimal(c.getString(c.getColumnIndex("balance"))), - c.getString(c.getColumnIndex("id")).split("_", 2)[1], - c.getLong(c.getColumnIndex("bankid")), - c.getInt(c.getColumnIndex("acctype"))); - account.setHidden(c.getInt(c.getColumnIndex("hidden")) == 1); - account.setNotify(c.getInt(c.getColumnIndex("notify")) == 1); - account.setCurrency(c.getString(c.getColumnIndex("currency"))); - account.setAliasfor(c.getString(c.getColumnIndex("aliasfor"))); - c.close(); if (loadTransactions) { ArrayList transactions = new ArrayList<>(); String fromAccount = accountId; if (account.getAliasfor() != null && account.getAliasfor().length() > 0) { fromAccount = Long.toString(account.getBankDbId()) + "_" + account.getAliasfor(); } - c = db.fetchTransactions(fromAccount); - if (!(c == null || c.isClosed() || (c.isBeforeFirst() && c.isAfterLast()))) { - while (!c.isLast() && !c.isAfterLast()) { - c.moveToNext(); - transactions.add(new Transaction(c.getString(c.getColumnIndex("transdate")), - c.getString(c.getColumnIndex("btransaction")), - new BigDecimal(c.getString(c.getColumnIndex("amount"))), - c.getString(c.getColumnIndex("currency")))); + Cursor tc = db.fetchTransactions(fromAccount); + try { + if (!(tc == null || tc.isClosed() || (tc.isBeforeFirst() && tc.isAfterLast()))) { + while (!tc.isLast() && !tc.isAfterLast()) { + tc.moveToNext(); + transactions.add(new Transaction(tc.getString(tc.getColumnIndex("transdate")), + tc.getString(tc.getColumnIndex("btransaction")), + new BigDecimal(tc.getString(tc.getColumnIndex("amount"))), + tc.getString(tc.getColumnIndex("currency")))); + } + } + } finally { + if (tc != null) { + tc.close(); } - c.close(); } account.setTransactions(transactions); } @@ -160,10 +174,10 @@ public class BankFactory { ArrayList accounts = new ArrayList<>(); DBAdapter db = new DBAdapter(context); Cursor c = db.fetchAccounts(bankId); - if (c == null) { - return accounts; - } try { + if (c == null || c.getCount() == 0) { + return accounts; + } while (!c.isLast() && !c.isAfterLast()) { c.moveToNext(); try { @@ -183,7 +197,9 @@ public class BankFactory { } } } finally { - c.close(); + if (c != null) { + c.close(); + } } return accounts; } @@ -193,10 +209,10 @@ public class BankFactory { Map decryptedProperties = new HashMap<>(); DBAdapter db = new DBAdapter(context); Cursor c = db.fetchProperties(Long.toString(bankId)); - if(c == null) { - return properties; - } try { + if (c == null || c.getCount() == 0) { + return properties; + } while (!c.isLast() && !c.isAfterLast()) { c.moveToNext(); String key = c.getString(c.getColumnIndex(Database.PROPERTY_KEY)); @@ -214,7 +230,9 @@ public class BankFactory { properties.put(key, value); } } finally { - c.close(); + if (c != null) { + c.close(); + } } storeDecryptedProperties(context, bankId, decryptedProperties); diff --git app/src/main/java/com/liato/bankdroid/db/DatabaseHelper.java app/src/main/java/com/liato/bankdroid/db/DatabaseHelper.java index ea72715..e4b166a 100644 --- app/src/main/java/com/liato/bankdroid/db/DatabaseHelper.java +++ app/src/main/java/com/liato/bankdroid/db/DatabaseHelper.java @@ -105,33 +105,38 @@ final public class DatabaseHelper extends SQLiteOpenHelper { // Add username, password and extras fields to properties table. Cursor c = db.query(tempTable, null, null, null,null,null,null); - if (!(c == null || c.isClosed() || (c.isBeforeFirst() && c.isAfterLast()))) { - while (!c.isLast() && !c.isAfterLast()) { - c.moveToNext(); - long id = c.getLong(c.getColumnIndex(LegacyDatabase.BANK_ID)); - - ContentValues usernameProperty = new ContentValues(); - usernameProperty.put(PROPERTY_CONNECTION_ID, id); - usernameProperty.put(PROPERTY_KEY, LegacyProviderConfiguration.USERNAME); - usernameProperty.put(PROPERTY_VALUE, c.getString(c.getColumnIndex(LegacyDatabase.BANK_USERNAME))); - db.insert(PROPERTY_TABLE_NAME, null, usernameProperty); - - ContentValues passwordProperty = new ContentValues(); - passwordProperty.put(PROPERTY_CONNECTION_ID, id); - passwordProperty.put(PROPERTY_KEY, LegacyProviderConfiguration.PASSWORD); - passwordProperty.put(PROPERTY_VALUE, c.getString(c.getColumnIndex(LegacyDatabase.BANK_PASSWORD))); - db.insert(PROPERTY_TABLE_NAME, null, passwordProperty); - - String extras = c.getString(c.getColumnIndex(LegacyDatabase.BANK_EXTRAS)); - if(extras != null && !extras.isEmpty()) { - ContentValues extrasProperty = new ContentValues(); - extrasProperty.put(PROPERTY_CONNECTION_ID, id); - extrasProperty.put(PROPERTY_KEY, LegacyProviderConfiguration.EXTRAS); - extrasProperty.put(PROPERTY_VALUE, extras); - db.insert(PROPERTY_TABLE_NAME, null, extrasProperty); + try { + if (!(c == null || c.isClosed() || (c.isBeforeFirst() && c.isAfterLast()))) { + while (!c.isLast() && !c.isAfterLast()) { + c.moveToNext(); + long id = c.getLong(c.getColumnIndex(LegacyDatabase.BANK_ID)); + + ContentValues usernameProperty = new ContentValues(); + usernameProperty.put(PROPERTY_CONNECTION_ID, id); + usernameProperty.put(PROPERTY_KEY, LegacyProviderConfiguration.USERNAME); + usernameProperty.put(PROPERTY_VALUE, c.getString(c.getColumnIndex(LegacyDatabase.BANK_USERNAME))); + db.insert(PROPERTY_TABLE_NAME, null, usernameProperty); + + ContentValues passwordProperty = new ContentValues(); + passwordProperty.put(PROPERTY_CONNECTION_ID, id); + passwordProperty.put(PROPERTY_KEY, LegacyProviderConfiguration.PASSWORD); + passwordProperty.put(PROPERTY_VALUE, c.getString(c.getColumnIndex(LegacyDatabase.BANK_PASSWORD))); + db.insert(PROPERTY_TABLE_NAME, null, passwordProperty); + + String extras = c.getString(c.getColumnIndex(LegacyDatabase.BANK_EXTRAS)); + if (extras != null && !extras.isEmpty()) { + ContentValues extrasProperty = new ContentValues(); + extrasProperty.put(PROPERTY_CONNECTION_ID, id); + extrasProperty.put(PROPERTY_KEY, LegacyProviderConfiguration.EXTRAS); + extrasProperty.put(PROPERTY_VALUE, extras); + db.insert(PROPERTY_TABLE_NAME, null, extrasProperty); + } } } - c.close(); + } finally { + if (c != null) { + c.close(); + } } db.execSQL("DROP TABLE " + tempTable); } diff --git config/quality/pmd/pmd-ruleset.xml config/quality/pmd/pmd-ruleset.xml index 3109e6b..19538ce 100644 --- config/quality/pmd/pmd-ruleset.xml +++ config/quality/pmd/pmd-ruleset.xml @@ -25,6 +25,13 @@ + + + + + + + diff --git tools/update-suppressions.sh tools/update-suppressions.sh index 4148ea6..53ff534 100755 --- tools/update-suppressions.sh +++ tools/update-suppressions.sh @@ -64,6 +64,13 @@ function set_pmd_suppressions() { + + + + + + + commit 63188088e36f3e351830d0e5b1f8b0b576fa5b54 Author: Johan Walles Date: Thu Oct 27 22:02:02 2016 +0200 Ensure on-disk passwords are unencrypted This is a step towards removing password encryption alltogether. The background is that it's broken on Androin Nougat anyway, and that it didn't provide any extra security before that either. Since Bankdroid needs to send plain text passwords to the banks, it must be possible to retrieve the plain text passwords automatically. And if the passwords are encrypted on disk, Bankdroid needs to have the key. And if Bankdroid stores both the key and the encrypted password on the phone, a determined attacker could get both anyway, and the encryption is useless. The only thing the encryption has protected against is a user rooting their own device and retrieving their own plain text passwords. This would enable the attacker to read their own account balance from the bank. Which they likely already could even before this change... This change also disables an Android Lint check whose outcome changes over time; these checks are impossible to maintain. And we fixed some warnings. diff --git app/src/main/java/com/liato/bankdroid/banking/BankFactory.java app/src/main/java/com/liato/bankdroid/banking/BankFactory.java index 9a7ea9c..d61e0ce 100644 --- app/src/main/java/com/liato/bankdroid/banking/BankFactory.java +++ app/src/main/java/com/liato/bankdroid/banking/BankFactory.java @@ -16,15 +16,20 @@ package com.liato.bankdroid.banking; +import com.crashlytics.android.answers.CustomEvent; import com.liato.bankdroid.banking.exceptions.BankException; import com.liato.bankdroid.db.Crypto; import com.liato.bankdroid.db.DBAdapter; import com.liato.bankdroid.db.Database; +import com.liato.bankdroid.db.DatabaseHelper; +import com.liato.bankdroid.utils.LoggingUtils; import net.sf.andhsli.hotspotlogin.SimpleCrypto; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import android.support.annotation.Nullable; import java.math.BigDecimal; @@ -37,7 +42,7 @@ import timber.log.Timber; public class BankFactory { - public static Bank fromBanktypeId(int id, Context context) throws BankException { + private static Bank fromBanktypeId(int id, Context context) throws BankException { return LegacyBankFactory.fromBanktypeId(id, context); } @@ -58,7 +63,7 @@ public class BankFactory { bank.setProperties(loadProperties(id, context)); bank.setData(new BigDecimal(c.getString(c.getColumnIndex("balance"))), - (c.getInt(c.getColumnIndex("disabled")) == 0 ? false : true), + (c.getInt(c.getColumnIndex("disabled")) != 0), c.getLong(c.getColumnIndex("_id")), c.getString(c.getColumnIndex("currency")), c.getString(c.getColumnIndex("custname")), @@ -76,36 +81,40 @@ public class BankFactory { } public static ArrayList banksFromDb(Context context, boolean loadAccounts) { - ArrayList banks = new ArrayList(); + ArrayList banks = new ArrayList<>(); DBAdapter db = new DBAdapter(context); Cursor c = db.fetchBanks(); - if (c == null || c.getCount() == 0) { + if (c == null) { return banks; } - while (!c.isLast() && !c.isAfterLast()) { - c.moveToNext(); - try { - Bank bank = fromBanktypeId(c.getInt(c.getColumnIndex("banktype")), context); - long id = c.getLong(c.getColumnIndex("_id")); - bank.setProperties(loadProperties(id, context)); - bank.setData(new BigDecimal(c.getString(c.getColumnIndex("balance"))), - (c.getInt(c.getColumnIndex("disabled")) == 0 ? false : true), - id, - c.getString(c.getColumnIndex("currency")), - c.getString(c.getColumnIndex("custname")), - c.getInt(c.getColumnIndex("hideAccounts"))); - if (loadAccounts) { - bank.setAccounts(accountsFromDb(context, bank.getDbId())); + try { + while (!c.isLast() && !c.isAfterLast()) { + c.moveToNext(); + try { + Bank bank = fromBanktypeId(c.getInt(c.getColumnIndex("banktype")), context); + long id = c.getLong(c.getColumnIndex("_id")); + bank.setProperties(loadProperties(id, context)); + bank.setData(new BigDecimal(c.getString(c.getColumnIndex("balance"))), + (c.getInt(c.getColumnIndex("disabled")) != 0), + id, + c.getString(c.getColumnIndex("currency")), + c.getString(c.getColumnIndex("custname")), + c.getInt(c.getColumnIndex("hideAccounts"))); + if (loadAccounts) { + bank.setAccounts(accountsFromDb(context, bank.getDbId())); + } + banks.add(bank); + } catch (BankException e) { + Timber.w(e, "BankFactory.banksFromDb()"); } - banks.add(bank); - } catch (BankException e) { - Timber.w(e, "BankFactory.banksFromDb()"); } + } finally { + c.close(); } - c.close(); return banks; } + @Nullable public static Account accountFromDb(Context context, String accountId, boolean loadTransactions) { DBAdapter db = new DBAdapter(context); @@ -120,13 +129,13 @@ public class BankFactory { c.getString(c.getColumnIndex("id")).split("_", 2)[1], c.getLong(c.getColumnIndex("bankid")), c.getInt(c.getColumnIndex("acctype"))); - account.setHidden(c.getInt(c.getColumnIndex("hidden")) == 1 ? true : false); - account.setNotify(c.getInt(c.getColumnIndex("notify")) == 1 ? true : false); + account.setHidden(c.getInt(c.getColumnIndex("hidden")) == 1); + account.setNotify(c.getInt(c.getColumnIndex("notify")) == 1); account.setCurrency(c.getString(c.getColumnIndex("currency"))); account.setAliasfor(c.getString(c.getColumnIndex("aliasfor"))); c.close(); if (loadTransactions) { - ArrayList transactions = new ArrayList(); + ArrayList transactions = new ArrayList<>(); String fromAccount = accountId; if (account.getAliasfor() != null && account.getAliasfor().length() > 0) { fromAccount = Long.toString(account.getBankDbId()) + "_" + account.getAliasfor(); @@ -147,56 +156,114 @@ public class BankFactory { return account; } - public static ArrayList accountsFromDb(Context context, long bankId) { - ArrayList accounts = new ArrayList(); + private static ArrayList accountsFromDb(Context context, long bankId) { + ArrayList accounts = new ArrayList<>(); DBAdapter db = new DBAdapter(context); Cursor c = db.fetchAccounts(bankId); - if (c == null || c.getCount() == 0) { + if (c == null) { return accounts; } - while (!c.isLast() && !c.isAfterLast()) { - c.moveToNext(); - try { - Account account = new Account(c.getString(c.getColumnIndex("name")), - new BigDecimal(c.getString(c.getColumnIndex("balance"))), - c.getString(c.getColumnIndex("id")).split("_", 2)[1], - c.getLong(c.getColumnIndex("bankid")), - c.getInt(c.getColumnIndex("acctype"))); - account.setHidden(c.getInt(c.getColumnIndex("hidden")) == 1 ? true : false); - account.setNotify(c.getInt(c.getColumnIndex("notify")) == 1 ? true : false); - account.setCurrency(c.getString(c.getColumnIndex("currency"))); - account.setAliasfor(c.getString(c.getColumnIndex("aliasfor"))); - accounts.add(account); - } catch (ArrayIndexOutOfBoundsException e) { - // Probably an old Avanza account - Timber.w(e, "Attempted to load an account without an ID: %d", bankId); + try { + while (!c.isLast() && !c.isAfterLast()) { + c.moveToNext(); + try { + Account account = new Account(c.getString(c.getColumnIndex("name")), + new BigDecimal(c.getString(c.getColumnIndex("balance"))), + c.getString(c.getColumnIndex("id")).split("_", 2)[1], + c.getLong(c.getColumnIndex("bankid")), + c.getInt(c.getColumnIndex("acctype"))); + account.setHidden(c.getInt(c.getColumnIndex("hidden")) == 1); + account.setNotify(c.getInt(c.getColumnIndex("notify")) == 1); + account.setCurrency(c.getString(c.getColumnIndex("currency"))); + account.setAliasfor(c.getString(c.getColumnIndex("aliasfor"))); + accounts.add(account); + } catch (ArrayIndexOutOfBoundsException e) { + // Probably an old Avanza account + Timber.w(e, "Attempted to load an account without an ID: %d", bankId); + } } + } finally { + c.close(); } - c.close(); return accounts; } - private static Map loadProperties(long id, Context context) { + private static Map loadProperties(long bankId, Context context) { Map properties = new HashMap<>(); + Map decryptedProperties = new HashMap<>(); DBAdapter db = new DBAdapter(context); - Cursor c = db.fetchProperties(Long.toString(id)); - if(c == null || c.getCount() == 0) { + Cursor c = db.fetchProperties(Long.toString(bankId)); + if(c == null) { return properties; } - while(!c.isLast() && !c.isAfterLast()) { - c.moveToNext(); - String key = c.getString(c.getColumnIndex(Database.PROPERTY_KEY)); - String value = c.getString(c.getColumnIndex(Database.PROPERTY_VALUE)); - if(LegacyProviderConfiguration.PASSWORD.equals(key)) { - try { - value = SimpleCrypto.decrypt(Crypto.getKey(), value); - } catch (Exception e) { - Timber.w(e, "Failed decrypting bank properties"); + try { + while (!c.isLast() && !c.isAfterLast()) { + c.moveToNext(); + String key = c.getString(c.getColumnIndex(Database.PROPERTY_KEY)); + String value = c.getString(c.getColumnIndex(Database.PROPERTY_VALUE)); + if (LegacyProviderConfiguration.PASSWORD.equals(key)) { + try { + value = SimpleCrypto.decrypt(Crypto.getKey(), value); + decryptedProperties.put(key, value); + } catch (Exception e) { + Timber.i("%s %s", + "Failed decrypting bank properties.", + "This usually means they are unencrypted, which is exactly what we want them to be."); + } } + properties.put(key, value); } - properties.put(key, value); + } finally { + c.close(); } - c.close(); + + storeDecryptedProperties(context, bankId, decryptedProperties); + return properties; } + + /** + * Stores decrypted passwords on disk. + *

+ * This is a step in removing password encryption alltogether. + *

+ * The background is that it's broken on Androin Nougat anyway, and that it + * didn't provide any extra security before that either. + *

+ * Since Bankdroid needs to send plain text passwords to the banks, it must + * be possible to retrieve the plain text passwords automatically. And if the + * passwords are encrypted on disk, Bankdroid needs to have the key. And if + * Bankdroid stores both the key and the encrypted password on the phone, a + * determined attacker could get both anyway, and the encryption is useless. + *

+ * The only thing the encryption has protected against is a using rooting + * their own device and retrieving their own plain text passwords. This would + * enable the attacker to reaa their own account balance from the bank. Which + * they likely already could even before this change... + */ + private static void storeDecryptedProperties( + Context context, long bankId, Map decryptedProperties) + { + if (decryptedProperties.isEmpty()) { + return; + } + + Timber.i("Storing %d decrypted properties...", decryptedProperties.size()); + SQLiteDatabase db = DatabaseHelper.getHelper(context).getWritableDatabase(); + for (Map.Entry property : decryptedProperties.entrySet()) { + String value = property.getValue(); + if (value != null && !value.isEmpty()) { + ContentValues propertyValues = new ContentValues(); + propertyValues.put(Database.PROPERTY_KEY, property.getKey()); + propertyValues.put(Database.PROPERTY_VALUE, value); + propertyValues.put(Database.PROPERTY_CONNECTION_ID, bankId); + db.insertWithOnConflict( + Database.PROPERTY_TABLE_NAME, null, propertyValues, + SQLiteDatabase.CONFLICT_REPLACE); + } + } + Timber.i("%d decrypted properties stored", decryptedProperties.size()); + + LoggingUtils.logCustom(new CustomEvent("Passwords Decrypted")); + } } diff --git app/src/main/java/com/liato/bankdroid/db/DBAdapter.java app/src/main/java/com/liato/bankdroid/db/DBAdapter.java index 240669b..215c59f 100644 --- app/src/main/java/com/liato/bankdroid/db/DBAdapter.java +++ app/src/main/java/com/liato/bankdroid/db/DBAdapter.java @@ -18,15 +18,13 @@ package com.liato.bankdroid.db; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; -import com.liato.bankdroid.banking.LegacyProviderConfiguration; import com.liato.bankdroid.banking.Transaction; -import net.sf.andhsli.hotspotlogin.SimpleCrypto; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.Nullable; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -34,8 +32,6 @@ import java.util.Calendar; import java.util.List; import java.util.Map; -import timber.log.Timber; - public class DBAdapter { @@ -169,13 +165,6 @@ public class DBAdapter { for(Map.Entry property : properties.entrySet()) { String value = property.getValue(); if(value != null && !value.isEmpty()) { - if (LegacyProviderConfiguration.PASSWORD.equals(property.getKey())) { - try { - value = SimpleCrypto.encrypt(Crypto.getKey(), bank.getPassword()); - } catch (Exception e) { - Timber.e(e, "Could not encrypt password."); - } - } ContentValues propertyValues = new ContentValues(); propertyValues.put(Database.PROPERTY_KEY, property.getKey()); propertyValues.put(Database.PROPERTY_VALUE, value); @@ -227,6 +216,7 @@ public class DBAdapter { mDb.update("banks", initialValues, "_id=" + bankId, null); } + @Nullable public Cursor getBank(String bankId) { Cursor c = mDb.query("banks", new String[]{"_id", "balance", "banktype", "disabled", @@ -238,10 +228,12 @@ public class DBAdapter { return c; } + @Nullable public Cursor getBank(long bankId) { return getBank(Long.toString(bankId)); } + @Nullable public Cursor getAccount(String id) { Cursor c = mDb.query("accounts", new String[]{"id", "balance", "name", "bankid", "acctype", "hidden", "notify", diff --git app/src/main/java/com/liato/bankdroid/db/Database.java app/src/main/java/com/liato/bankdroid/db/Database.java index 0558483..93d3c14 100644 --- app/src/main/java/com/liato/bankdroid/db/Database.java +++ app/src/main/java/com/liato/bankdroid/db/Database.java @@ -6,7 +6,7 @@ public class Database { static final int DATABASE_VERSION = 12; - static final String PROPERTY_TABLE_NAME = "connection_properties"; + public static final String PROPERTY_TABLE_NAME = "connection_properties"; public static final String PROPERTY_CONNECTION_ID = "connection_id"; public static final String PROPERTY_KEY = "property"; public static final String PROPERTY_VALUE = "value"; diff --git app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java index 4867459..f0cf5dc 100644 --- app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java +++ app/src/main/java/com/liato/bankdroid/utils/LoggingUtils.java @@ -1,12 +1,14 @@ package com.liato.bankdroid.utils; +import com.crashlytics.android.Crashlytics; +import com.crashlytics.android.answers.Answers; +import com.crashlytics.android.answers.CustomEvent; +import com.liato.bankdroid.BuildConfig; + import android.content.Context; import android.text.TextUtils; import android.util.Log; -import com.crashlytics.android.Crashlytics; -import com.liato.bankdroid.BuildConfig; - import io.fabric.sdk.android.Fabric; import timber.log.Timber; @@ -37,6 +39,13 @@ public class LoggingUtils { !EmulatorUtils.RUNNING_ON_EMULATOR; } + public static void logCustom(CustomEvent event) { + if (isCrashlyticsEnabled()) { + event.putCustomAttribute("App Version", BuildConfig.VERSION_NAME); + Answers.getInstance().logCustom(event); + } + } + private static class CrashlyticsTree extends Timber.Tree { CrashlyticsTree(Context context) { Fabric.with(context, new Crashlytics()); diff --git app/src/main/java/net/sf/andhsli/hotspotlogin/SimpleCrypto.java app/src/main/java/net/sf/andhsli/hotspotlogin/SimpleCrypto.java index d4a1995..88e4154 100644 --- app/src/main/java/net/sf/andhsli/hotspotlogin/SimpleCrypto.java +++ app/src/main/java/net/sf/andhsli/hotspotlogin/SimpleCrypto.java @@ -22,18 +22,14 @@ import javax.crypto.spec.SecretKeySpec; * String cleartext = SimpleCrypto.decrypt(masterpassword, crypto) * * + * @deprecated Broken + * on Android Nougat, + * considered + * broken by Android Lint even before then. + * * @author ferenc.hechler */ public class SimpleCrypto { - - private final static String HEX = "0123456789ABCDEF"; - - public static String encrypt(String seed, String cleartext) throws Exception { - byte[] rawKey = getRawKey(StringUtils.getBytes(seed)); - byte[] result = encrypt(rawKey, StringUtils.getBytes(cleartext)); - return toHex(result); - } - public static String decrypt(String seed, String encrypted) throws Exception { byte[] rawKey = getRawKey(StringUtils.getBytes(seed)); byte[] enc = toByte(encrypted); @@ -56,14 +52,6 @@ public class SimpleCrypto { return raw; } - private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception { - SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); - Cipher cipher = Cipher.getInstance("AES"); - cipher.init(Cipher.ENCRYPT_MODE, skeySpec); - byte[] encrypted = cipher.doFinal(clear); - return encrypted; - } - private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception { SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); Cipher cipher = Cipher.getInstance("AES"); @@ -72,7 +60,7 @@ public class SimpleCrypto { return decrypted; } - public static byte[] toByte(String hexString) { + private static byte[] toByte(String hexString) { int len = hexString.length() / 2; byte[] result = new byte[len]; for (int i = 0; i < len; i++) { @@ -80,19 +68,4 @@ public class SimpleCrypto { } return result; } - - public static String toHex(byte[] buf) { - if (buf == null) { - return ""; - } - StringBuffer result = new StringBuffer(2 * buf.length); - for (int i = 0; i < buf.length; i++) { - appendHex(result, buf[i]); - } - return result.toString(); - } - - private static void appendHex(StringBuffer sb, byte b) { - sb.append(HEX.charAt((b >> 4) & 0x0f)).append(HEX.charAt(b & 0x0f)); - } } diff --git config/quality/lint/lint.xml config/quality/lint/lint.xml index 8195ce2..f81c4ca 100644 --- config/quality/lint/lint.xml +++ config/quality/lint/lint.xml @@ -30,6 +30,7 @@ + commit eabc99cfadbf724f5d3129bc7b011587c6fd43d0 Author: Robert Högberg Date: Sat Oct 29 20:59:00 2016 +0200 Bioklubben: Update start web page http://bioklubben.sf.se -> https://bioklubben.sf.se The http version no longer exists diff --git CHANGELOG CHANGELOG index fa02959..970ea23 100644 --- CHANGELOG +++ CHANGELOG @@ -1,5 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. +Not yet released +* Bioklubben: Use https. It's secure and http page no longer exists. + v1.9.11 (2016-10-26) * Warn about disabled banks in the transactions list * Show warning text about disabled banks in the main activity diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bioklubben.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bioklubben.java index e31540f..df9e368 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bioklubben.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Bioklubben.java @@ -39,6 +39,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import eu.nullbyte.android.urllib.CertificateReader; import eu.nullbyte.android.urllib.Urllib; public class Bioklubben extends Bank { @@ -47,7 +48,7 @@ public class Bioklubben extends Bank { private static final String NAME_SHORT = "bioklubben"; - private static final String URL = "http://bioklubben.sf.se/Start.aspx"; + private static final String URL = "https://bioklubben.sf.se/Start.aspx"; private static final int BANKTYPE_ID = Bank.BIOKLUBBEN; @@ -76,9 +77,9 @@ public class Bioklubben extends Bank { @Override protected LoginPackage preLogin() throws BankException, IOException { - urlopen = new Urllib(context); + urlopen = new Urllib(context, CertificateReader.getCertificates(context, R.raw.cert_bioklubben)); urlopen.setAllowCircularRedirects(true); - response = urlopen.open("http://bioklubben.sf.se/Start.aspx"); + response = urlopen.open(URL); Document d = Jsoup.parse(response); Element e = d.getElementById("__VIEWSTATE"); @@ -109,7 +110,7 @@ public class Bioklubben extends Bank { postData.add( new BasicNameValuePair("ctl00$ContentPlaceHolder1$LoginUserControl$PasswordTextBox", getPassword())); - return new LoginPackage(urlopen, postData, response, "http://bioklubben.sf.se/Start.aspx"); + return new LoginPackage(urlopen, postData, response, URL); } public Urllib login() throws LoginException, BankException, IOException { @@ -129,7 +130,7 @@ public class Bioklubben extends Bank { } urlopen = login(); Document d = Jsoup.parse(urlopen.open( - "http://bioklubben.sf.se/MyPurchases.aspx?ParentTreeID=1&TreeID=1")); + "https://bioklubben.sf.se/MyPurchases.aspx?ParentTreeID=1&TreeID=1")); Element e = d.getElementById("ctl00_ContentPlaceHolder1_BonusPointsLabel"); if (e == null) { throw new BankException( diff --git bankdroid-legacy/src/main/res/raw/cert_bioklubben.pem bankdroid-legacy/src/main/res/raw/cert_bioklubben.pem new file mode 100644 index 0000000..a01ed1e --- /dev/null +++ bankdroid-legacy/src/main/res/raw/cert_bioklubben.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIF4TCCBMmgAwIBAgIQVPm9MeoE7SQFdlULvDppQjANBgkqhkiG9w0BAQsFADBE +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMU +R2VvVHJ1c3QgU1NMIENBIC0gRzMwHhcNMTYwMjAxMDAwMDAwWhcNMTcwMTMxMjM1 +OTU5WjBqMQswCQYDVQQGEwJTRTEYMBYGA1UECAwPU1RPQ0tIT0xNUyBMw6ROMQ4w +DAYDVQQHDAVTb2xuYTESMBAGA1UEChQJU0YgQmlvIEFCMQswCQYDVQQLDAJJVDEQ +MA4GA1UEAxQHKi5zZi5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKLUmrPm9g2Z12yVD9jnGgk3C2xrdOYqjfjFGOrU2SfTaI1L8POt2s9YspYcKkFG +5EB7Iv3aIwL0TccsbWT1PaiVvT7hgU94n5fYjjHlMiVMdPpHunGj9KqO12/zz++e +Qa8xITx5AW3S7CYHIBqxPIU43/6ukXcmsGe4ngbHxhl7nfWcgWT/qxUA7guWqxON +KNaNOsw4KeJizxhURT52nf5+dJIZ9j/3x8vu+WC8kJ9LQzKdFuFOZ05/Ivwj+WcI +SbOOaisxaQCdkFMDtTfWPBX9CjEvAEqYsthFj2trvxZYgvnN6zR7eRdAH2chgrh9 +//ljApp85rzAVClGncBZKq0CAwEAAaOCAqcwggKjMBkGA1UdEQQSMBCCByouc2Yu +c2WCBXNmLnNlMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgWgMCsGA1UdHwQkMCIw +IKAeoByGGmh0dHA6Ly9nbi5zeW1jYi5jb20vZ24uY3JsMIGdBgNVHSAEgZUwgZIw +gY8GBmeBDAECAjCBhDA/BggrBgEFBQcCARYzaHR0cHM6Ly93d3cuZ2VvdHJ1c3Qu +Y29tL3Jlc291cmNlcy9yZXBvc2l0b3J5L2xlZ2FsMEEGCCsGAQUFBwICMDUMM2h0 +dHBzOi8vd3d3Lmdlb3RydXN0LmNvbS9yZXNvdXJjZXMvcmVwb3NpdG9yeS9sZWdh +bDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHwYDVR0jBBgwFoAU0m/3 +lvSFP3I8MH0j2oV4m6N8WnwwVwYIKwYBBQUHAQEESzBJMB8GCCsGAQUFBzABhhNo +dHRwOi8vZ24uc3ltY2QuY29tMCYGCCsGAQUFBzAChhpodHRwOi8vZ24uc3ltY2Iu +Y29tL2duLmNydDCCAQMGCisGAQQB1nkCBAIEgfQEgfEA7wB2AN3rHSt6DU+mIIuB +rYFocH4ujp0B1VyIjT0RxM227L7MAAABUp0mwQkAAAQDAEcwRQIgequqjt1sy5zl +gCK93rW9xs6YxLs1bCfp96HIYhZ49kACIQDhjtfaakCmfkQ9UDMBp7WpE1kA4PFl +jNIZnsUqdf4meAB1AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB +Up0mvTEAAAQDAEYwRAIgR0W9StBOqzQ85oWMWO6h/P4M+p77PJQrjgdyQlcePVUC +IHRoUg4Ywdh9ookJITb8K9dn5MGikr7I6om9QwWNBb+6MA0GCSqGSIb3DQEBCwUA +A4IBAQAlL/o5EhDh5lNkjJkxPzFnX4xuf2/PtyepQmlE9f0/o1ICgUgHDy2Vs2r/ +yCWyjKSl+APsd0AOuKJtxXm9s+TtHDRAysODUVhSQYLjWOZoWrpQ3d0bDhDHhy8S +3AaqoE65tRXbZHZ7sSB95XpKgPARwn918y1Oe6aTEE8JvSV/cXVyHbng5RB2ddJP +OFXUmh+/uLgbPQ/V4wPipTbWLDgA+D7XuyQVzCNvhPD4A8w0oBi/HxjjYMdPByoK +LjVEmYCdw91c/h6Avm9AUK92XGXkNdiML+hBEY2YJQZghH1yz98hbqfm0x5b43GI +Lk4ymn/K6HZMbtsrXfcWX8N3UmO5 +-----END CERTIFICATE----- +bioklubben.sf.se:443 commit 13e94fcea34623cfd5d3fa734ef1c855f190f7ba Author: Robert Högberg Date: Fri Oct 28 00:27:16 2016 +0200 Remove stray Villabanken logo diff --git assets/villabanken.psd assets/villabanken.psd deleted file mode 100644 index 2efedbb..0000000 Binary files assets/villabanken.psd and /dev/null differ commit 94072b6173bdcbe1f96146cf1587cdc359ea3139 Author: Johan Walles Date: Thu Oct 27 06:19:09 2016 +0200 Fix empty catch blocks This should improve the Crashlytics statistics. diff --git app/src/main/java/com/liato/bankdroid/TimePreference.java app/src/main/java/com/liato/bankdroid/TimePreference.java index 44586c5..fc13af3 100644 --- app/src/main/java/com/liato/bankdroid/TimePreference.java +++ app/src/main/java/com/liato/bankdroid/TimePreference.java @@ -8,6 +8,8 @@ import android.util.AttributeSet; import android.view.View; import android.widget.TimePicker; +import timber.log.Timber; + public class TimePreference extends DialogPreference { private int lastValue = 0; @@ -64,6 +66,7 @@ public class TimePreference extends DialogPreference { try { val = Integer.parseInt(defaultValue.toString()); } catch (NumberFormatException e) { + Timber.e(e, "TimePreference's defaultValue is not a number"); } } diff --git app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java index 7e26254..f289d76 100644 --- app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java +++ app/src/main/java/com/liato/bankdroid/appwidget/AutoRefreshService.java @@ -394,6 +394,7 @@ public class AutoRefreshService extends Service { refreshWidgets = true; db.disableBank(bank.getDbId()); } catch (BankChoiceException e) { + Timber.w(e, "BankChoiceException"); } catch (Exception e) { Timber.e(e, "An unexpected error occurred while updating bank %s", bank.getShortName()); } diff --git app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java index 17c19ae..fd0011c 100644 --- app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java +++ app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java @@ -395,6 +395,7 @@ public abstract class BankdroidWidgetProvider extends AppWidgetProvider { Timber.w(e, "Invalid credentials for bank %s", bank.getShortName()); DBAdapter.disable(bank, context); } catch (BankChoiceException e) { + Timber.w(e, "BankChoiceException"); } catch (IOException e) { if (NetworkUtils.isInternetAvailable()) { Timber.e(e, "Could not update bank %s", bank.getShortName()); diff --git app/src/main/java/com/liato/bankdroid/banking/BankFactory.java app/src/main/java/com/liato/bankdroid/banking/BankFactory.java index b1a8888..9a7ea9c 100644 --- app/src/main/java/com/liato/bankdroid/banking/BankFactory.java +++ app/src/main/java/com/liato/bankdroid/banking/BankFactory.java @@ -99,7 +99,7 @@ public class BankFactory { } banks.add(bank); } catch (BankException e) { - //e.printStackTrace(); + Timber.w(e, "BankFactory.banksFromDb()"); } } c.close(); @@ -168,7 +168,8 @@ public class BankFactory { account.setAliasfor(c.getString(c.getColumnIndex("aliasfor"))); accounts.add(account); } catch (ArrayIndexOutOfBoundsException e) { - // Attempted to load an account without and ID, probably an old Avanza account. + // Probably an old Avanza account + Timber.w(e, "Attempted to load an account without an ID: %d", bankId); } } c.close(); diff --git app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java index 2de42b7..87a0c1d 100644 --- app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java +++ app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java @@ -236,10 +236,9 @@ public class ColorPickerPreference } mValue = color; setPreviewColor(); - try { - getOnPreferenceChangeListener().onPreferenceChange(this, color); - } catch (NullPointerException e) { - + OnPreferenceChangeListener listener = getOnPreferenceChangeListener(); + if (listener != null) { + listener.onPreferenceChange(this, color); } } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/Helpers.java bankdroid-legacy/src/main/java/com/liato/bankdroid/Helpers.java index 2c2e4ce..2ea759a 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/Helpers.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/Helpers.java @@ -142,7 +142,7 @@ public class Helpers { .getMethod("overridePendingTransition", int.class, int.class); method.invoke(activity, in, out); } catch (Exception e) { - // Can't change animation, so do nothing + Timber.w(e, "Can't change animation, so do nothing"); } } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankHelper.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankHelper.java index 82ad171..496f9e4 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankHelper.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/LegacyBankHelper.java @@ -6,6 +6,8 @@ import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; +import timber.log.Timber; + public class LegacyBankHelper { private static Map legacyProviderReferences; @@ -39,7 +41,7 @@ public class LegacyBankHelper { references.put(legacyId, reference); legacyIds.put(reference, legacyId); } catch(IllegalAccessException e) { - //TODO log if provider could not be mapped. + Timber.e(e, "Provider could not be mapped"); } } legacyProviderReferences = legacyIds; diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Jojo.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Jojo.java index b3c620f..fddf1bb 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Jojo.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/Jojo.java @@ -42,6 +42,7 @@ import java.util.List; import eu.nullbyte.android.urllib.CertificateReader; import eu.nullbyte.android.urllib.Urllib; +import timber.log.Timber; public class Jojo extends Bank { @@ -141,6 +142,7 @@ public class Jojo extends Bank { } } catch (IOException e) { // Ignore and defaults to zero + Timber.w(e, "Getting Jojo card balance failed"); } return BigDecimal.ZERO; } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java index d1ece28..6969afa 100644 --- bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/banking/banks/lansforsakringar/Lansforsakringar.java @@ -130,10 +130,9 @@ public class Lansforsakringar extends Bank { try { is.close(); } catch(IOException e) { - // Ignore + Timber.w(e, "Closing JSON stream failed"); } } - } private T readJsonValue(String url, String postData, Class valueType) @@ -235,8 +234,7 @@ public class Lansforsakringar extends Bank { } account.setTransactions(transactions); } catch (BankException e) { - // No transactions for account if this fails. - // readJsonValue will print the stack trace + Timber.e(e, "Failed updating Länsförsäkringar transactions"); } super.updateComplete(); diff --git bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertPinningSSLSocketFactory.java bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertPinningSSLSocketFactory.java index 2a87b49..616084c 100644 --- bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertPinningSSLSocketFactory.java +++ bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertPinningSSLSocketFactory.java @@ -46,6 +46,8 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; +import timber.log.Timber; + public class CertPinningSSLSocketFactory extends SSLSocketFactory { private SSLContext sslcontext = null; @@ -133,7 +135,9 @@ public class CertPinningSSLSocketFactory extends SSLSocketFactory { // close the socket before re-throwing the exception try { sslsock.close(); - } catch (Exception x) { /*ignore*/ } + } catch (Exception e) { + Timber.w(e, "Error closing SSL socket (ignored)"); + } throw iox; } return sslsock; diff --git bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertificateReader.java bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertificateReader.java index 4bb2e20..99d7338 100644 --- bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertificateReader.java +++ bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/CertificateReader.java @@ -5,9 +5,6 @@ import android.content.Context; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -21,7 +18,7 @@ public class CertificateReader { public static Certificate[] getCertificates(Context context, int... rawResCerts) { - List certificates = new ArrayList(); + List certificates = new ArrayList<>(); try { CertificateFactory cf = CertificateFactory.getInstance("X.509"); for (int resId : rawResCerts) { @@ -43,26 +40,4 @@ public class CertificateReader { } return certificates.toArray(new Certificate[certificates.size()]); } - - public static ClientCertificate getClientCertificate(Context context, int rawResCert, - String password) { - InputStream is = null; - try { - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - is = new BufferedInputStream(context.getResources().openRawResource(rawResCert)); - keyStore.load(is, password.toCharArray()); - return new ClientCertificate(keyStore, password); - } catch (IOException | NoSuchAlgorithmException | CertificateException | KeyStoreException e) { - Timber.w(e, "Failed to get client certificate"); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - //noop - } - } - } - return null; - } } diff --git bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java index 803be41..2636816 100644 --- bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java +++ bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java @@ -16,6 +16,7 @@ package eu.nullbyte.android.urllib; +import com.liato.bankdroid.legacy.BuildConfig; import com.liato.bankdroid.legacy.R; import com.liato.bankdroid.utils.ExceptionUtils; @@ -65,8 +66,6 @@ import org.apache.http.util.EntityUtils; import android.content.Context; import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.Build; import android.preference.PreferenceManager; @@ -396,22 +395,12 @@ public class Urllib { private String createUserAgentString() { String appName = mContext.getString(R.string.app_name); - String packageName = ""; - String appVersion = ""; - - try { - PackageInfo packageInfo = mContext.getPackageManager() - .getPackageInfo(mContext.getPackageName(), PackageManager.GET_CONFIGURATIONS); - packageName = packageInfo.packageName; - appVersion = packageInfo.versionName; - } catch (PackageManager.NameNotFoundException ignore) { - } Configuration config = mContext.getResources().getConfiguration(); return String .format("%1$s/%2$s (%3$s; U; Android %4$s; %5$s-%6$s; %10$s Build/%7$s; %8$s) %9$s %10$s" , appName - , appVersion + , BuildConfig.VERSION_NAME , System.getProperty("os.name", "Linux") , Build.VERSION.RELEASE , config.locale.getLanguage().toLowerCase() diff --git config/quality/pmd/pmd-ruleset.xml config/quality/pmd/pmd-ruleset.xml index 9dbddb2..3109e6b 100644 --- config/quality/pmd/pmd-ruleset.xml +++ config/quality/pmd/pmd-ruleset.xml @@ -39,7 +39,6 @@ - @@ -69,7 +68,6 @@ - commit 8e477f6eef16910b02b85f66f7a1402a30087bb2 (tag: v1.9.11) Author: Mathias Åhsberg Date: Wed Oct 26 21:12:58 2016 +0200 Create release v1.9.11 diff --git CHANGELOG CHANGELOG index 5d85914..fa02959 100644 --- CHANGELOG +++ CHANGELOG @@ -1,10 +1,22 @@ Please view this file on the master branch, on stable branches it's out of date. -Not yet released +v1.9.11 (2016-10-26) * Warn about disabled banks in the transactions list * Show warning text about disabled banks in the main activity -* Remove TrustBuddy since they're no longer in business -* Remove unused class MobilbankenBase +* Removes support for TrustBuddy since they're no longer in business +* Removes support for Audi since they require MobiltBankId +* Removes support for VolvoFinans since they require MobiltBankId +* Removes support for EasyCard since they require MobiltBankId +* Removes support for Preem since they require MobiltBankId +* Removes support for ResursBank since they require MobiltBankId +* Removes support for Seat since they require MobiltBankId +* Removes support for Shell since they require MobiltBankId +* Removes support for Skoda since they require MobiltBankId +* Removes support for SupremeCard since they require MobiltBankId +* Removes support for Villabanken since they require MobiltBankId +* Removes support for Volkswagen since they require MobiltBankId +* Merged NordnetDirekt with Nordnet. +* Merged AvanzaMini with Avanza. v1.9.10.10 (2016-10-17) * Fixes crash for Amex commit d42dea167a24ae90c840fcb7d6c0cb386f4db441 Author: Mathias Åhsberg Date: Wed Oct 26 20:53:40 2016 +0200 Upgrade gradle build tools to latest stable. diff --git app/build.gradle app/build.gradle index da46be5..aafa141 100644 --- app/build.gradle +++ app/build.gradle @@ -7,8 +7,12 @@ buildscript { } dependencies { - classpath 'io.fabric.tools:gradle:1.+' classpath "org.ajoberstar:gradle-git:1.5.1" + + // The dynamic version here is explicitly recommended by the Crashlytics docs: + // https://docs.fabric.io/android/fabric/integration.html#modify-build-gradle + //noinspection GradleDynamicVersion + classpath 'io.fabric.tools:gradle:1.+' } } apply plugin: 'com.android.application' @@ -83,7 +87,7 @@ dependencies { compile project(':bankdroid-core') compile 'com.jakewharton:butterknife:6.1.0' compile 'com.jakewharton.timber:timber:4.3.1' - compile "com.android.support:appcompat-v7:24.1.1" + compile "com.android.support:appcompat-v7:24.2.1" compile 'com.google.collections:google-collections:1.0' compile('com.crashlytics.sdk.android:crashlytics:2.6.5@aar') { transitive = true; diff --git bankdroid-legacy/build.gradle bankdroid-legacy/build.gradle index 943e966..2cda778 100644 --- bankdroid-legacy/build.gradle +++ bankdroid-legacy/build.gradle @@ -27,14 +27,14 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile project(':bankdroid-interface') - compile 'com.android.support:appcompat-v7:24.1.1' + compile 'com.android.support:appcompat-v7:24.2.1' compile 'com.jakewharton.timber:timber:4.3.1' compile ('org.apache.commons:commons-io:1.3.2') {exclude module: 'commons-io'} compile 'org.jsoup:jsoup:1.7.3' compile 'com.fasterxml.jackson.core:jackson-core:2.1.0' compile 'com.fasterxml.jackson.core:jackson-databind:2.1.0' compile 'com.fasterxml.jackson.core:jackson-annotations:2.1.0' - compile('org.simpleframework:simple-xml:2.7.+') { + compile('org.simpleframework:simple-xml:2.7.1') { exclude module: 'stax' exclude module: 'stax-api' exclude module: 'xpp3' diff --git build.gradle build.gradle index 8f0f520..43763d6 100644 --- build.gradle +++ build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.1' + classpath 'com.android.tools.build:gradle:2.2.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git config/quality/lint/lint.xml config/quality/lint/lint.xml index 8f68763..8195ce2 100644 --- config/quality/lint/lint.xml +++ config/quality/lint/lint.xml @@ -1,5 +1,8 @@ + + + @@ -12,8 +15,6 @@ - - diff --git tools/update-suppressions.sh tools/update-suppressions.sh index 363091b..4148ea6 100755 --- tools/update-suppressions.sh +++ tools/update-suppressions.sh @@ -21,6 +21,9 @@ function set_lint_suppressions() { cat > ${LINT_XML} << EOF + + + EOF commit f999a2aa8ded6f873c6ab8e8502255e3f7409675 Author: Johan Walles Date: Tue Oct 25 21:31:33 2016 +0200 Fake Urllib exception stack traces In Crashlytics we have a lot of stack traces originating in Urllib. With this change in place, those exceptions will appear to be originating from whatever bank tried to call Urllib, and it will be much more obvious in Crashlytics which banks actually have the most problems. diff --git bankdroid-legacy/build.gradle bankdroid-legacy/build.gradle index a5dd3f4..943e966 100644 --- bankdroid-legacy/build.gradle +++ bankdroid-legacy/build.gradle @@ -39,4 +39,7 @@ dependencies { exclude module: 'stax-api' exclude module: 'xpp3' } + + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' } diff --git bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/ExceptionUtils.java bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/ExceptionUtils.java new file mode 100644 index 0000000..e61097b --- /dev/null +++ bankdroid-legacy/src/main/java/com/liato/bankdroid/utils/ExceptionUtils.java @@ -0,0 +1,67 @@ +package com.liato.bankdroid.utils; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; + +import timber.log.Timber; + +public class ExceptionUtils { + private static final String PREFIX = "com.liato.bankdroid."; + + /** + * Take an exception thrown and make it look like it came from Bankdroid. + *

+ * Specifically, if Urllib.java, called by Bankdroid code, throws an exception, + * rewrite the exception so that it appears as if it was thrown from the + * Bankdroid method calling Urllib, but caused by the original Exception. + */ + public static T bankdroidifyException(T exception) { + StackTraceElement[] bankdroidifiedStacktrace = + bankdroidifyStacktrace(exception.getStackTrace()); + if (bankdroidifiedStacktrace.length == exception.getStackTrace().length) { + // Unable to bankdroidify stacktrace, never mind + return exception; + } + + T returnMe; + try { + returnMe = (T)exception.getClass().getConstructor(String.class) + .newInstance(exception.getMessage()); + } catch (InstantiationException e) { + Timber.e(e, "Unable to Bankdroidify exception of type %s", exception.getClass()); + return exception; + } catch (InvocationTargetException e) { + Timber.e(e, "Unable to Bankdroidify exception of type %s", exception.getClass()); + return exception; + } catch (IllegalAccessException e) { + Timber.e(e, "Unable to Bankdroidify exception of type %s", exception.getClass()); + return exception; + } catch (NoSuchMethodException e) { + Timber.e(e, "Unable to Bankdroidify exception of type %s", exception.getClass()); + return exception; + } + + returnMe.initCause(exception); + + returnMe.setStackTrace(bankdroidifiedStacktrace); + + return returnMe; + } + + /** + * Remove all initial non-Bankdroid frames from a stack. + * + * @return A copy of rawStack but with the initial non-Bankdroid frames removed + */ + private static StackTraceElement[] bankdroidifyStacktrace(final StackTraceElement[] rawStack) { + for (int i = 0; i < rawStack.length; i++) { + StackTraceElement stackTraceElement = rawStack[i]; + if (stackTraceElement.getClassName().startsWith(PREFIX)) { + return Arrays.copyOfRange(rawStack, i, rawStack.length); + } + } + + // No Bankdroid stack frames found, never mind + return rawStack; + } +} diff --git bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java index 2cc5e36..803be41 100644 --- bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java +++ bankdroid-legacy/src/main/java/eu/nullbyte/android/urllib/Urllib.java @@ -17,6 +17,7 @@ package eu.nullbyte.android.urllib; import com.liato.bankdroid.legacy.R; +import com.liato.bankdroid.utils.ExceptionUtils; import org.apache.http.ConnectionReuseStrategy; import org.apache.http.HttpEntity; @@ -146,7 +147,11 @@ public class Urllib { } public String open(String url) throws ClientProtocolException, IOException { - return this.open(url, new ArrayList()); + try { + return this.open(url, new ArrayList()); + } catch (IOException e) { + throw ExceptionUtils.bankdroidifyException(e); + } } public String post(String url) throws ClientProtocolException, IOException { @@ -155,7 +160,11 @@ public class Urllib { public String open(String url, List postData) throws ClientProtocolException, IOException { - return open(url, postData, false); + try { + return open(url, postData, false); + } catch (IOException e) { + throw ExceptionUtils.bankdroidifyException(e); + } } public String open(String url, List postData, boolean forcePost) @@ -171,7 +180,11 @@ public class Urllib { boolean forcePost) throws ClientProtocolException, IOException { HttpEntity entity = (postData == null || postData.isEmpty()) && !forcePost ? null : new UrlEncodedFormEntity(postData, this.charset); - return openAsHttpResponse(url, entity, forcePost); + try { + return openAsHttpResponse(url, entity, forcePost); + } catch (IOException e) { + throw ExceptionUtils.bankdroidifyException(e); + } } public HttpResponse openAsHttpResponse(String url, boolean forcePost) @@ -181,10 +194,14 @@ public class Urllib { public HttpResponse openAsHttpResponse(String url, HttpEntity entity, boolean forcePost) throws ClientProtocolException, IOException { - if ((entity == null) && !forcePost) { - return openAsHttpResponse(url, entity, HttpMethod.GET); - } else { - return openAsHttpResponse(url, entity, HttpMethod.POST); + try { + if ((entity == null) && !forcePost) { + return openAsHttpResponse(url, entity, HttpMethod.GET); + } else { + return openAsHttpResponse(url, entity, HttpMethod.POST); + } + } catch (IOException e) { + throw ExceptionUtils.bankdroidifyException(e); } } @@ -273,8 +290,12 @@ public class Urllib { public InputStream openStream(String url, String postData, boolean forcePost) throws ClientProtocolException, IOException { - return openStream(url, postData != null ? new StringEntity(postData, this.charset) : null, - forcePost); + try { + return openStream(url, postData != null ? new StringEntity(postData, this.charset) : null, + forcePost); + } catch (IOException e) { + throw ExceptionUtils.bankdroidifyException(e); + } } public InputStream openStream(String url, HttpEntity postData, boolean forcePost) diff --git bankdroid-legacy/src/test/java/com/liato/bankdroid/utils/ExceptionUtilsTest.java bankdroid-legacy/src/test/java/com/liato/bankdroid/utils/ExceptionUtilsTest.java new file mode 100644 index 0000000..803dc5f --- /dev/null +++ bankdroid-legacy/src/test/java/com/liato/bankdroid/utils/ExceptionUtilsTest.java @@ -0,0 +1,41 @@ +package com.liato.bankdroid.utils; + +import org.junit.Assert; +import org.junit.Test; + +import eu.nullbyte.android.urllib.Urllib; + +public class ExceptionUtilsTest { + @Test + @SuppressWarnings("PMD") // This is for the stack trace printing, we really want to do it here + public void bankdroidifyException() throws Exception { + Exception raw = null; + try { + new Urllib(null); + Assert.fail("Exception expected"); + } catch (NullPointerException e) { + raw = e; + } + + // Print stack traces, useful if the tests fail + System.err.println("Before:"); + raw.printStackTrace(); + + System.err.println(); + System.err.println("After:"); + Exception bankdroidified = ExceptionUtils.bankdroidifyException(raw); + bankdroidified.printStackTrace(); + + Assert.assertFalse("Test setup: Top frame of initial exception shouldn't be in Bankdroid", + raw.getStackTrace()[0].getClassName().startsWith("com.liato.bankdroid.")); + + Assert.assertTrue("Top frame of bankdroidified exception should be in Bankdroid", + bankdroidified.getStackTrace()[0].getClassName().startsWith("com.liato.bankdroid.")); + + // Verify that e is the cause of bankdroidified + Assert.assertSame(raw, bankdroidified.getCause()); + + // Verify that re-bankdroidifying is a no-op + Assert.assertSame(bankdroidified, ExceptionUtils.bankdroidifyException(bankdroidified)); + } +} commit b75ce9788c33d9d6ef70bfe6fbb3fae8647f9232 Author: Johan Walles Date: Thu Oct 20 20:04:51 2016 +0200 Show warning for disabled banks Before this change, in the banks list in the main UI, disabled banks were marked with a warning icon. This change adds to that warning icon a text describing what's actually going on and what the user can do about it. Before starting to work on this I fixed all Android Lint reported warnings in the .java and .xml files I had to change, and re-enabled those warnings. One of those warnings was about using sp rather than dp for text. I suppressed that warning for the widgets, but heeded it in all other places. This results in a UI that better adapts to the user's font size preferences. The UI changes I have tested with both "small" and "huge" font size, and it looks fine. diff --git CHANGELOG CHANGELOG index 858714a..5d85914 100644 --- CHANGELOG +++ CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. Not yet released * Warn about disabled banks in the transactions list +* Show warning text about disabled banks in the main activity * Remove TrustBuddy since they're no longer in business * Remove unused class MobilbankenBase diff --git app/src/main/java/com/liato/bankdroid/adapters/AccountsAdapter.java app/src/main/java/com/liato/bankdroid/adapters/AccountsAdapter.java index af4e6ec..9413be0 100644 --- app/src/main/java/com/liato/bankdroid/adapters/AccountsAdapter.java +++ app/src/main/java/com/liato/bankdroid/adapters/AccountsAdapter.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; import android.preference.PreferenceManager; +import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -42,50 +43,30 @@ public class AccountsAdapter extends BaseAdapter { public final static int VIEWTYPE_EMPTY = 2; - SharedPreferences prefs; + private final SharedPreferences prefs; private ArrayList banks; - private Context context; - - private LayoutInflater inflater; + private final LayoutInflater inflater; private boolean showHidden; public AccountsAdapter(Context context, boolean showHidden) { - this.context = context; - this.banks = new ArrayList(); - inflater = LayoutInflater.from(this.context); + this.banks = new ArrayList<>(); + inflater = LayoutInflater.from(context); this.showHidden = showHidden; prefs = PreferenceManager.getDefaultSharedPreferences(context); } - public void addGroup(Bank bank) { - banks.add(bank); - } - public void setGroups(ArrayList banks) { this.banks = banks; - /*for (Bank b : this.banks) { - ArrayList as = b.getAccounts(); - for (Account a : as) { - if (a.isHidden() && !showHidden) { - as.remove(a); - } - - } - }*/ - } - - public boolean isShowHidden() { - return showHidden; } public void setShowHidden(boolean showHidden) { this.showHidden = showHidden; } - public View newBankView(Bank bank, ViewGroup parent, View convertView) { + private View newBankView(Bank bank, ViewGroup parent, View convertView) { if (convertView == null) { convertView = inflater.inflate(R.layout.listitem_accounts_group, parent, false); } @@ -103,16 +84,16 @@ public class AccountsAdapter extends BaseAdapter { bank.getDecimalFormatter(), false)); icon.setImageResource(bank.getImageResource()); - ImageView warning = (ImageView) convertView.findViewById(R.id.imgWarning); + View warning = convertView.findViewById(R.id.txtDisabledWarningX); if (bank.isDisabled()) { warning.setVisibility(View.VISIBLE); } else { - warning.setVisibility(View.INVISIBLE); + warning.setVisibility(View.GONE); } return convertView; } - public View newAccountView(Account account, ViewGroup parent, View convertView) { + private View newAccountView(Account account, ViewGroup parent, View convertView) { if ((account.isHidden() && !showHidden) || account.getBank().getHideAccounts()) { return convertView == null ? inflater.inflate(R.layout.empty, parent, false) : convertView; @@ -158,6 +139,7 @@ public class AccountsAdapter extends BaseAdapter { } @Override + @Nullable public Object getItem(int position) { if (banks.size() == 0) { return null; @@ -188,19 +170,21 @@ public class AccountsAdapter extends BaseAdapter { } @Override + @Nullable public View getView(int position, View convertView, ViewGroup parent) { Object item = getItem(position); if (item == null) { return null; } if (item instanceof Bank) { - return newBankView((Bank) item, parent, convertView); + return newBankView((Bank)item, parent, convertView); } else if (item instanceof Account) { - return newAccountView((Account) item, parent, convertView); + return newAccountView((Account)item, parent, convertView); } return null; } + @Override public boolean isEnabled(int position) { if (getItemViewType(position) == VIEWTYPE_EMPTY) { return false; @@ -208,7 +192,6 @@ public class AccountsAdapter extends BaseAdapter { return true; } - @Override public int getViewTypeCount() { return 3; @@ -220,8 +203,9 @@ public class AccountsAdapter extends BaseAdapter { if (item instanceof Bank) { return VIEWTYPE_BANK; } else { - if ((((Account) item).isHidden() && !showHidden) || - ((Account) item).getBank().getHideAccounts()) { + final Account account = (Account)item; + if ((account.isHidden() && !showHidden) || + account.getBank().getHideAccounts()) { return VIEWTYPE_EMPTY; } } diff --git app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java index fb3ddf2..17c19ae 100644 --- app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java +++ app/src/main/java/com/liato/bankdroid/appwidget/BankdroidWidgetProvider.java @@ -166,7 +166,7 @@ public abstract class BankdroidWidgetProvider extends AppWidgetProvider { //intent = new Intent(context, AccountsActivity.class); pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); - views.setOnClickPendingIntent(R.id.imgWarning, pendingIntent); + views.setOnClickPendingIntent(R.id.layWidgetContainer, pendingIntent); intent = new Intent(context, WidgetService.class); intent.setAction(AutoRefreshService.BROADCAST_WIDGET_REFRESH); diff --git app/src/main/res/layout-land/choose_lock_pattern.xml app/src/main/res/layout-land/choose_lock_pattern.xml index e32dc0a..a98f3a9 100644 --- app/src/main/res/layout-land/choose_lock_pattern.xml +++ app/src/main/res/layout-land/choose_lock_pattern.xml @@ -1,10 +1,10 @@ - + android:text="@string/lockpattern_confirm_button_text" + android:maxLines="1"/> @@ -58,8 +58,8 @@ android:layout_above="@id/footerRightButton" android:layout_centerHorizontal="true" android:ellipsize="marquee" - android:singleLine="true" - android:text="@string/lockpattern_restart_button_text" /> + android:text="@string/lockpattern_restart_button_text" + android:maxLines="1"/> diff --git app/src/main/res/layout-land/dialog_color_picker.xml app/src/main/res/layout-land/dialog_color_picker.xml index 7f63aff..fc0caa3 100644 --- app/src/main/res/layout-land/dialog_color_picker.xml +++ app/src/main/res/layout-land/dialog_color_picker.xml @@ -4,9 +4,9 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -14,30 +14,31 @@ limitations under the License. --> - - + - + - + - + - + tools:ignore="HardcodedText"/> + - + - - \ No newline at end of file + + diff --git app/src/main/res/layout/about.xml app/src/main/res/layout/about.xml index 82dda73..4faa0a1 100644 --- app/src/main/res/layout/about.xml +++ app/src/main/res/layout/about.xml @@ -26,14 +26,14 @@ android:id="@+id/imgTextLogo" android:layout_marginTop="20dp" android:scaleType="fitXY" android:layout_width="wrap_content" android:layout_centerHorizontal="true"> - + + android:layout_width="fill_parent" android:textSize="20sp"> - + + + +